From c72a47d0f010ccebef8c033f84a0260056087a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 11 Sep 2025 18:53:26 +0200 Subject: [PATCH 01/29] test: fix non-compiling tests --- conn_with_mockserver_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/conn_with_mockserver_test.go b/conn_with_mockserver_test.go index 2a6aeece..848ee24a 100644 --- a/conn_with_mockserver_test.go +++ b/conn_with_mockserver_test.go @@ -154,12 +154,12 @@ func TestExecuteBegin(t *testing.T) { t.Fatal(err) } - requests := drainRequestsFromServer(server.TestSpanner) - beginRequests := requestsOfType(requests, reflect.TypeOf(&spannerpb.BeginTransactionRequest{})) + requests := server.TestSpanner.DrainRequestsFromServer() + beginRequests := testutil.RequestsOfType(requests, reflect.TypeOf(&spannerpb.BeginTransactionRequest{})) if g, w := len(beginRequests), 0; g != w { t.Fatalf("begin requests count mismatch\n Got: %v\nWant: %v", g, w) } - executeRequests := requestsOfType(requests, reflect.TypeOf(&spannerpb.ExecuteSqlRequest{})) + executeRequests := testutil.RequestsOfType(requests, reflect.TypeOf(&spannerpb.ExecuteSqlRequest{})) if g, w := len(executeRequests), 1; g != w { t.Fatalf("execute requests count mismatch\n Got: %v\nWant: %v", g, w) } @@ -167,8 +167,8 @@ func TestExecuteBegin(t *testing.T) { if request.GetTransaction() == nil || request.GetTransaction().GetBegin() == nil { t.Fatal("missing begin transaction on ExecuteSqlRequest") } - commitRequests := requestsOfType(requests, reflect.TypeOf(&spannerpb.CommitRequest{})) - rollbackRequests := requestsOfType(requests, reflect.TypeOf(&spannerpb.RollbackRequest{})) + commitRequests := testutil.RequestsOfType(requests, reflect.TypeOf(&spannerpb.CommitRequest{})) + rollbackRequests := testutil.RequestsOfType(requests, reflect.TypeOf(&spannerpb.RollbackRequest{})) if end == "commit" { if g, w := len(commitRequests), 1; g != w { t.Fatalf("commit requests count mismatch\n Got: %v\nWant: %v", g, w) From 3267dd784cb0b768070e7469aa10eb43185c5818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 11 Sep 2025 19:28:21 +0200 Subject: [PATCH 02/29] feat: add TimestampBound option to ExecOptions Add a TimestampBound option to ExecOptions to make it easier to set the read staleness for a single-use read-only transaction. --- conn.go | 14 ++++++++++++++ conn_with_mockserver_test.go | 29 +++++++++++++++++++++++++++++ driver.go | 7 +++++++ 3 files changed, 50 insertions(+) diff --git a/conn.go b/conn.go index 7f9ece77..a78e856c 100644 --- a/conn.go +++ b/conn.go @@ -401,6 +401,16 @@ func (c *conn) setReadOnlyStaleness(staleness spanner.TimestampBound) (driver.Re return driver.ResultNoRows, nil } +func (c *conn) readOnlyStalenessPointer() *spanner.TimestampBound { + val := propertyReadOnlyStaleness.GetConnectionPropertyValue(c.state) + if val == nil || !val.HasValue() { + return nil + } + staleness, _ := val.GetValue() + timestampBound := staleness.(spanner.TimestampBound) + return ×tampBound +} + func (c *conn) IsolationLevel() sql.IsolationLevel { return propertyIsolationLevel.GetValueOrDefault(c.state) } @@ -987,6 +997,7 @@ func (c *conn) options(reset bool) *ExecOptions { }, }, PartitionedQueryOptions: PartitionedQueryOptions{}, + TimestampBound: c.readOnlyStalenessPointer(), } if c.tempExecOptions != nil { effectiveOptions.merge(c.tempExecOptions) @@ -1274,6 +1285,9 @@ func (c *conn) rollback(ctx context.Context) error { } func queryInSingleUse(ctx context.Context, c *spanner.Client, statement spanner.Statement, tb spanner.TimestampBound, options *ExecOptions) *spanner.RowIterator { + if options.TimestampBound != nil { + tb = *options.TimestampBound + } return c.Single().WithTimestampBound(tb).QueryWithOptions(ctx, statement, options.QueryOptions) } diff --git a/conn_with_mockserver_test.go b/conn_with_mockserver_test.go index 848ee24a..befa2780 100644 --- a/conn_with_mockserver_test.go +++ b/conn_with_mockserver_test.go @@ -20,6 +20,7 @@ import ( "fmt" "reflect" "testing" + "time" "cloud.google.com/go/longrunning/autogen/longrunningpb" "cloud.google.com/go/spanner" @@ -587,6 +588,34 @@ func TestSetLocalReadLockMode(t *testing.T) { } } +func TestTimestampBound(t *testing.T) { + t.Parallel() + + db, server, teardown := setupTestDBConnection(t) + defer teardown() + ctx := context.Background() + + staleness := spanner.MaxStaleness(10 * time.Second) + row := db.QueryRowContext(ctx, testutil.SelectFooFromBar, ExecOptions{TimestampBound: &staleness}) + if row.Err() != nil { + t.Fatal(row.Err()) + } + var val int64 + if err := row.Scan(&val); err != nil { + t.Fatal(err) + } + + requests := server.TestSpanner.DrainRequestsFromServer() + executeRequests := testutil.RequestsOfType(requests, reflect.TypeOf(&spannerpb.ExecuteSqlRequest{})) + if g, w := len(executeRequests), 1; g != w { + t.Fatalf("execute requests count mismatch\n Got: %v\nWant: %v", g, w) + } + request := executeRequests[0].(*spannerpb.ExecuteSqlRequest) + if g, w := request.Transaction.GetSingleUse().GetReadOnly().GetMaxStaleness().GetSeconds(), int64(10); g != w { + t.Fatalf("read staleness mismatch\n Got: %v\nWant: %v", g, w) + } +} + func TestCreateDatabase(t *testing.T) { t.Parallel() diff --git a/driver.go b/driver.go index e76c5a59..3d38ed55 100644 --- a/driver.go +++ b/driver.go @@ -173,6 +173,10 @@ type ExecOptions struct { TransactionOptions spanner.TransactionOptions // QueryOptions are the query options that will be used for the statement. QueryOptions spanner.QueryOptions + // TimestampBound is the timestamp bound that will be used for the statement + // if it is a query outside a transaction. Setting this option will override + // the default TimestampBound that is set on the connection. + TimestampBound *spanner.TimestampBound // PartitionedQueryOptions are used for partitioned queries, and ignored // for all other statements. @@ -234,6 +238,9 @@ func (dest *ExecOptions) merge(src *ExecOptions) { if src.AutocommitDMLMode != Unspecified { dest.AutocommitDMLMode = src.AutocommitDMLMode } + if src.TimestampBound != nil { + dest.TimestampBound = src.TimestampBound + } (&dest.PartitionedQueryOptions).merge(&src.PartitionedQueryOptions) mergeQueryOptions(&dest.QueryOptions, &src.QueryOptions) mergeTransactionOptions(&dest.TransactionOptions, &src.TransactionOptions) From f23d2ee249f29a907b7c289a450859c8cdd8b3c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 9 Oct 2025 11:41:27 +0200 Subject: [PATCH 03/29] chore: add ADO.NET driver Adds an initial version of an ADO.NET driver. --- .github/workflows/ado-net-tests.yml | 30 + drivers/spanner-ado-net/.gitignore | 4 + drivers/spanner-ado-net/LICENSE | 202 ++++ drivers/spanner-ado-net/README.md | 5 + drivers/spanner-ado-net/global.json | 7 + .../spanner-ado-net-benchmarks/README.md | 5 + .../spanner-ado-net-benchmarks/deploy.txt | 46 + .../spanner-ado-net-benchmarks.csproj | 19 + .../tpcc/LastNameGenerator.cs | 22 + .../tpcc/Program.cs | 158 ++++ .../tpcc/RowNotFoundException.cs | 5 + .../tpcc/SchemaDefinition.cs | 200 ++++ .../tpcc/SchemaUtil.cs | 47 + .../spanner-ado-net-benchmarks/tpcc/Stats.cs | 97 ++ .../tpcc/TpccRunner.cs | 861 ++++++++++++++++++ .../tpcc/loader/CustomerLoader.cs | 112 +++ .../tpcc/loader/DataLoader.cs | 83 ++ .../tpcc/loader/DistrictLoader.cs | 89 ++ .../tpcc/loader/ItemLoader.cs | 90 ++ .../tpcc/loader/StockLoader.cs | 119 +++ .../tpcc/loader/WarehouseLoader.cs | 67 ++ .../spanner-ado-net-samples/Program.cs | 37 + .../spanner-ado-net-samples/README.md | 5 + .../spanner-ado-net-samples.csproj | 17 + .../CommandTests.cs | 123 +++ .../ConnectionStringTests.cs | 19 + .../ConnectionTests.cs | 19 + .../DataReaderTests.cs | 107 +++ .../DbFactoryFixture.cs | 441 +++++++++ .../DbFactoryTests.cs | 19 + .../DbProviderFactoryTests.cs | 19 + .../GetValueConversionTests.cs | 138 +++ .../ParameterTests.cs | 83 ++ .../README.md | 5 + .../TransactionTests.cs | 19 + .../appsettings.json | 8 + ...spanner-ado-net-specification-tests.csproj | 31 + .../AbstractMockServerTests.cs | 57 ++ .../spanner-ado-net-tests/BasicTests.cs | 111 +++ .../spanner-ado-net-tests/BatchTests.cs | 272 ++++++ .../spanner-ado-net-tests/CommandTests.cs | 178 ++++ .../spanner-ado-net-tests/ConnectionTests.cs | 177 ++++ .../spanner-ado-net-tests/README.md | 5 + .../spanner-ado-net-tests/TransactionTests.cs | 182 ++++ .../spanner-ado-net-tests/appsettings.json | 8 + .../spanner-ado-net-tests.csproj | 32 + drivers/spanner-ado-net/spanner-ado-net.sln | 40 + .../spanner-ado-net/AssemblyInfo.cs | 3 + .../spanner-ado-net/spanner-ado-net/README.md | 5 + .../spanner-ado-net/SpannerBatch.cs | 129 +++ .../spanner-ado-net/SpannerBatchCommand.cs | 34 + .../SpannerBatchCommandCollection.cs | 95 ++ .../spanner-ado-net/SpannerCommand.cs | 260 ++++++ .../spanner-ado-net/SpannerCommandBuilder.cs | 46 + .../spanner-ado-net/SpannerConnection.cs | 290 ++++++ .../SpannerConnectionStringBuilder.cs | 22 + .../spanner-ado-net/SpannerDataAdapter.cs | 22 + .../spanner-ado-net/SpannerDataReader.cs | 825 +++++++++++++++++ .../spanner-ado-net/SpannerDataSource.cs | 33 + .../spanner-ado-net/SpannerDbException.cs | 42 + .../spanner-ado-net/SpannerFactory.cs | 94 ++ .../spanner-ado-net/SpannerParameter.cs | 186 ++++ .../SpannerParameterCollection.cs | 218 +++++ .../spanner-ado-net/SpannerPool.cs | 97 ++ .../spanner-ado-net/SpannerTransaction.cs | 149 +++ .../spanner-ado-net/TypeConversion.cs | 87 ++ .../spanner-ado-net/publish.sh | 11 + .../spanner-ado-net/spanner-ado-net.csproj | 28 + 68 files changed, 7096 insertions(+) create mode 100644 .github/workflows/ado-net-tests.yml create mode 100644 drivers/spanner-ado-net/.gitignore create mode 100644 drivers/spanner-ado-net/LICENSE create mode 100644 drivers/spanner-ado-net/README.md create mode 100644 drivers/spanner-ado-net/global.json create mode 100644 drivers/spanner-ado-net/spanner-ado-net-benchmarks/README.md create mode 100644 drivers/spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt create mode 100644 drivers/spanner-ado-net/spanner-ado-net-benchmarks/spanner-ado-net-benchmarks.csproj create mode 100644 drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/LastNameGenerator.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Program.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/RowNotFoundException.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/SchemaDefinition.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/SchemaUtil.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Stats.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/TpccRunner.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/CustomerLoader.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/DataLoader.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/DistrictLoader.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/ItemLoader.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/StockLoader.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/WarehouseLoader.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/Program.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/README.md create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/spanner-ado-net-samples.csproj create mode 100644 drivers/spanner-ado-net/spanner-ado-net-specification-tests/CommandTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-specification-tests/ConnectionStringTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-specification-tests/ConnectionTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-specification-tests/DataReaderTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-specification-tests/DbFactoryFixture.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-specification-tests/DbFactoryTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-specification-tests/DbProviderFactoryTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-specification-tests/GetValueConversionTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-specification-tests/ParameterTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-specification-tests/README.md create mode 100644 drivers/spanner-ado-net/spanner-ado-net-specification-tests/TransactionTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-specification-tests/appsettings.json create mode 100644 drivers/spanner-ado-net/spanner-ado-net-specification-tests/spanner-ado-net-specification-tests.csproj create mode 100644 drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-tests/BasicTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-tests/BatchTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-tests/README.md create mode 100644 drivers/spanner-ado-net/spanner-ado-net-tests/TransactionTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-tests/appsettings.json create mode 100644 drivers/spanner-ado-net/spanner-ado-net-tests/spanner-ado-net-tests.csproj create mode 100644 drivers/spanner-ado-net/spanner-ado-net.sln create mode 100644 drivers/spanner-ado-net/spanner-ado-net/AssemblyInfo.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net/README.md create mode 100644 drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net/SpannerBatchCommand.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net/SpannerBatchCommandCollection.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net/SpannerCommandBuilder.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net/SpannerConnectionStringBuilder.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net/SpannerDataAdapter.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net/SpannerDataSource.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net/SpannerDbException.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net/SpannerFactory.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net/SpannerParameter.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net/SpannerParameterCollection.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net/SpannerPool.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net/TypeConversion.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net/publish.sh create mode 100644 drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj diff --git a/.github/workflows/ado-net-tests.yml b/.github/workflows/ado-net-tests.yml new file mode 100644 index 00000000..9092004f --- /dev/null +++ b/.github/workflows/ado-net-tests.yml @@ -0,0 +1,30 @@ +on: + push: + branches: [ main ] + pull_request: +permissions: + contents: read + pull-requests: write +name: ADO.NET Tests +jobs: + units: + strategy: + matrix: + dotnet-version: ['8.0.x', '9.0.x'] + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Install dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ matrix.dotnet-version }} + - name: Checkout code + uses: actions/checkout@v4 + - name: spanner-ado-net-tests + working-directory: drivers/spanner-ado-net/spanner-ado-net-tests + run: dotnet test --verbosity normal + shell: bash + - name: spanner-ado-net-specification-tests + working-directory: drivers/spanner-ado-net/spanner-ado-net-specification-tests + run: dotnet test --verbosity normal + shell: bash diff --git a/drivers/spanner-ado-net/.gitignore b/drivers/spanner-ado-net/.gitignore new file mode 100644 index 00000000..808112cd --- /dev/null +++ b/drivers/spanner-ado-net/.gitignore @@ -0,0 +1,4 @@ +.idea +obj +bin +*DotSettings.user diff --git a/drivers/spanner-ado-net/LICENSE b/drivers/spanner-ado-net/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/drivers/spanner-ado-net/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/drivers/spanner-ado-net/README.md b/drivers/spanner-ado-net/README.md new file mode 100644 index 00000000..fbcfda13 --- /dev/null +++ b/drivers/spanner-ado-net/README.md @@ -0,0 +1,5 @@ +# Spanner ADO.NET Data Provider + +ADO.NET Data Provider for Spanner. + +__ALPHA: Not for production use__ diff --git a/drivers/spanner-ado-net/global.json b/drivers/spanner-ado-net/global.json new file mode 100644 index 00000000..2ddda36c --- /dev/null +++ b/drivers/spanner-ado-net/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.0", + "rollForward": "latestMinor", + "allowPrerelease": false + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/README.md b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/README.md new file mode 100644 index 00000000..e7015b23 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/README.md @@ -0,0 +1,5 @@ +# Spanner ADO.NET Data Provider Benchmarks + +Benchmarks for the ADO.NET Data Provider for Spanner. + +__ALPHA: Not for production use__ diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt new file mode 100644 index 00000000..b08e5498 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt @@ -0,0 +1,46 @@ + +gcloud run deploy spannerlib-dotnet-benchmark-tpcc \ + --region=europe-north1 \ + --no-allow-unauthenticated --no-cpu-throttling \ + --min-instances=1 --max-instances=1 \ + --cpu=4 --memory=2Gi \ + --set-env-vars=NUM_WAREHOUSES=100,TRANSACTIONS_PER_SECOND=50,NUM_CLIENTS=50 \ + --base-image dotnet8 \ + --source . + +gcloud run deploy spannerlib-dotnet-benchmark-tpcc \ + --region=europe-north1 \ + --no-allow-unauthenticated --no-cpu-throttling \ + --min-instances=1 --max-instances=1 \ + --cpu=4 --memory=2Gi \ + --set-env-vars=NUM_WAREHOUSES=100,TRANSACTIONS_PER_SECOND=50,NUM_CLIENTS=50,RETRY_ABORTS_INTERNALLY=false \ + --base-image dotnet8 \ + --source . + + +gcloud run deploy spannerlib-dotnet-benchmark-tpcc \ + --region=europe-north1 \ + --no-allow-unauthenticated --no-cpu-throttling \ + --min-instances=1 --max-instances=1 \ + --cpu=4 --memory=2Gi \ + --set-env-vars=NUM_WAREHOUSES=100,CLIENT_TYPE=ClientLib,TRANSACTIONS_PER_SECOND=50,NUM_CLIENTS=50 \ + --base-image dotnet8 \ + --source . + +gcloud run deploy spannerlib-dotnet-benchmark-tpcc \ + --region=europe-north1 \ + --no-allow-unauthenticated --no-cpu-throttling \ + --min-instances=1 --max-instances=1 \ + --cpu=4 --memory=2Gi \ + --set-env-vars=NUM_WAREHOUSES=100,CLIENT_TYPE=NativeSpannerLib,TRANSACTIONS_PER_SECOND=50,NUM_CLIENTS=50 \ + --base-image dotnet8 \ + --source . + +gcloud run deploy spannerlib-dotnet-benchmark-tpcc \ + --region=europe-north1 \ + --no-allow-unauthenticated --no-cpu-throttling \ + --min-instances=1 --max-instances=1 \ + --cpu=4 --memory=2Gi \ + --set-env-vars=NUM_WAREHOUSES=100,CLIENT_TYPE=NativeSpannerLib,TRANSACTIONS_PER_SECOND=50,NUM_CLIENTS=50,RETRY_ABORTS_INTERNALLY=false \ + --base-image dotnet8 \ + --source . diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/spanner-ado-net-benchmarks.csproj b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/spanner-ado-net-benchmarks.csproj new file mode 100644 index 00000000..1c8e0a3d --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/spanner-ado-net-benchmarks.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + Google.Cloud.Spanner.DataProvider.Benchmarks + enable + enable + Google.Cloud.Spanner.DataProvider.Benchmarks + default + + + + + + + + + diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/LastNameGenerator.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/LastNameGenerator.cs new file mode 100644 index 00000000..2179eb61 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/LastNameGenerator.cs @@ -0,0 +1,22 @@ +namespace Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc; + +public static class LastNameGenerator +{ + private static readonly string[] Parts = ["BAR", "OUGHT", "ABLE", "PRI", "PRES", "ESE", "ANTI", "CALLY", "ATION", "EING"]; + + public static string GenerateLastName(long rowIndex) { + int row; + if (rowIndex < 1000L) + { + row = (int) rowIndex; + } + else + { + row = Random.Shared.Next(1000); + } + return Parts[row / 100] + + Parts[row / 10 % 10] + + Parts[row % 10]; + } + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Program.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Program.cs new file mode 100644 index 00000000..62e0d7d1 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Program.cs @@ -0,0 +1,158 @@ +using System.Collections.Concurrent; +using System.Data.Common; +using System.Diagnostics; +using Google.Cloud.Spanner.Admin.Database.V1; +using Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc.loader; +using Microsoft.AspNetCore.Builder; + +namespace Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc; + +public static class Program +{ + enum ClientType + { + SpannerLib, + NativeSpannerLib, + ClientLib, + } + + public static async Task Main(string[] args) + { + var cancellationTokenSource = new CancellationTokenSource(); + var builder = WebApplication.CreateBuilder(args); + var port = Environment.GetEnvironmentVariable("PORT") ?? "8080"; + var url = $"http://0.0.0.0:{port}"; + var app = builder.Build(); + app.MapGet("/", () => { }); + var webapp = app.RunAsync(url); + + var logWaitTime = int.Parse(Environment.GetEnvironmentVariable("LOG_WAIT_TIME") ?? "10"); + var database = Environment.GetEnvironmentVariable("DATABASE") ?? "projects/appdev-soda-spanner-staging/instances/knut-test-ycsb/databases/dotnet-tpcc"; + var retryAbortsInternally = bool.Parse(Environment.GetEnvironmentVariable("RETRY_ABORTS_INTERNALLY") ?? "true"); + var numWarehouses = int.Parse(Environment.GetEnvironmentVariable("NUM_WAREHOUSES") ?? "10"); + var numClients = int.Parse(Environment.GetEnvironmentVariable("NUM_CLIENTS") ?? "10"); + var targetTps = int.Parse(Environment.GetEnvironmentVariable("TRANSACTIONS_PER_SECOND") ?? "0"); + var clientTypeName = Environment.GetEnvironmentVariable("CLIENT_TYPE") ?? "SpannerLib"; + if (!Enum.TryParse(clientTypeName, out ClientType clientType)) + { + throw new ArgumentException($"Unknown client type: {clientTypeName}"); + } + + var connectionString = $"Data Source={database}"; + if (!retryAbortsInternally) + { + connectionString += ";retryAbortsInternally=false"; + } + await using (var connection = new SpannerConnection()) + { + connection.ConnectionString = connectionString; + await connection.OpenAsync(cancellationTokenSource.Token); + + Console.WriteLine("Creating schema..."); + await SchemaUtil.CreateSchemaAsync(connection, DatabaseDialect.Postgresql, cancellationTokenSource.Token); + + Console.WriteLine("Loading data..."); + var loader = new DataLoader(connection, numWarehouses); + await loader.LoadAsync(cancellationTokenSource.Token); + } + + Console.WriteLine("Running benchmark..."); + var stats = new Stats(); + + if (targetTps > 0) + { + var maxWaitTime = 2 * 1000 / targetTps; + Console.WriteLine($"Clients: {numClients}"); + Console.WriteLine($"Transactions per second: {targetTps}"); + Console.WriteLine($"Max wait time: {maxWaitTime}"); + var runners = new BlockingCollection(); + for (var client = 0; client < numClients; client++) + { + runners.Add(await CreateRunnerAsync(clientType, connectionString, stats, numWarehouses, cancellationTokenSource), cancellationTokenSource.Token); + } + var lastLogTime = DateTime.UtcNow; + while (!cancellationTokenSource.IsCancellationRequested) + { + var randomWaitTime = Random.Shared.Next(0, maxWaitTime); + var stopwatch = Stopwatch.StartNew(); + if (runners.TryTake(out var runner, 20_000, cancellationTokenSource.Token)) + { + var source = new CancellationTokenSource(); + source.CancelAfter(TimeSpan.FromSeconds(10)); + var token = source.Token; + stats.RegisterTransactionStarted(); + var task = runner!.RunTransactionAsync(token); + _ = task.ContinueWith(_ => + { + stats.RegisterTransactionCompleted(); + runners.Add(runner, cancellationTokenSource.Token); + task.Dispose(); + }, TaskContinuationOptions.ExecuteSynchronously); + } + else + { + await Console.Error.WriteLineAsync("No runner available"); + } + randomWaitTime -= (int) stopwatch.ElapsedMilliseconds; + if (randomWaitTime > 0) + { + await Task.Delay(TimeSpan.FromMilliseconds(randomWaitTime), cancellationTokenSource.Token); + } + if ((DateTime.UtcNow - lastLogTime).TotalSeconds >= logWaitTime) + { + Console.WriteLine($"Num available runners: {runners.Count}"); + Console.WriteLine($"Thread pool size: {ThreadPool.ThreadCount}"); + stats.LogStats(); + lastLogTime = DateTime.UtcNow; + } + } + } + else + { + var tasks = new List(); + for (var client = 0; client < numClients; client++) + { + var runner = await CreateRunnerAsync(clientType, connectionString, stats, numWarehouses, cancellationTokenSource); + tasks.Add(runner.RunAsync(cancellationTokenSource.Token)); + } + while (!cancellationTokenSource.Token.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(logWaitTime), cancellationTokenSource.Token); + stats.LogStats(); + } + await Task.WhenAll(tasks); + } + + await app.StopAsync(cancellationTokenSource.Token); + await webapp; + } + + private static async Task CreateRunnerAsync( + ClientType clientType, + string connectionString, + Stats stats, + int numWarehouses, + CancellationTokenSource cancellationTokenSource) + { + DbConnection connection; + if (clientType == ClientType.SpannerLib) + { + connection = new SpannerConnection(); + } + else if (clientType == ClientType.NativeSpannerLib) + { + connection = new SpannerConnection {UseNativeLibrary = true}; + } + else if (clientType == ClientType.ClientLib) + { + connection = new Google.Cloud.Spanner.Data.SpannerConnection(); + } + else + { + throw new ArgumentException($"Unknown client type: {clientType}"); + } + connection.ConnectionString = connectionString; + await connection.OpenAsync(cancellationTokenSource.Token); + return new TpccRunner(stats, connection, numWarehouses); + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/RowNotFoundException.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/RowNotFoundException.cs new file mode 100644 index 00000000..f1b3530b --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/RowNotFoundException.cs @@ -0,0 +1,5 @@ +using System.Data.Common; + +namespace Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc; + +public class RowNotFoundException(string message) : DbException(message); \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/SchemaDefinition.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/SchemaDefinition.cs new file mode 100644 index 00000000..dff383ea --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/SchemaDefinition.cs @@ -0,0 +1,200 @@ +namespace Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc; + +public static class SchemaDefinition +{ + public const string CreateTablesPostgreSql = @" +START BATCH DDL; + +CREATE TABLE IF NOT EXISTS warehouse ( + w_id int not null, + w_name varchar(10), + w_street_1 varchar(20), + w_street_2 varchar(20), + w_city varchar(20), + w_state varchar(2), + w_zip varchar(9), + w_tax decimal, + w_ytd decimal, + primary key (w_id) +); + +create table IF NOT EXISTS district ( + d_id int not null, + w_id int not null, + d_name varchar(10), + d_street_1 varchar(20), + d_street_2 varchar(20), + d_city varchar(20), + d_state varchar(2), + d_zip varchar(9), + d_tax decimal, + d_ytd decimal, + d_next_o_id int, + primary key (w_id, d_id) +); + +-- CUSTOMER TABLE + +create table IF NOT EXISTS customer ( + c_id int not null, + d_id int not null, + w_id int not null, + c_first varchar(16), + c_middle varchar(2), + c_last varchar(16), + c_street_1 varchar(20), + c_street_2 varchar(20), + c_city varchar(20), + c_state varchar(2), + c_zip varchar(9), + c_phone varchar(16), + c_since timestamptz, + c_credit varchar(2), + c_credit_lim bigint, + c_discount decimal, + c_balance decimal, + c_ytd_payment decimal, + c_payment_cnt int, + c_delivery_cnt int, + c_data text, + PRIMARY KEY(w_id, d_id, c_id) +); + +-- HISTORY TABLE + +create table IF NOT EXISTS history ( + c_id int, + d_id int, + w_id int, + h_d_id int, + h_w_id int, + h_date timestamptz, + h_amount decimal, + h_data varchar(24), + PRIMARY KEY(c_id, d_id, w_id, h_d_id, h_w_id, h_date) +); + +create table IF NOT EXISTS orders ( + o_id int not null, + d_id int not null, + w_id int not null, + c_id int not null, + o_entry_d timestamptz, + o_carrier_id int, + o_ol_cnt int, + o_all_local int, + PRIMARY KEY(w_id, d_id, c_id, o_id) +); + +-- NEW_ORDER table + +create table IF NOT EXISTS new_orders ( + o_id int not null, + c_id int not null, + d_id int not null, + w_id int not null, + PRIMARY KEY(w_id, d_id, o_id, c_id) +); + +create table IF NOT EXISTS order_line ( + o_id int not null, + c_id int not null, + d_id int not null, + w_id int not null, + ol_number int not null, + ol_i_id int, + ol_supply_w_id int, + ol_delivery_d timestamptz, + ol_quantity int, + ol_amount decimal, + ol_dist_info varchar(24), + PRIMARY KEY(w_id, d_id, o_id, c_id, ol_number) +); + +-- STOCK table + +create table IF NOT EXISTS stock ( + s_i_id int not null, + w_id int not null, + s_quantity int, + s_dist_01 varchar(24), + s_dist_02 varchar(24), + s_dist_03 varchar(24), + s_dist_04 varchar(24), + s_dist_05 varchar(24), + s_dist_06 varchar(24), + s_dist_07 varchar(24), + s_dist_08 varchar(24), + s_dist_09 varchar(24), + s_dist_10 varchar(24), + s_ytd decimal, + s_order_cnt int, + s_remote_cnt int, + s_data varchar(50), + PRIMARY KEY(w_id, s_i_id) +); + +create table IF NOT EXISTS item ( + i_id int not null, + i_im_id int, + i_name varchar(24), + i_price decimal, + i_data varchar(50), + PRIMARY KEY(i_id) +); + +CREATE INDEX idx_customer ON customer (w_id,d_id,c_last,c_first); +CREATE INDEX idx_orders ON orders (w_id,d_id,o_id); +CREATE INDEX fkey_stock_2 ON stock (s_i_id); +CREATE INDEX fkey_order_line_2 ON order_line (ol_supply_w_id,ol_i_id); +CREATE INDEX fkey_history_1 ON history (w_id,d_id,c_id); +CREATE INDEX fkey_history_2 ON history (h_w_id,h_d_id ); + +ALTER TABLE new_orders ADD CONSTRAINT fkey_new_orders_1_ FOREIGN KEY(w_id,d_id,c_id,o_id) REFERENCES orders(w_id,d_id,c_id,o_id); +ALTER TABLE orders ADD CONSTRAINT fkey_orders_1_ FOREIGN KEY(w_id,d_id,c_id) REFERENCES customer(w_id,d_id,c_id); +ALTER TABLE customer ADD CONSTRAINT fkey_customer_1_ FOREIGN KEY(w_id,d_id) REFERENCES district(w_id,d_id); +ALTER TABLE history ADD CONSTRAINT fkey_history_1_ FOREIGN KEY(w_id,d_id,c_id) REFERENCES customer(w_id,d_id,c_id); +ALTER TABLE history ADD CONSTRAINT fkey_history_2_ FOREIGN KEY(h_w_id,h_d_id) REFERENCES district(w_id,d_id); +ALTER TABLE district ADD CONSTRAINT fkey_district_1_ FOREIGN KEY(w_id) REFERENCES warehouse(w_id); +ALTER TABLE order_line ADD CONSTRAINT fkey_order_line_1_ FOREIGN KEY(w_id,d_id,c_id,o_id) REFERENCES orders(w_id,d_id,c_id,o_id); +ALTER TABLE order_line ADD CONSTRAINT fkey_order_line_2_ FOREIGN KEY(ol_supply_w_id,ol_i_id) REFERENCES stock(w_id,s_i_id); +ALTER TABLE stock ADD CONSTRAINT fkey_stock_1_ FOREIGN KEY(w_id) REFERENCES warehouse(w_id); +ALTER TABLE stock ADD CONSTRAINT fkey_stock_2_ FOREIGN KEY(s_i_id) REFERENCES item(i_id); + +RUN BATCH; +"; + + public const string DropTables = @" +start batch ddl; + +drop index if exists fkey_history_2; +drop index if exists fkey_history_1; +drop index if exists fkey_order_line_2; +drop index if exists fkey_stock_2; +drop index if exists idx_orders; +drop index if exists idx_customer; + +ALTER TABLE new_orders DROP CONSTRAINT fkey_new_orders_1_; +ALTER TABLE orders DROP CONSTRAINT fkey_orders_1_; +ALTER TABLE customer DROP CONSTRAINT fkey_customer_1_; +ALTER TABLE history DROP CONSTRAINT fkey_history_1_; +ALTER TABLE history DROP CONSTRAINT fkey_history_2_; +ALTER TABLE district DROP CONSTRAINT fkey_district_1_; +ALTER TABLE order_line DROP CONSTRAINT fkey_order_line_1_; +ALTER TABLE order_line DROP CONSTRAINT fkey_order_line_2_; +ALTER TABLE stock DROP CONSTRAINT fkey_stock_1_; +ALTER TABLE stock DROP CONSTRAINT fkey_stock_2_; + +drop table if exists new_orders; +drop table if exists order_line; +drop table if exists history; +drop table if exists orders; +drop table if exists stock; +drop table if exists customer; +drop table if exists district; +drop table if exists warehouse; +drop table if exists item; + +run batch; +"; +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/SchemaUtil.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/SchemaUtil.cs new file mode 100644 index 00000000..49acdd8e --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/SchemaUtil.cs @@ -0,0 +1,47 @@ +using Google.Cloud.Spanner.Admin.Database.V1; + +namespace Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc; + +static class SchemaUtil +{ + internal static async Task CreateSchemaAsync(SpannerConnection connection, DatabaseDialect dialect, CancellationToken cancellationToken) + { + await using var cmd = connection.CreateCommand(); + cmd.CommandText = "select count(1) " + + "from information_schema.tables " + + "where " + + (dialect == DatabaseDialect.Postgresql ? "table_schema='public' and " : "table_schema='' and ") + + "table_name in ('warehouse', 'district', 'customer', 'history', 'orders', 'new_orders', 'order_line', 'stock', 'item')"; + var count = await cmd.ExecuteScalarAsync(cancellationToken); + if (count is long and 9) + { + return; + } + + var commands = SchemaDefinition.CreateTablesPostgreSql.Split(";"); + foreach (var command in commands) + { + if (command.Trim() == "") + { + continue; + } + cmd.CommandText = command; + await cmd.ExecuteNonQueryAsync(cancellationToken); + } + } + + internal static async Task DropSchemaAsync(SpannerConnection connection, CancellationToken cancellationToken) + { + await using var cmd = connection.CreateCommand(); + var commands = SchemaDefinition.DropTables.Split(";"); + foreach (var command in commands) + { + if (command.Trim() == "") + { + continue; + } + cmd.CommandText = command; + await cmd.ExecuteNonQueryAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Stats.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Stats.cs new file mode 100644 index 00000000..56ab5e10 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Stats.cs @@ -0,0 +1,97 @@ +namespace Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc; + +internal class Stats +{ + private readonly DateTime _startTime; + private ulong _numTransactions; + private ulong _numTransactionsStarted; + private ulong _numTransactionsCompleted; + private ulong _numFailedTransactions; + private ulong _numNewOrderTransactions; + private ulong _numPaymentTransactions; + private ulong _numOrderStatusTransactions; + private ulong _numDeliveryTransactions; + private ulong _numStockLevelTransactions; + private Exception? _lastException; + + private ulong _totalMillis; + + internal Stats() + { + _startTime = DateTime.UtcNow; + } + + internal void RegisterTransactionStarted() + { + Interlocked.Increment(ref _numTransactionsStarted); + } + + internal void RegisterTransactionCompleted() + { + Interlocked.Increment(ref _numTransactionsCompleted); + } + + internal void RegisterTransaction(TpccRunner.TransactionType transactionType, TimeSpan duration) + { + Interlocked.Increment(ref _numTransactions); + Interlocked.Add(ref _totalMillis, (ulong) duration.TotalMilliseconds); + switch (transactionType) + { + case TpccRunner.TransactionType.NewOrder: + Interlocked.Increment(ref _numNewOrderTransactions); + break; + case TpccRunner.TransactionType.Payment: + Interlocked.Increment(ref _numPaymentTransactions); + break; + case TpccRunner.TransactionType.OrderStatus: + Interlocked.Increment(ref _numOrderStatusTransactions); + break; + case TpccRunner.TransactionType.Delivery: + Interlocked.Increment(ref _numDeliveryTransactions); + break; + case TpccRunner.TransactionType.StockLevel: + Interlocked.Increment(ref _numStockLevelTransactions); + break; + default: + throw new ArgumentOutOfRangeException(nameof(transactionType), transactionType, null); + } + } + + internal void RegisterFailedTransaction(TpccRunner.TransactionType transactionType, TimeSpan duration, Exception error) + { + Interlocked.Increment(ref _numFailedTransactions); + lock (this) + { + _lastException = error; + } + } + + internal void LogStats() + { + lock (this) + { + if (_lastException != null) + { + Console.Error.WriteLine(_lastException); + _lastException = null; + } + } + Console.Write(ToString()); + } + + public override string ToString() + { + return $" Total duration: {DateTime.UtcNow - _startTime}{Environment.NewLine}" + + $"Transactions/sec: {Interlocked.Read(ref _numTransactions) / (DateTime.UtcNow - _startTime).TotalSeconds}{Environment.NewLine}" + + $" Total: {Interlocked.Read(ref _numTransactions)}{Environment.NewLine}" + + $" Avg: {Interlocked.Read(ref _totalMillis) / Interlocked.Read(ref _numTransactions)}{Environment.NewLine}" + + $" Started: {Interlocked.Read(ref _numTransactionsStarted)}{Environment.NewLine}" + + $" Completed: {Interlocked.Read(ref _numTransactionsCompleted)}{Environment.NewLine}" + + $" Failed: {Interlocked.Read(ref _numFailedTransactions)}{Environment.NewLine}" + + $" Num new order: {Interlocked.Read(ref _numNewOrderTransactions)}{Environment.NewLine}" + + $" Num payment: {Interlocked.Read(ref _numPaymentTransactions)}{Environment.NewLine}" + + $"Num order status: {Interlocked.Read(ref _numOrderStatusTransactions)}{Environment.NewLine}" + + $" Num delivery: {Interlocked.Read(ref _numDeliveryTransactions)}{Environment.NewLine}" + + $" Num stock level: {Interlocked.Read(ref _numStockLevelTransactions)}{Environment.NewLine}"; + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/TpccRunner.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/TpccRunner.cs new file mode 100644 index 00000000..bf753df6 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/TpccRunner.cs @@ -0,0 +1,861 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Data; +using System.Data.Common; +using System.Diagnostics; +using Google.Cloud.Spanner.Data; +using Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc.loader; +using Google.Cloud.Spanner.V1; +using Google.Rpc; +using SpannerException = Google.Cloud.SpannerLib.SpannerException; + +namespace Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc; + +internal class TpccRunner +{ + internal enum TransactionType + { + Unknown, + NewOrder, + Payment, + OrderStatus, + Delivery, + StockLevel, + } + + private readonly Stats _stats; + private readonly DbConnection _connection; + private readonly int _numWarehouses; + private readonly int _numDistrictsPerWarehouse; + private readonly int _numCustomersPerDistrict; + private readonly int _numItems; + private readonly bool _isClientLib; + + private DbTransaction? _currentTransaction; + + internal TpccRunner( + Stats stats, + DbConnection connection, + int numWarehouses, + int numDistrictsPerWarehouse = 10, + int numCustomersPerDistrict = 3000, + int numItems = 100_000) + { + _stats = stats; + _connection = connection; + _numWarehouses = numWarehouses; + _numDistrictsPerWarehouse = numDistrictsPerWarehouse; + _numCustomersPerDistrict = numCustomersPerDistrict; + _numItems = numItems; + _isClientLib = connection is Data.SpannerConnection; + } + + internal async Task RunAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await RunTransactionAsync(cancellationToken); + } + } + + internal async Task RunTransactionAsync(CancellationToken cancellationToken) + { + var watch = Stopwatch.StartNew(); + var transaction = Random.Shared.Next(23); + var transactionType = TransactionType.Unknown; + var attempts = 0; + while (true) + { + attempts++; + try + { + if (transaction < 10) + { + transactionType = TransactionType.NewOrder; + await NewOrderAsync(cancellationToken); + } + else if (transaction < 20) + { + transactionType = TransactionType.Payment; + await PaymentAsync(cancellationToken); + } + else if (transaction < 21) + { + transactionType = TransactionType.OrderStatus; + await OrderStatusAsync(cancellationToken); + } + else if (transaction < 22) + { + transactionType = TransactionType.Delivery; + await DeliveryAsync(cancellationToken); + } + else if (transaction < 23) + { + transactionType = TransactionType.StockLevel; + await StockLevelAsync(cancellationToken); + } + else + { + throw new ArgumentException($"Invalid transaction type {transaction}"); + } + + _stats.RegisterTransaction(transactionType, watch.Elapsed); + break; + } + catch (Exception exception) + { + await SilentRollbackTransactionAsync(cancellationToken); + if (attempts < 10) + { + if (exception is SpannerException { Code: Code.Aborted }) + { + continue; + } + + if (exception is Data.SpannerException { ErrorCode: ErrorCode.Aborted }) + { + continue; + } + } + else + { + await Console.Error.WriteLineAsync($"Giving up after {attempts} attempts"); + } + + _stats.RegisterFailedTransaction(transactionType, watch.Elapsed, exception); + break; + } + finally + { + if (_currentTransaction != null) + { + await Console.Error.WriteLineAsync("Transaction still open!"); + await _currentTransaction.DisposeAsync(); + _currentTransaction = null; + } + } + } + } + + private async Task NewOrderAsync(CancellationToken cancellationToken) + { + var warehouseId = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numWarehouses)); + var districtId = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numDistrictsPerWarehouse)); + var customerId = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numCustomersPerDistrict)); + + var orderLineCount = Random.Shared.Next(5, 16); + var itemIds = new long[orderLineCount]; + var supplyWarehouses = new long[orderLineCount]; + var quantities = new int[orderLineCount]; + var rollback = Random.Shared.Next(100); + var allLocal = 1; + + for (var line = 0; line < orderLineCount; line++) + { + if (rollback == 1 && line == orderLineCount - 1) + { + itemIds[line] = DataLoader.ReverseBitsUnsigned(long.MaxValue); + } + else + { + // TODO: Make sure that the chosen item IDs are unique. + itemIds[line] = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numItems)); + } + + if (Random.Shared.Next(100) == 50) + { + supplyWarehouses[line] = GetOtherWarehouseId(warehouseId); + allLocal = 0; + } + else + { + supplyWarehouses[line] = warehouseId; + } + + quantities[line] = Random.Shared.Next(1, 10); + } + + await BeginTransactionAsync("new_order", cancellationToken); + + // TODO: These queries can run in parallel. + var row = await ExecuteRowAsync( + "SELECT c_discount, c_last, c_credit, w_tax " + + "FROM customer c, warehouse w " + + "WHERE w.w_id = $1 AND c.w_id = w.w_id AND c.d_id = $2 AND c.c_id = $3 " + + "FOR UPDATE", cancellationToken, + warehouseId, districtId, customerId); + var discount = ToDecimal(row[0]); + var last = (string)row[1]; + var credit = (string)row[2]; + var warehouseTax = ToDecimal(row[3]); + + row = await ExecuteRowAsync( + "SELECT d_next_o_id, d_tax " + + "FROM district " + + "WHERE w_id = $1 AND d_id = $2 FOR UPDATE", cancellationToken, + warehouseId, districtId); + var districtNextOrderId = row[0] is DBNull ? 0L : (long)row[0]; + var districtTax = ToDecimal(row[1]); + + object batch = _isClientLib ? (_currentTransaction as Data.SpannerTransaction)!.CreateBatchDmlCommand() : _connection.CreateBatch(); + CreateBatchCommand( + batch, + "UPDATE district SET d_next_o_id = $1 WHERE d_id = $2 AND w_id= $3", + districtNextOrderId + 1L, districtId, warehouseId); + CreateBatchCommand( + batch, + "INSERT INTO orders (o_id, d_id, w_id, c_id, o_entry_d, o_ol_cnt, o_all_local) " + + "VALUES ($1,$2,$3,$4,CURRENT_TIMESTAMP,$5,$6)", + districtNextOrderId, districtId, warehouseId, customerId, orderLineCount, allLocal); + CreateBatchCommand( + batch, + "INSERT INTO new_orders (o_id, c_id, d_id, w_id) VALUES ($1,$2,$3,$4)", + districtNextOrderId, customerId, districtId, warehouseId); + + for (var line = 0; line < orderLineCount; line++) + { + var orderLineSupplyWarehouseId = supplyWarehouses[line]; + var orderLineItemId = itemIds[line]; + var orderLineQuantity = quantities[line]; + try + { + row = await ExecuteRowAsync( + "SELECT i_price, i_name, i_data FROM item WHERE i_id = $1", + cancellationToken, + orderLineItemId); + } + catch (RowNotFoundException) + { + // TODO: Record deliberate rollback + await RollbackTransactionAsync(cancellationToken); + return; + } + + var itemPrice = ToDecimal(row[0]); + var itemName = (string)row[1]; + var itemData = (string)row[2]; + + row = await ExecuteRowAsync( + "SELECT s_quantity, s_data, s_dist_01, s_dist_02, s_dist_03, s_dist_04, s_dist_05, s_dist_06, s_dist_07, s_dist_08, s_dist_09, s_dist_10 " + + "FROM stock " + + "WHERE s_i_id = $1 AND w_id= $2 FOR UPDATE", + cancellationToken, + orderLineItemId, orderLineSupplyWarehouseId); + var stockQuantity = (long)row[0]; + var stockData = (string)row[1]; + var stockDistrict = new string[10]; + for (int i = 2; i < stockDistrict.Length + 2; i++) + { + stockDistrict[i - 2] = (string)row[i]; + } + + var orderLineDistrictInfo = + stockDistrict[(int)(DataLoader.ReverseBitsUnsigned((ulong)districtId) % stockDistrict.Length)]; + if (stockQuantity > orderLineQuantity) + { + stockQuantity = stockQuantity - orderLineQuantity; + } + else + { + stockQuantity = stockQuantity - orderLineQuantity + 91; + } + + CreateBatchCommand(batch, "UPDATE stock SET s_quantity=$1 WHERE s_i_id=$2 AND w_id=$3", + stockQuantity, orderLineItemId, orderLineSupplyWarehouseId); + + var totalTax = 1m + warehouseTax + districtTax; + var discountFactor = 1m - discount; + var orderLineAmount = orderLineQuantity * itemPrice * totalTax * discountFactor; + CreateBatchCommand(batch, + "INSERT INTO order_line (o_id, c_id, d_id, w_id, ol_number, ol_i_id, ol_supply_w_id, ol_quantity, ol_amount, ol_dist_info) " + + "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + districtNextOrderId, + customerId, + districtId, + warehouseId, + line, + orderLineItemId, + orderLineSupplyWarehouseId, + orderLineQuantity, + orderLineAmount, + orderLineDistrictInfo); + } + + if (batch is Data.SpannerBatchCommand spannerBatchCommand) + { + await spannerBatchCommand.ExecuteNonQueryAsync(cancellationToken); + } + else if (batch is DbBatch dbBatch) + { + await dbBatch.ExecuteNonQueryAsync(cancellationToken); + } + else + { + throw new NotSupportedException("Batch type not supported"); + } + await CommitTransactionAsync(cancellationToken); + } + + private async Task PaymentAsync(CancellationToken cancellationToken) + { + var warehouseId = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numWarehouses)); + var districtId = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numDistrictsPerWarehouse)); + var customerId = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numCustomersPerDistrict)); + var amount = Random.Shared.Next(1, 500000) / 100m; + + long customerWarehouseId; + long customerDistrictId; + var lastName = LastNameGenerator.GenerateLastName(long.MaxValue); + bool byName; + object[] row; + if (Random.Shared.Next(100) < 60) + { + byName = true; + } + else + { + byName = false; + } + if (Random.Shared.Next(100) < 85) + { + customerWarehouseId = warehouseId; + customerDistrictId = districtId; + } + else + { + customerWarehouseId = GetOtherWarehouseId(warehouseId); + customerDistrictId = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numDistrictsPerWarehouse)); + } + await BeginTransactionAsync("payment", cancellationToken); + await ExecuteNonQueryAsync("UPDATE warehouse SET w_ytd = w_ytd + $1 WHERE w_id = $2", + cancellationToken, amount, warehouseId); + + row = await ExecuteRowAsync( + "SELECT w_street_1, w_street_2, w_city, w_state, w_zip, w_name " + + "FROM warehouse " + + "WHERE w_id = $1", + cancellationToken, warehouseId); + var warehouseStreet1 = (string) row[0]; + var warehouseStreet2 = (string) row[1]; + var warehouseCity = (string) row[2]; + var warehouseState = (string) row[3]; + var warehouseZip = (string) row[4]; + var warehouseName = (string) row[5]; + + await ExecuteNonQueryAsync( + "UPDATE district SET d_ytd = d_ytd + $1 WHERE w_id = $2 AND d_id= $3", + cancellationToken, amount, warehouseId, districtId); + + row = await ExecuteRowAsync( + "SELECT d_street_1, d_street_2, d_city, d_state, d_zip, d_name " + + "FROM district " + + "WHERE w_id = $1 AND d_id = $2", + cancellationToken, warehouseId, districtId); + var districtStreet1 = (string) row[0]; + var districtStreet2 = (string) row[1]; + var districtCity = (string) row[2]; + var districtState = (string) row[3]; + var districtZip = (string) row[4]; + var districtName = (string) row[5]; + + if (byName) + { + row = await ExecuteRowAsync( + "SELECT count(c_id) namecnt " + + "FROM customer " + + "WHERE w_id = $1 AND d_id= $2 AND c_last=$3", + cancellationToken, customerWarehouseId, customerDistrictId, lastName); + var nameCount = (int) (long) row[0]; + if (nameCount % 2 == 0) + { + nameCount++; + } + var resultSet = await ExecuteQueryAsync( + "SELECT c_id " + + "FROM customer " + + "WHERE w_id=$1 AND d_id=$2 AND c_last=$3 " + + "ORDER BY c_first", + cancellationToken, customerWarehouseId, customerDistrictId, lastName); + for (var counter = 0; counter < Math.Min(nameCount, resultSet.Count); counter++) + { + customerId = (long) resultSet[counter][0]; + } + } + row = await ExecuteRowAsync( + "SELECT c_first, c_middle, c_last, c_street_1, c_street_2, c_city, c_state, c_zip, c_phone, c_credit, c_credit_lim, c_discount, c_balance, c_ytd_payment, c_since " + + "FROM customer " + + "WHERE w_id=$1 AND d_id=$2 AND c_id=$3 FOR UPDATE", + cancellationToken, customerWarehouseId, customerDistrictId, customerId); + var firstName = (string) row[0]; + var middleName = (string) row[1]; + lastName = (string) row[2]; + var street1 = (string) row[3]; + var street2 = (string) row[4]; + var city = (string) row[5]; + var state = (string) row[6]; + var zip = (string) row[7]; + var phone = (string) row[8]; + var credit = (string) row[9]; + var creditLimit = (long) row[10]; + var discount = ToDecimal(row[11]); + var balance = ToDecimal(row[12]); + var ytdPayment = ToDecimal(row[13]); + var since = (DateTime) row[14]; + + // TODO: Use batching from here + balance = balance - amount; + ytdPayment = ytdPayment + amount; + if ("BC".Equals(credit)) + { + row = await ExecuteRowAsync( + "SELECT c_data FROM customer WHERE w_id=$1 AND d_id=$2 AND c_id=$3", + cancellationToken, customerWarehouseId, customerDistrictId, customerId); + var customerData = (string)row[0]; + var newCustomerData = + $"| {customerId} {customerDistrictId} {customerWarehouseId} {districtId} {warehouseId} {amount} {DateTime.Now} {customerData}"; + if (newCustomerData.Length > 500) + { + newCustomerData = newCustomerData.Substring(0, 500); + } + await ExecuteNonQueryAsync( + "UPDATE customer " + + "SET c_balance=$1, c_ytd_payment=$2, c_data=$3 " + + "WHERE w_id = $4 AND d_id=$5 AND c_id=$6", + cancellationToken, + balance, + ytdPayment, + newCustomerData, + customerWarehouseId, + customerDistrictId, + customerId + ); + } + else + { + await ExecuteNonQueryAsync( + "UPDATE customer " + + "SET c_balance=$1, c_ytd_payment=$2 " + + "WHERE w_id = $3 AND d_id=$4 AND c_id=$5", + cancellationToken, balance, ytdPayment, customerWarehouseId, customerDistrictId, customerId); + } + + var data = $"{warehouseName} {districtName}"; + if (data.Length > 24) + { + data = data.Substring(0, 24); + } + await ExecuteNonQueryAsync( + "INSERT INTO history (d_id, w_id, c_id, h_d_id, h_w_id, h_date, h_amount, h_data) " + + "VALUES ($1,$2,$3,$4,$5,CURRENT_TIMESTAMP,$6,$7)", + cancellationToken, + customerDistrictId, + customerWarehouseId, + customerId, + districtId, + warehouseId, + amount, + data); + + await CommitTransactionAsync(cancellationToken); + } + + private async Task OrderStatusAsync(CancellationToken cancellationToken) + { + var warehouseId = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numWarehouses)); + var districtId = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numDistrictsPerWarehouse)); + var customerId = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numCustomersPerDistrict)); + + var lastName = LastNameGenerator.GenerateLastName(long.MaxValue); + object[] row; + var byName = Random.Shared.Next(100) < 60; + + decimal balance; + string first, middle, last; + + await BeginTransactionAsync("order_status", cancellationToken); + if (byName) + { + row = await ExecuteRowAsync( + "SELECT count(c_id) namecnt " + + "FROM customer " + + "WHERE w_id=$1 AND d_id=$2 AND c_last=$3", + cancellationToken, warehouseId, districtId, lastName); + int nameCount = (int) (long) row[0]; + if (nameCount % 2 == 0) + { + nameCount++; + } + var resultSet = await ExecuteQueryAsync( + "SELECT c_balance, c_first, c_middle, c_id " + + "FROM customer WHERE w_id = $1 AND d_id=$2 AND c_last=$3 " + + "ORDER BY c_first", + cancellationToken, warehouseId, districtId, lastName); + for (int counter = 0; counter < Math.Min(nameCount, resultSet.Count); counter++) + { + balance = ToDecimal(resultSet[counter][0]); + first = (string) resultSet[counter][1]; + middle = (string) resultSet[counter][2]; + customerId = (long) resultSet[counter][3]; + } + } + else + { + row = await ExecuteRowAsync( + "SELECT c_balance, c_first, c_middle, c_last " + + "FROM customer " + + "WHERE w_id = $1 AND d_id=$2 AND c_id=$3", + cancellationToken, warehouseId, districtId, customerId); + balance = ToDecimal(row[0]); + first = (string) row[1]; + middle = (string) row[2]; + last = (string) row[3]; + } + + var maybeRow = await ExecuteRowAsync(false, + "SELECT o_id, o_carrier_id, o_entry_d " + + "FROM orders " + + "WHERE w_id = $1 AND d_id = $2 AND c_id = $3 " + + "ORDER BY o_id DESC", + cancellationToken, warehouseId, districtId, customerId); + var orderId = maybeRow == null ? 0L : (long) maybeRow[0]; + + long item_id, supply_warehouse_id, quantity; + decimal amount; + DateTime? delivery_date; + var results = await ExecuteQueryAsync( + "SELECT ol_i_id, ol_supply_w_id, ol_quantity, ol_amount, ol_delivery_d " + + "FROM order_line " + + "WHERE w_id = $1 AND d_id = $2 AND o_id = $3", + cancellationToken, warehouseId, districtId, orderId); + for (var counter = 0; counter < results.Count; counter++) + { + item_id = (long) results[counter][0]; // item_id + supply_warehouse_id = (long) results[counter][1]; // supply_warehouse_id + quantity = (long) results[counter][2]; // quantity + amount = ToDecimal(results[counter][3]); // amount + delivery_date = results[counter][4] is DBNull ? null : (DateTime) results[counter][4]; // delivery_date + } + await CommitTransactionAsync(cancellationToken); + } + + private async Task DeliveryAsync(CancellationToken cancellationToken) + { + var warehouseId = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numWarehouses)); + var carrierId = Random.Shared.Next(10); + + await BeginTransactionAsync("delivery", cancellationToken); + + for (var district = 0L; district < _numDistrictsPerWarehouse; district++) + { + var districtId = DataLoader.ReverseBitsUnsigned((ulong)district); + var row = await ExecuteRowAsync(false, + "SELECT o_id, c_id " + + "FROM new_orders " + + "WHERE d_id = $1 AND w_id = $2 " + + "ORDER BY o_id ASC " + + "LIMIT 1 FOR UPDATE", + cancellationToken, districtId, warehouseId); + if (row != null) + { + var newOrderId = (long)row[0]; + var customerId = (long)row[1]; + await ExecuteNonQueryAsync( + "DELETE " + + "FROM new_orders " + + "WHERE o_id = $1 AND c_id = $2 AND d_id = $3 AND w_id = $4", + cancellationToken, newOrderId, customerId, districtId, warehouseId); + row = await ExecuteRowAsync( + "SELECT c_id FROM orders WHERE o_id = $1 AND d_id = $2 AND w_id = $3", + cancellationToken, newOrderId, districtId, warehouseId); + await ExecuteNonQueryAsync( + "UPDATE orders " + + "SET o_carrier_id = $1 " + + "WHERE o_id = $2 AND c_id = $3 AND d_id = $4 AND w_id = $5", + cancellationToken, carrierId, newOrderId, customerId, districtId, warehouseId); + await ExecuteNonQueryAsync( + "UPDATE order_line " + + "SET ol_delivery_d = CURRENT_TIMESTAMP " + + "WHERE o_id = $1 AND c_id = $2 AND d_id = $3 AND w_id = $4", + cancellationToken, newOrderId, customerId, districtId, warehouseId); + row = await ExecuteRowAsync( + "SELECT SUM(ol_amount) sm " + + "FROM order_line " + + "WHERE o_id = $1 AND c_id = $2 AND d_id = $3 AND w_id = $4", + cancellationToken, newOrderId, customerId, districtId, warehouseId); + var sumOrderLineAmount = ToDecimal(row[0]); + await ExecuteNonQueryAsync( + "UPDATE customer " + + "SET c_balance = c_balance + $1, c_delivery_cnt = c_delivery_cnt + 1 " + + "WHERE c_id = $2 AND d_id = $3 AND w_id = $4", + cancellationToken, sumOrderLineAmount, customerId, districtId, warehouseId); + } + } + await CommitTransactionAsync(cancellationToken); + } + + private async Task StockLevelAsync(CancellationToken cancellationToken) + { + var warehouseId = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numWarehouses)); + var districtId = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numDistrictsPerWarehouse)); + var level = Random.Shared.Next(10, 21); + + await BeginTransactionAsync("stock_level", cancellationToken); + String stockLevelQueries = "case1"; + Object[] row; + + row = await ExecuteRowAsync( + "SELECT d_next_o_id FROM district WHERE d_id = $1 AND w_id= $2", + cancellationToken, districtId, warehouseId); + var nextOrderId = row[0] is DBNull ? 0L : (long) row[0]; + var resultSet = + await ExecuteQueryAsync( + "SELECT COUNT(DISTINCT (s_i_id)) " + + "FROM order_line ol, stock s " + + "WHERE ol.w_id = $1 " + + "AND ol.d_id = $2 " + + "AND ol.o_id < $3 " + + "AND ol.o_id >= $4 " + + "AND s.w_id= $5 " + + "AND s_i_id=ol_i_id " + + "AND s_quantity < $6", + cancellationToken, + warehouseId, districtId, nextOrderId, nextOrderId - 20, warehouseId, level); + for (var counter = 0; counter < resultSet.Count; counter++) { + var orderLineItemId = (long) resultSet[counter][0]; + row = await ExecuteRowAsync( + "SELECT count(1) FROM stock " + + "WHERE w_id = $1 AND s_i_id = $2 " + + "AND s_quantity < $3", + cancellationToken, warehouseId, orderLineItemId, level); + var stockCount = (long) row[0]; + } + + await CommitTransactionAsync(cancellationToken); + } + + private decimal ToDecimal(object value) + { + return _isClientLib ? ((PgNumeric) value).ToDecimal(LossOfPrecisionHandling.Truncate) : (decimal) value; + } + + private async Task BeginTransactionAsync(string tag, CancellationToken cancellationToken = default) + { + if (_connection is Data.SpannerConnection spannerConnection) + { + _currentTransaction = await spannerConnection.BeginTransactionAsync( + SpannerTransactionCreationOptions.ReadWrite.WithIsolationLevel(IsolationLevel.RepeatableRead), + new SpannerTransactionOptions + { + Tag = tag, + }, + cancellationToken); + } + else if (_connection is SpannerConnection connection) + { + _currentTransaction = await connection.BeginTransactionAsync(IsolationLevel.RepeatableRead, cancellationToken); + await ExecuteNonQueryAsync($"set local transaction_tag = '{tag}'", cancellationToken); + } + } + + private async Task CommitTransactionAsync(CancellationToken cancellationToken = default) + { + if (_currentTransaction != null) + { + await _currentTransaction.CommitAsync(cancellationToken); + await _currentTransaction.DisposeAsync(); + _currentTransaction = null; + } + } + + private async Task SilentRollbackTransactionAsync(CancellationToken cancellationToken = default) + { + try + { + if (_currentTransaction != null) + { + await RollbackTransactionAsync(cancellationToken); + } + else + { + await ExecuteNonQueryAsync("rollback", cancellationToken); + } + } + catch (Exception) + { + if (_currentTransaction != null) + { + await _currentTransaction.DisposeAsync(); + _currentTransaction = null; + } + } + } + + private async Task RollbackTransactionAsync(CancellationToken cancellationToken = default) + { + if (_currentTransaction != null) + { + await _currentTransaction.RollbackAsync(cancellationToken); + await _currentTransaction.DisposeAsync(); + _currentTransaction = null; + } + } + + private void CreateBatchCommand(object batch, string commandText, params object[] parameters) + { + if (batch is Data.SpannerBatchCommand command) + { + CreateBatchCommand(command, commandText, parameters); + } + else if (batch is DbBatch dbBatch) + { + CreateBatchCommand(dbBatch, commandText, parameters); + } + else + { + throw new ArgumentException("unknown batch type"); + } + } + + private void CreateBatchCommand(Data.SpannerBatchCommand batch, string commandText, params object[] parameters) + { + var paramCollection = new Data.SpannerParameterCollection(); + for (var i=0; i < parameters.Length; i++) + { + var value = parameters[i]; + if (value is decimal d) + { + value = PgNumeric.FromDecimal(d); + } + paramCollection.Add(new Data.SpannerParameter {ParameterName = $"p{i+1}", Value = value}); + } + batch.Add(commandText, paramCollection); + } + + private void CreateBatchCommand(DbBatch batch, string commandText, params object[] parameters) + { + var batchCommand = batch.CreateBatchCommand(); + batchCommand.CommandText = commandText; + for (var i = 0; i < parameters.Length; i++) + { + CreateParameter(batchCommand, $"p{i+1}", parameters[i]); + } + batch.BatchCommands.Add(batchCommand); + } + + private void CreateParameter(DbBatchCommand cmd, string parameterName, object parameterValue) + { + var parameter = cmd.CreateParameter(); + parameter.ParameterName = parameterName; + parameter.Value = parameterValue; + cmd.Parameters.Add(parameter); + } + + private async Task ExecuteNonQueryAsync(string commandText, + CancellationToken cancellationToken, params object[] parameters) + { + using var command = CreateCommand(commandText, parameters); + await command.ExecuteNonQueryAsync(cancellationToken); + } + + private Task ExecuteRowAsync(string commandText, + CancellationToken cancellationToken, params object[] parameters) + { + return ExecuteRowAsync(true, commandText, cancellationToken, parameters)!; + } + + private async Task ExecuteRowAsync(bool mustFindRow, string commandText, + CancellationToken cancellationToken, params object[] parameters) + { + using var command = CreateCommand(commandText, parameters); + using var reader = await command.ExecuteReaderAsync(cancellationToken); + if (!await reader.ReadAsync(cancellationToken)) + { + if (mustFindRow) + { + throw new RowNotFoundException("Row not found"); + } + return null; + } + var result = new object[reader.FieldCount]; + for (var i = 0; i < reader.FieldCount; i++) + { + result[i] = reader.GetValue(i); + } + return result; + } + + private async Task> ExecuteQueryAsync(string commandText, + CancellationToken cancellationToken, params object[] parameters) + { + using var command = CreateCommand(commandText, parameters); + using var reader = await command.ExecuteReaderAsync(cancellationToken); + var result = new List(); + while (await reader.ReadAsync(cancellationToken)) + { + var row = new object[reader.FieldCount]; + for (var i = 0; i < reader.FieldCount; i++) + { + row[i] = reader.GetValue(i); + } + result.Add(row); + } + return result; + } + + private DbCommand CreateCommand(string commandText, params object[] parameters) + { + var command = _connection.CreateCommand(); + command.CommandText = commandText; + command.Transaction = _currentTransaction; + for (var i = 0; i < parameters.Length; i++) + { + CreateParameter(command, $"p{i+1}", parameters[i]); + } + return command; + } + + private void CreateParameter(DbCommand cmd, string parameterName, object parameterValue) + { + var parameter = cmd.CreateParameter(); + parameter.ParameterName = parameterName; + if (_isClientLib) + { + var value = parameterValue; + if (value is decimal d) + { + value = PgNumeric.FromDecimal(d); + } + parameter.Value = value; + } + else + { + parameter.Value = parameterValue; + } + cmd.Parameters.Add(parameter); + } + + private long GetOtherWarehouseId(long currentId) { + if (_numWarehouses == 1) { + return currentId; + } + while (true) { + var otherId = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numWarehouses)); + if (otherId != currentId) { + return otherId; + } + } + } + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/CustomerLoader.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/CustomerLoader.cs new file mode 100644 index 00000000..d5f82835 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/CustomerLoader.cs @@ -0,0 +1,112 @@ +using System.Globalization; +using Google.Cloud.Spanner.V1; +using Google.Protobuf.WellKnownTypes; + +namespace Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc.loader; + +internal class CustomerLoader +{ + private readonly SpannerConnection _connection; + + private readonly int _warehouseCount; + + private readonly int _districtsPerWarehouse; + + private readonly int _customersPerDistrict; + + internal CustomerLoader(SpannerConnection connection, int warehouseCount, int districtsPerWarehouse, int customersPerDistrict) + { + _connection = connection; + _warehouseCount = warehouseCount; + _districtsPerWarehouse = districtsPerWarehouse; + _customersPerDistrict = customersPerDistrict; + } + + internal async Task LoadAsync(CancellationToken cancellationToken = default) + { + var count = await CountAsync(cancellationToken); + if (count >= _warehouseCount * _districtsPerWarehouse * _customersPerDistrict) + { + return; + } + + for (var warehouse = 0; warehouse < _warehouseCount; warehouse++) + { + for (var district = 0; district < _districtsPerWarehouse; district++) + { + var group = new BatchWriteRequest.Types.MutationGroup + { + Mutations = { Capacity = 1 } + }; + group.Mutations.Add(CreateMutation(warehouse, district, _customersPerDistrict)); + await _connection.WriteMutationsAsync(group, cancellationToken); + } + } + } + + private async Task CountAsync(CancellationToken cancellationToken = default) + { + await using var command = _connection.CreateCommand(); + command.CommandText = "SELECT COUNT(1) FROM customer"; + var result = await command.ExecuteScalarAsync(cancellationToken); + return result == null ? 0L : (long) result; + } + + private Mutation CreateMutation(int warehouse, int district, int rows) + { + var mutation = new Mutation + { + InsertOrUpdate = new Mutation.Types.Write + { + Table = "customer", + Columns = { "c_id", "d_id", "w_id", "c_first", "c_middle", "c_last", "c_street_1", "c_street_2", + "c_city", "c_state", "c_zip", "c_phone", "c_since", "c_credit", "c_credit_lim", "c_discount", + "c_balance", "c_ytd_payment", "c_payment_cnt", "c_delivery_cnt", "c_data", + }, + Values = + { + Capacity = _customersPerDistrict, + } + } + }; + for (var i = 0; i < rows; i++) + { + mutation.InsertOrUpdate.Values.Add(CreateRandomCustomer(warehouse, district, i)); + } + return mutation; + } + + private ListValue CreateRandomCustomer(int warehouse, int district, int index) + { + var row = new ListValue + { + Values = + { + Capacity = 22 + } + }; + row.Values.Add(Value.ForString($"{DataLoader.ReverseBitsUnsigned((ulong) index)}")); + row.Values.Add(Value.ForString($"{DataLoader.ReverseBitsUnsigned((ulong) district)}")); + row.Values.Add(Value.ForString($"{DataLoader.ReverseBitsUnsigned((ulong) warehouse)}")); + row.Values.Add(Value.ForString(DataLoader.RandomString(16))); + row.Values.Add(Value.ForString(DataLoader.RandomString(2))); + row.Values.Add(Value.ForString(LastNameGenerator.GenerateLastName(index))); + row.Values.Add(Value.ForString(DataLoader.RandomString(20))); + row.Values.Add(Value.ForString(DataLoader.RandomString(20))); + row.Values.Add(Value.ForString(DataLoader.RandomString(20))); + row.Values.Add(Value.ForString(DataLoader.RandomString(2))); + row.Values.Add(Value.ForString(DataLoader.RandomString(9))); + row.Values.Add(Value.ForString(DataLoader.RandomString(16))); + row.Values.Add(Value.ForString(DataLoader.RandomTimestamp())); + row.Values.Add(Value.ForString(Random.Shared.Next(2) == 0 ? "GC" : "BC")); + row.Values.Add(Value.ForString(Random.Shared.Next(100, 5000).ToString(CultureInfo.InvariantCulture))); + row.Values.Add(Value.ForString(DataLoader.RandomDecimal(1, 40))); + row.Values.Add(Value.ForString("0.0")); + row.Values.Add(Value.ForString("0.0")); + row.Values.Add(Value.ForString("0")); + row.Values.Add(Value.ForString("0")); + row.Values.Add(Value.ForString(DataLoader.RandomString(500))); + + return row; + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/DataLoader.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/DataLoader.cs new file mode 100644 index 00000000..a965b3d6 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/DataLoader.cs @@ -0,0 +1,83 @@ +using System.Globalization; +using System.Xml; + +namespace Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc.loader; + +public class DataLoader +{ + private readonly SpannerConnection _connection; + private readonly int _numWarehouses; + private readonly int _numDistrictsPerWarehouse; + private readonly int _numCustomersPerDistrict; + private readonly int _numItems; + + public DataLoader( + SpannerConnection connection, + int numWarehouses, + int numDistrictsPerWarehouse = 10, + int numCustomersPerDistrict = 3000, + int numItems = 100_000) + { + _connection = connection; + _numWarehouses = numWarehouses; + _numDistrictsPerWarehouse = numDistrictsPerWarehouse; + _numCustomersPerDistrict = numCustomersPerDistrict; + _numItems = numItems; + } + + public async Task LoadAsync(CancellationToken cancellationToken) + { + Console.WriteLine("Loading warehouses..."); + var warehouseLoader = new WarehouseLoader(_connection, _numWarehouses); + await warehouseLoader.LoadAsync(cancellationToken); + Console.WriteLine("Loading items..."); + var itemLoader = new ItemLoader(_connection, _numItems); + await itemLoader.LoadAsync(cancellationToken); + Console.WriteLine("Loading districts..."); + var districtLoader = new DistrictLoader(_connection, _numWarehouses, _numDistrictsPerWarehouse); + await districtLoader.LoadAsync(cancellationToken); + Console.WriteLine("Loading customers..."); + var customerLoader = new CustomerLoader(_connection, _numWarehouses, _numDistrictsPerWarehouse, _numCustomersPerDistrict); + await customerLoader.LoadAsync(cancellationToken); + Console.WriteLine("Loading stock..."); + var stockLoader = new StockLoader(_connection, _numWarehouses, _numItems); + await stockLoader.LoadAsync(cancellationToken); + } + + public static long ReverseBitsUnsigned(ulong n) + { + // Step 1: Swap adjacent bits + n = ((n >> 1) & 0x5555555555555555UL) | ((n & 0x5555555555555555UL) << 1); + // Step 2: Swap adjacent pairs of bits + n = ((n >> 2) & 0x3333333333333333UL) | ((n & 0x3333333333333333UL) << 2); + // Step 3: Swap adjacent nibbles (4 bits) + n = ((n >> 4) & 0x0F0F0F0F0F0F0F0FUL) | ((n & 0x0F0F0F0F0F0F0F0FUL) << 4); + // Step 4: Swap adjacent bytes + n = ((n >> 8) & 0x00FF00FF00FF00FFUL) | ((n & 0x00FF00FF00FF00FFUL) << 8); + // Step 5: Swap adjacent 2-byte words + n = ((n >> 16) & 0x0000FFFF0000FFFFUL) | ((n & 0x0000FFFF0000FFFFUL) << 16); + // Step 6: Swap the high and low 4-byte words (32 bits) + n = (n >> 32) | (n << 32); + return (long) n; + } + + internal static string RandomString(int length) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + return new string(Enumerable.Repeat(chars, length) + .Select(s => s[Random.Shared.Next(s.Length)]).ToArray()); + } + + internal static string RandomDecimal(int min, int max) + { + var d = (decimal) Random.Shared.Next(min, max) / 100; + return d.ToString("F", CultureInfo.InvariantCulture); + } + + internal static string RandomTimestamp() + { + var ts = DateTime.UtcNow.AddTicks(-Random.Shared.NextInt64(10 * 365 * TimeSpan.TicksPerDay)); + return XmlConvert.ToString(Convert.ToDateTime(ts, CultureInfo.InvariantCulture), + XmlDateTimeSerializationMode.Utc); + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/DistrictLoader.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/DistrictLoader.cs new file mode 100644 index 00000000..5e339853 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/DistrictLoader.cs @@ -0,0 +1,89 @@ +using Google.Cloud.Spanner.V1; +using Google.Protobuf.WellKnownTypes; + +namespace Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc.loader; + +internal class DistrictLoader +{ + private readonly SpannerConnection _connection; + + private readonly int _warehouseCount; + + private readonly int _districtsPerWarehouse; + + internal DistrictLoader(SpannerConnection connection, int warehouseCount, int districtsPerWarehouse) + { + _connection = connection; + _warehouseCount = warehouseCount; + _districtsPerWarehouse = districtsPerWarehouse; + } + + internal async Task LoadAsync(CancellationToken cancellationToken = default) + { + var count = await CountAsync(cancellationToken); + if (count >= _warehouseCount * _districtsPerWarehouse) + { + return; + } + for (var warehouse = 0; warehouse < _warehouseCount; warehouse++) + { + var group = new BatchWriteRequest.Types.MutationGroup + { + Mutations = { Capacity = 1 } + }; + group.Mutations.Add(CreateMutation(warehouse, _districtsPerWarehouse)); + await _connection.WriteMutationsAsync(group, cancellationToken); + } + } + + private async Task CountAsync(CancellationToken cancellationToken = default) + { + await using var command = _connection.CreateCommand(); + command.CommandText = "SELECT COUNT(1) FROM district"; + var result = await command.ExecuteScalarAsync(cancellationToken); + return result == null ? 0L : (long) result; + } + + private Mutation CreateMutation(int warehouse, int rows) + { + var mutation = new Mutation + { + InsertOrUpdate = new Mutation.Types.Write + { + Table = "district", + Columns = { "d_id", "w_id", "d_name", "d_street_1", "d_street_2", "d_city", "d_state", "d_zip", "d_tax", "d_ytd" }, + Values = + { + Capacity = _districtsPerWarehouse, + } + } + }; + for (var i = 0; i < rows; i++) + { + mutation.InsertOrUpdate.Values.Add(CreateRandomDistrict(warehouse, i)); + } + return mutation; + } + + private ListValue CreateRandomDistrict(int warehouse, int index) + { + var row = new ListValue + { + Values = + { + Capacity = 10 + } + }; + row.Values.Add(Value.ForString($"{DataLoader.ReverseBitsUnsigned((ulong) index)}")); + row.Values.Add(Value.ForString($"{DataLoader.ReverseBitsUnsigned((ulong) warehouse)}")); + row.Values.Add(Value.ForString($"W#{warehouse}D#{index}")); + row.Values.Add(Value.ForString(DataLoader.RandomString(20))); + row.Values.Add(Value.ForString(DataLoader.RandomString(20))); + row.Values.Add(Value.ForString(DataLoader.RandomString(20))); + row.Values.Add(Value.ForString(DataLoader.RandomString(2))); + row.Values.Add(Value.ForString(DataLoader.RandomString(9))); + row.Values.Add(Value.ForString(DataLoader.RandomDecimal(0, 21))); + row.Values.Add(Value.ForString("0.0")); + return row; + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/ItemLoader.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/ItemLoader.cs new file mode 100644 index 00000000..aed4c3ee --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/ItemLoader.cs @@ -0,0 +1,90 @@ +using Google.Cloud.Spanner.V1; +using Google.Protobuf.WellKnownTypes; + +namespace Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc.loader; + +internal class ItemLoader +{ + private static readonly int RowsPerGroup = 1000; + + private readonly SpannerConnection _connection; + + private readonly int _rowCount; + + internal ItemLoader(SpannerConnection connection, int rowCount) + { + _connection = connection; + _rowCount = rowCount; + } + + internal async Task LoadAsync(CancellationToken cancellationToken = default) + { + var count = await CountAsync(cancellationToken); + if (count >= _rowCount) + { + return; + } + + var batch = 0; + var remaining = _rowCount; + while (remaining > 0) + { + var group = new BatchWriteRequest.Types.MutationGroup + { + Mutations = { Capacity = 1 } + }; + var rows = Math.Min(RowsPerGroup, remaining); + group.Mutations.Add(CreateMutation(batch, rows)); + await _connection.WriteMutationsAsync(group, cancellationToken); + remaining -= rows; + batch++; + } + } + + private async Task CountAsync(CancellationToken cancellationToken = default) + { + await using var command = _connection.CreateCommand(); + command.CommandText = "SELECT COUNT(1) FROM item"; + var result = await command.ExecuteScalarAsync(cancellationToken); + return result == null ? 0L : (long) result; + } + + private Mutation CreateMutation(int batch, int rows) + { + var mutation = new Mutation + { + InsertOrUpdate = new Mutation.Types.Write + { + Table = "item", + Columns = { "i_id", "i_im_id", "i_name", "i_price", "i_data" }, + Values = + { + Capacity = _rowCount, + } + } + }; + for (var i = 0; i < rows; i++) + { + mutation.InsertOrUpdate.Values.Add(CreateRandomItem(batch, i)); + } + return mutation; + } + + private ListValue CreateRandomItem(int batch, int index) + { + var row = new ListValue + { + Values = + { + Capacity = 5 + } + }; + var id = (long)batch * RowsPerGroup + index; + row.Values.Add(Value.ForString($"{DataLoader.ReverseBitsUnsigned((ulong) id)}")); + row.Values.Add(Value.ForString($"{Random.Shared.Next(1, 2000001)}")); + row.Values.Add(Value.ForString(DataLoader.RandomString(24))); + row.Values.Add(Value.ForString(DataLoader.RandomDecimal(100, 10001))); + row.Values.Add(Value.ForString(DataLoader.RandomString(50))); + return row; + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/StockLoader.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/StockLoader.cs new file mode 100644 index 00000000..05cdff2c --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/StockLoader.cs @@ -0,0 +1,119 @@ +using Google.Cloud.Spanner.V1; +using Google.Protobuf.WellKnownTypes; + +namespace Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc.loader; + +internal class StockLoader +{ + private static readonly int RowsPerGroup = 1000; + + private readonly SpannerConnection _connection; + + private readonly int _warehouseCount; + + private readonly int _numItems; + + internal StockLoader(SpannerConnection connection, int warehouseCount, int numItems) + { + _connection = connection; + _warehouseCount = warehouseCount; + _numItems = numItems; + } + + internal async Task LoadAsync(CancellationToken cancellationToken = default) + { + var count = await CountAsync(cancellationToken); + if (count >= _warehouseCount * _numItems) + { + return; + } + for (var warehouse = 0; warehouse < _warehouseCount; warehouse++) + { + for (var item=0; item<_numItems; item += RowsPerGroup) + { + var group = new BatchWriteRequest.Types.MutationGroup + { + Mutations = { Capacity = 1 } + }; + group.Mutations.Add(CreateMutation(warehouse, item, RowsPerGroup)); + await _connection.WriteMutationsAsync(group, cancellationToken); + } + } + } + + private async Task CountAsync(CancellationToken cancellationToken = default) + { + await using var command = _connection.CreateCommand(); + command.CommandText = "SELECT COUNT(1) FROM stock"; + var result = await command.ExecuteScalarAsync(cancellationToken); + return result == null ? 0L : (long) result; + } + + private Mutation CreateMutation(int warehouse, int item, int rows) + { + var mutation = new Mutation + { + InsertOrUpdate = new Mutation.Types.Write + { + Table = "stock", + Columns = { "s_i_id", "w_id", "s_quantity", "s_dist_01", "s_dist_02", "s_dist_03", "s_dist_04", "s_dist_05", + "s_dist_06", "s_dist_07", "s_dist_08", "s_dist_09", "s_dist_10", "s_ytd", "s_order_cnt", "s_remote_cnt", "s_data" }, + Values = + { + Capacity = _numItems, + } + } + }; + for (var i = 0; i < rows; i++) + { + mutation.InsertOrUpdate.Values.Add(CreateRandomStock(warehouse, item, i)); + } + return mutation; + } + + private ListValue CreateRandomStock(int warehouse, int item, int index) + { + var row = new ListValue + { + Values = + { + Capacity = 10 + } + }; + // s_i_id int not null, + // w_id int not null, + // s_quantity int, + // s_dist_01 varchar(24), + // s_dist_02 varchar(24), + // s_dist_03 varchar(24), + // s_dist_04 varchar(24), + // s_dist_05 varchar(24), + // s_dist_06 varchar(24), + // s_dist_07 varchar(24), + // s_dist_08 varchar(24), + // s_dist_09 varchar(24), + // s_dist_10 varchar(24), + // s_ytd decimal, + // s_order_cnt int, + // s_remote_cnt int, + // s_data varchar(50), + row.Values.Add(Value.ForString($"{DataLoader.ReverseBitsUnsigned((ulong) (item + index))}")); + row.Values.Add(Value.ForString($"{DataLoader.ReverseBitsUnsigned((ulong) warehouse)}")); + row.Values.Add(Value.ForString(Random.Shared.Next(1, 500).ToString())); + row.Values.Add(Value.ForString(DataLoader.RandomString(24))); + row.Values.Add(Value.ForString(DataLoader.RandomString(24))); + row.Values.Add(Value.ForString(DataLoader.RandomString(24))); + row.Values.Add(Value.ForString(DataLoader.RandomString(24))); + row.Values.Add(Value.ForString(DataLoader.RandomString(24))); + row.Values.Add(Value.ForString(DataLoader.RandomString(24))); + row.Values.Add(Value.ForString(DataLoader.RandomString(24))); + row.Values.Add(Value.ForString(DataLoader.RandomString(24))); + row.Values.Add(Value.ForString(DataLoader.RandomString(24))); + row.Values.Add(Value.ForString(DataLoader.RandomString(24))); + row.Values.Add(Value.ForString("0.0")); + row.Values.Add(Value.ForString("0")); + row.Values.Add(Value.ForString("0")); + row.Values.Add(Value.ForString(DataLoader.RandomString(50))); + return row; + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/WarehouseLoader.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/WarehouseLoader.cs new file mode 100644 index 00000000..a9cb6fd4 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/loader/WarehouseLoader.cs @@ -0,0 +1,67 @@ +using Google.Cloud.Spanner.V1; +using Google.Protobuf.WellKnownTypes; + +namespace Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc.loader; + +internal class WarehouseLoader +{ + private readonly SpannerConnection _connection; + + private readonly int _rowCount; + + internal WarehouseLoader(SpannerConnection connection, int rowCount) + { + _connection = connection; + _rowCount = rowCount; + } + + internal Task LoadAsync(CancellationToken cancellationToken = default) + { + return _connection.WriteMutationsAsync(new BatchWriteRequest.Types.MutationGroup + { + Mutations = { CreateMutation() } + }, cancellationToken); + } + + private Mutation CreateMutation() + { + var mutation = new Mutation + { + InsertOrUpdate = new Mutation.Types.Write + { + Table = "warehouse", + Columns = { "w_id", "w_name", "w_street_1", "w_street_2", "w_city", "w_state", "w_zip", "w_tax", "w_ytd" }, + Values = + { + Capacity = _rowCount, + } + } + }; + for (var i = 0; i < _rowCount; i++) + { + mutation.InsertOrUpdate.Values.Add(CreateRandomWarehouse(i)); + } + return mutation; + } + + private ListValue CreateRandomWarehouse(int index) + { + var row = new ListValue + { + Values = + { + Capacity = 9 + } + }; + row.Values.Add(Value.ForString($"{DataLoader.ReverseBitsUnsigned((ulong) index)}")); + row.Values.Add(Value.ForString($"W#{index}")); + row.Values.Add(Value.ForString(DataLoader.RandomString(20))); + row.Values.Add(Value.ForString(DataLoader.RandomString(20))); + row.Values.Add(Value.ForString(DataLoader.RandomString(20))); + row.Values.Add(Value.ForString(DataLoader.RandomString(2))); + row.Values.Add(Value.ForString(DataLoader.RandomString(9))); + row.Values.Add(Value.ForString(DataLoader.RandomDecimal(0, 21))); + row.Values.Add(Value.ForString("0.0")); + return row; + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Program.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Program.cs new file mode 100644 index 00000000..c5287edf --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Program.cs @@ -0,0 +1,37 @@ +// Copyright 2025 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 +// +// https://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. + +using Google.Cloud.Spanner.DataProvider; + +using var connection = new SpannerConnection {ConnectionString = "/projects/appdev-soda-spanner-staging/instances/knut-test-ycsb/databases/knut-test-db"}; +connection.Open(); +using var cmd = connection.CreateCommand(); +cmd.CommandText = "select * from all_types where col_varchar is not null limit 10"; + +using var reader = cmd.ExecuteReader(); +for (int i = 0; i < reader.FieldCount; i++) +{ + Console.Write(reader.GetName(i)); + Console.Write("|"); +} +Console.WriteLine(); +while (reader.Read()) +{ + for (int i = 0; i < reader.FieldCount; i++) + { + Console.Write(reader.GetValue(i)); + Console.Write("|"); + } + Console.WriteLine(); +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/README.md b/drivers/spanner-ado-net/spanner-ado-net-samples/README.md new file mode 100644 index 00000000..b07bb9b5 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/README.md @@ -0,0 +1,5 @@ +# Spanner ADO.NET Data Provider Samples + +Samples for ADO.NET Data Provider for Spanner. + +__ALPHA: Not for production use__ diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/spanner-ado-net-samples.csproj b/drivers/spanner-ado-net/spanner-ado-net-samples/spanner-ado-net-samples.csproj new file mode 100644 index 00000000..3c247b26 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/spanner-ado-net-samples.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + Google.Cloud.Spanner.DataProvider.Samples + enable + enable + Google.Cloud.Spanner.DataProvider.Samples + default + + + + + + + diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/CommandTests.cs b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/CommandTests.cs new file mode 100644 index 00000000..a2feb8ab --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/CommandTests.cs @@ -0,0 +1,123 @@ +// Copyright 2025 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 +// +// https://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. + +using AdoNet.Specification.Tests; +using Google.Cloud.SpannerLib.MockServer; +using Xunit; +using TypeCode = Google.Cloud.Spanner.V1.TypeCode; + +namespace Google.Cloud.Spanner.DataProvider.SpecificationTests; + +public class CommandTests(DbFactoryFixture fixture) : CommandTestBase(fixture) +{ + [Fact] + public override void Execute_throws_for_unknown_ParameterValue_type() + { + Fixture.MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT @Parameter;", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.String}, "Parameter", new CustomClass().ToString())); + base.Execute_throws_for_unknown_ParameterValue_type(); + } + + [Fact] + public override void ExecuteReader_binds_parameters() + { + Fixture.MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT @Parameter;", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "Parameter", 1L)); + base.ExecuteReader_binds_parameters(); + } + + [Fact] + public override void ExecuteReader_supports_CloseConnection() + { + Fixture.MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 0;", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "c", 0L)); + base.ExecuteReader_supports_CloseConnection(); + } + + [Fact] + public override void ExecuteReader_works_when_trailing_comments() + { + Fixture.MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 0; -- My favorite number", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "c", 0L)); + base.ExecuteReader_works_when_trailing_comments(); + } + + [Fact] + public override void ExecuteScalar_returns_DBNull_when_null() + { + Fixture.MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT NULL;", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "c", DBNull.Value)); + base.ExecuteScalar_returns_DBNull_when_null(); + } + + [Fact] + public override void ExecuteScalar_returns_first_when_multiple_columns() + { + Fixture.MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 42, 43;", + StatementResult.CreateResultSet([Tuple.Create(TypeCode.Int64, "c1"), Tuple.Create(TypeCode.Int64, "c1")], [[42L, 43L]])); + base.ExecuteScalar_returns_first_when_multiple_columns(); + } + + [Fact] + public override void ExecuteScalar_returns_real() + { + Fixture.MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 3.14;", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Float64}, "c", 3.14d)); + base.ExecuteScalar_returns_real(); + } + + [Fact] + public override void ExecuteScalar_returns_string_when_text() + { + Fixture.MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 'test';", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.String}, "c", "test")); + base.ExecuteScalar_returns_string_when_text(); + } + + [Fact(Skip = "Spanner does not support multiple SQL statements in one string")] + public override void ExecuteScalar_returns_first_when_batching() + { + } + + [Fact(Skip = "Spanner does not support empty statements")] + public override void ExecuteReader_HasRows_is_false_for_comment() + { + } + + [Fact(Skip = "Spanner does not use the command text once the reader has been opened")] + public override void CommandText_throws_when_set_when_open_reader() + { + } + + [Fact(Skip = "Spanner does not need the connection after the reader has been opened")] + public override void Connection_throws_when_set_when_open_reader() + { + } + + [Fact(Skip = "Spanner does not need the connection after the reader has been opened")] + public override void Connection_throws_when_set_to_null_when_open_reader() + { + } + + [Fact(Skip = "Spanner supports multiple open readers for one command")] + public override void ExecuteReader_throws_when_reader_open() + { + } + + [Fact(Skip = "Spanner only supports one transaction per connection and therefore ignores the transaction property")] + public override void ExecuteReader_throws_when_transaction_required() + { + } + +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/ConnectionStringTests.cs b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/ConnectionStringTests.cs new file mode 100644 index 00000000..0160687d --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/ConnectionStringTests.cs @@ -0,0 +1,19 @@ +// Copyright 2025 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 +// +// https://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. + +using AdoNet.Specification.Tests; + +namespace Google.Cloud.Spanner.DataProvider.SpecificationTests; + +public class ConnectionStringTests(DbFactoryFixture fixture) : ConnectionStringTestBase(fixture); \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/ConnectionTests.cs b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/ConnectionTests.cs new file mode 100644 index 00000000..eb8f2dcb --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/ConnectionTests.cs @@ -0,0 +1,19 @@ +// Copyright 2025 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 +// +// https://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. + +using AdoNet.Specification.Tests; + +namespace Google.Cloud.Spanner.DataProvider.SpecificationTests; + +public class ConnectionTests(DbFactoryFixture fixture) : ConnectionTestBase(fixture); \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DataReaderTests.cs b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DataReaderTests.cs new file mode 100644 index 00000000..028b0836 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DataReaderTests.cs @@ -0,0 +1,107 @@ +// Copyright 2025 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 +// +// https://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. + +using AdoNet.Specification.Tests; +using Google.Cloud.SpannerLib.MockServer; +using Xunit; +using TypeCode = Google.Cloud.Spanner.V1.TypeCode; + +namespace Google.Cloud.Spanner.DataProvider.SpecificationTests; + +public class DataReaderTests(DbFactoryFixture fixture) : DataReaderTestBase(fixture) +{ + [Fact(Skip = "SpannerLib does not support multiple statements in one query string")] + public override void HasRows_works_when_batching() + { + } + + [Fact(Skip = "SpannerLib does not support multiple statements in one query string")] + public override void NextResult_works() + { + } + + [Fact(Skip = "SpannerLib does not support multiple statements in one query string")] + public override void SingleResult_returns_one_result_set() + { + } + + [Fact(Skip = "SpannerLib does not support multiple statements in one query string")] + public override void SingleRow_returns_one_result_set() + { + } + + [Fact(Skip = "Getting stats after closing a DataReader is not supported")] + public override void RecordsAffected_returns_negative_1_after_close_when_no_rows() + { + } + + [Fact(Skip = "Getting stats after closing a DataReader is not supported")] + public override void RecordsAffected_returns_negative_1_after_dispose_when_no_rows() + { + } + + public override void GetFieldValue_works_utf8_four_bytes() + { + Fixture.MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT '😀';", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.String}, "c", "😀")); + base.GetFieldValue_works_utf8_four_bytes(); + } + + public override void GetString_works_utf8_four_bytes() + { + Fixture.MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT '😀';", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.String}, "c", "😀")); + base.GetString_works_utf8_four_bytes(); + } + + public override void GetValue_to_string_works_utf8_four_bytes() + { + Fixture.MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT '😀';", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.String}, "c", "😀")); + base.GetValue_to_string_works_utf8_four_bytes(); + } + + public override void GetFieldValue_works_utf8_three_bytes() + { + Fixture.MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 'Ḁ';", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.String}, "c", "Ḁ")); + base.GetFieldValue_works_utf8_three_bytes(); + } + + public override void GetFieldValue_works_utf8_two_bytes() + { + Fixture.MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 'Ä';", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.String}, "c", "Ä")); + base.GetFieldValue_works_utf8_two_bytes(); + } + + public override void GetValues_works() + { + Fixture.MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 'a', NULL;", + StatementResult.CreateResultSet([Tuple.Create(TypeCode.String, "c1"), Tuple.Create(TypeCode.Int64, "c2")], [["a", DBNull.Value]])); + base.GetValues_works(); + } + + public override void Item_by_name_works() + { + Fixture.MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 'test' AS Id;", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.String}, "Id", "test")); + base.Item_by_name_works(); + } + + [Fact(Skip = "The default implementation of GetTextReader returns an empty reader for null values")] + public override void GetTextReader_throws_for_null_String() + { + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DbFactoryFixture.cs b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DbFactoryFixture.cs new file mode 100644 index 00000000..7675bf42 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DbFactoryFixture.cs @@ -0,0 +1,441 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Data; +using System.Data.Common; +using AdoNet.Specification.Tests; +using Google.Cloud.SpannerLib.MockServer; +using TypeCode = Google.Cloud.Spanner.V1.TypeCode; + +namespace Google.Cloud.Spanner.DataProvider.SpecificationTests; + +public class DbFactoryFixture : IDisposable, ISelectValueFixture, IDeleteFixture +{ + static DbFactoryFixture() + { + AppDomain.CurrentDomain.ProcessExit += (_, _) => + { + SpannerPool.CloseSpannerLib(); + }; + } + + private bool _disposed; + internal readonly SpannerMockServerFixture MockServerFixture = new (); + + public DbProviderFactory Factory => SpannerFactory.Instance; + public string ConnectionString => $"{MockServerFixture.Host}:{MockServerFixture.Port}/projects/p1/instances/i1/databases/d1;UsePlainText=true"; + + public IReadOnlyCollection SupportedDbTypes { get; } = [ + DbType.Binary, + DbType.Boolean, + DbType.Date, + DbType.DateTime, + DbType.Decimal, + DbType.Double, + DbType.Guid, + DbType.Int64, + DbType.Single, + DbType.String, + ]; + public string SelectNoRows => "select * from (select 1) where false"; + public System.Type NullValueExceptionType { get; } = typeof(InvalidCastException); + public string DeleteNoRows => "delete from foo where false"; + + public DbFactoryFixture() + { + Reset(); + } + + public void Reset() + { + MockServerFixture.SpannerMock.Reset(); + MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 1;", StatementResult.CreateSelect1ResultSet()); + MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 1", StatementResult.CreateSelect1ResultSet()); + MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT NULL;", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "c", DBNull.Value)); + MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 1 AS id;", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "id", 1)); + MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 1 AS Id;", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "Id", 1)); + MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 'test';", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.String}, "c", "test")); + MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 'ab¢d';", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.String}, "c", "ab¢d")); + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(SelectNoRows, + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "c")); + MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 42 UNION SELECT 43;", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "c", 42, 43)); + MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 1 UNION SELECT 2;", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "c", 1, 2)); + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(DeleteNoRows, StatementResult.CreateUpdateCount(0)); + } + + public string CreateSelectSql(DbType dbType, ValueKind kind) + { + return dbType switch + { + DbType.Binary => CreateSelectSqlBinary(kind), + DbType.Boolean => CreateSelectSqlBoolean(kind), + DbType.Date => CreateSelectSqlDate(kind), + DbType.DateTime => CreateSelectSqlDateTime(kind), + DbType.Decimal => CreateSelectSqlDecimal(kind), + DbType.Double => CreateSelectSqlDouble(kind), + DbType.Guid => CreateSelectSqlGuid(kind), + DbType.Int64 => CreateSelectSqlInt64(kind), + DbType.Single => CreateSelectSqlSingle(kind), + DbType.String => CreateSelectSqlString(kind), + _ => throw new NotImplementedException("Not implemented") + }; + } + + private string CreateSelectSqlBinary(ValueKind kind) + { + var sql = "SELECT bytes_col FROM my_table;"; + switch (kind) + { + case ValueKind.Empty: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Bytes }, "bytes_col", Array.Empty())); + break; + case ValueKind.Zero: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Bytes }, "bytes_col", new byte[]{0})); + break; + case ValueKind.One: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Bytes }, "bytes_col", new byte[]{0x11})); + break; + case ValueKind.Maximum: + case ValueKind.Minimum: + case ValueKind.Null: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Bytes }, "bytes_col", DBNull.Value)); + break; + default: + throw new NotImplementedException("Not implemented"); + } + return sql; + } + private string CreateSelectSqlBoolean(ValueKind kind) + { + var sql = "SELECT bool_col FROM my_table;"; + switch (kind) + { + case ValueKind.Maximum: + case ValueKind.One: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Bool }, "bool_col", true)); + break; + case ValueKind.Empty: + case ValueKind.Minimum: + case ValueKind.Zero: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Bool }, "bool_col", false)); + break; + case ValueKind.Null: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Bool }, "bool_col", DBNull.Value)); + break; + default: + throw new NotImplementedException("Not implemented"); + } + return sql; + } + + private string CreateSelectSqlDate(ValueKind kind) + { + var sql = "SELECT date_col FROM my_table;"; + switch (kind) + { + case ValueKind.One: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Date }, "date_col", "1111-11-11")); + break; + case ValueKind.Maximum: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Date }, "date_col", "9999-12-31")); + break; + case ValueKind.Minimum: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Date }, "date_col", "0001-01-01")); + break; + case ValueKind.Zero: + case ValueKind.Empty: + case ValueKind.Null: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Date }, "date_col", DBNull.Value)); + break; + default: + throw new NotImplementedException("Not implemented"); + } + return sql; + } + + private string CreateSelectSqlDateTime(ValueKind kind) + { + var sql = "SELECT timestamp_col FROM my_table;"; + switch (kind) + { + case ValueKind.One: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Timestamp }, "timestamp_col", "1111-11-11T11:11:11.111000000Z")); + break; + case ValueKind.Maximum: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Timestamp }, "timestamp_col", "9999-12-31T23:59:59.999000000Z")); + break; + case ValueKind.Minimum: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Timestamp }, "timestamp_col", "0001-01-01T00:00:00Z")); + break; + case ValueKind.Zero: + case ValueKind.Empty: + case ValueKind.Null: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Timestamp }, "timestamp_col", DBNull.Value)); + break; + default: + throw new NotImplementedException("Not implemented"); + } + return sql; + } + + private string CreateSelectSqlDecimal(ValueKind kind) + { + var sql = "SELECT numeric_col FROM my_table;"; + switch (kind) + { + case ValueKind.Zero: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Numeric }, "numeric_col", "0")); + break; + case ValueKind.One: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Numeric }, "numeric_col", "1")); + break; + case ValueKind.Maximum: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Numeric }, "numeric_col", "99999999999999999999.999999999999999")); + break; + case ValueKind.Minimum: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Numeric }, "numeric_col", "0.000000000000001")); + break; + case ValueKind.Empty: + case ValueKind.Null: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Numeric }, "numeric_col", DBNull.Value)); + break; + default: + throw new NotImplementedException("Not implemented"); + } + return sql; + } + + private string CreateSelectSqlDouble(ValueKind kind) + { + var sql = "SELECT float64_col FROM my_table;"; + switch (kind) + { + case ValueKind.Zero: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Float64 }, "float64_col", 0.0d)); + break; + case ValueKind.One: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Float64 }, "float64_col", 1.0d)); + break; + case ValueKind.Maximum: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Float64 }, "float64_col", 1.79e308d)); + break; + case ValueKind.Minimum: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Float64 }, "float64_col", 2.23e-308d)); + break; + case ValueKind.Empty: + case ValueKind.Null: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Float64 }, "float64_col", DBNull.Value)); + break; + default: + throw new NotImplementedException("Not implemented"); + } + return sql; + } + + private string CreateSelectSqlGuid(ValueKind kind) + { + var sql = "SELECT uuid_col FROM my_table;"; + switch (kind) + { + case ValueKind.Zero: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Uuid }, "uuid_col", "00000000-0000-0000-0000-000000000000")); + break; + case ValueKind.One: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Uuid }, "uuid_col", "11111111-1111-1111-1111-111111111111")); + break; + case ValueKind.Maximum: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Uuid }, "uuid_col", "ccddeeff-aabb-8899-7766-554433221100")); + break; + case ValueKind.Minimum: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Uuid }, "uuid_col", "33221100-5544-7766-9988-aabbccddeeff")); + break; + case ValueKind.Empty: + case ValueKind.Null: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Uuid }, "uuid_col", DBNull.Value)); + break; + default: + throw new NotImplementedException("Not implemented"); + } + return sql; + } + + private string CreateSelectSqlInt64(ValueKind kind) + { + var sql = "SELECT int64_col FROM my_table;"; + switch (kind) + { + case ValueKind.Zero: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Int64 }, "int64_col", 0L)); + break; + case ValueKind.One: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Int64 }, "int64_col", 1L)); + break; + case ValueKind.Maximum: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Int64 }, "int64_col", long.MaxValue)); + break; + case ValueKind.Minimum: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Int64 }, "int64_col", long.MinValue)); + break; + case ValueKind.Empty: + case ValueKind.Null: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Int64 }, "int64_col", DBNull.Value)); + break; + default: + throw new NotImplementedException("Not implemented"); + } + return sql; + } + + private string CreateSelectSqlSingle(ValueKind kind) + { + var sql = "SELECT float32_col FROM my_table;"; + switch (kind) + { + case ValueKind.Zero: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Float32 }, "float32_col", 0.0f)); + break; + case ValueKind.One: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Float32 }, "float32_col", 1.0f)); + break; + case ValueKind.Maximum: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Float32 }, "float32_col", 3.40e38f)); + break; + case ValueKind.Minimum: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Float32 }, "float32_col", 1.18e-38f)); + break; + case ValueKind.Empty: + case ValueKind.Null: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.Float32 }, "float32_col", DBNull.Value)); + break; + default: + throw new NotImplementedException("Not implemented"); + } + return sql; + } + + private string CreateSelectSqlString(ValueKind kind) + { + var sql = "SELECT string_col FROM my_table;"; + switch (kind) + { + case ValueKind.Zero: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.String }, "string_col", "0")); + break; + case ValueKind.One: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.String }, "string_col", "1")); + break; + case ValueKind.Empty: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.String }, "string_col", "")); + break; + case ValueKind.Maximum: + case ValueKind.Minimum: + case ValueKind.Null: + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type { Code = TypeCode.String }, "string_col", DBNull.Value)); + break; + default: + throw new NotImplementedException("Not implemented"); + } + return sql; + } + + public string CreateSelectSql(byte[] value) + { + var sql = "SELECT bytes_col FROM my_table;"; + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Bytes}, "bytes_col", value)); + + return sql; + } + + protected void MarkDisposed() + { + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + try + { + MockServerFixture.Dispose(); + // var source = new CancellationTokenSource(); + // source.CancelAfter(1000); + // Task.Run(() => SpannerPool.CloseSpannerLibWhenAllConnectionsClosedAsync(source.Token), source.Token).Wait(source.Token); + } + finally + { + _disposed = true; + } + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DbFactoryTests.cs b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DbFactoryTests.cs new file mode 100644 index 00000000..7a34fecc --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DbFactoryTests.cs @@ -0,0 +1,19 @@ +// Copyright 2025 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 +// +// https://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. + +using AdoNet.Specification.Tests; + +namespace Google.Cloud.Spanner.DataProvider.SpecificationTests; + +public class DbFactoryTests(DbFactoryFixture fixture) : DbFactoryTestBase(fixture); \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DbProviderFactoryTests.cs b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DbProviderFactoryTests.cs new file mode 100644 index 00000000..6ddf7eee --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DbProviderFactoryTests.cs @@ -0,0 +1,19 @@ +// Copyright 2025 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 +// +// https://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. + +using AdoNet.Specification.Tests; + +namespace Google.Cloud.Spanner.DataProvider.SpecificationTests; + +public class DbProviderFactoryTests(DbFactoryFixture fixture) : DbProviderFactoryTestBase(fixture); \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/GetValueConversionTests.cs b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/GetValueConversionTests.cs new file mode 100644 index 00000000..4f4870f0 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/GetValueConversionTests.cs @@ -0,0 +1,138 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Data; +using System.Globalization; +using AdoNet.Specification.Tests; + +namespace Google.Cloud.Spanner.DataProvider.SpecificationTests; + +public class GetValueConversionTests(DbFactoryFixture fixture) : GetValueConversionTestBase(fixture) +{ + // Spanner uses DateOnly for DATE columns. + public override void GetFieldType_for_Date() => TestGetFieldType(DbType.Date, ValueKind.One, typeof(DateOnly)); + + public override void GetValue_for_Date() => TestGetValue(DbType.Date, ValueKind.One, new DateOnly(1111, 11, 11)); + + + // Spanner allows string values to be cast to numerical values. + public override void GetDecimal_throws_for_zero_String() => TestGetValue(DbType.String, ValueKind.Zero, x => x.GetDecimal(0), 0.0m); + + public override void GetDecimal_throws_for_one_String() => TestGetValue(DbType.String, ValueKind.One, x => x.GetDecimal(0), 1.0m); + + public override void GetDouble_throws_for_zero_String() => TestGetValue(DbType.String, ValueKind.Zero, x => x.GetDouble(0), 0.0d); + + public override void GetDouble_throws_for_one_String() => TestGetValue(DbType.String, ValueKind.One, x => x.GetDouble(0), 1.0d); + + public override void GetDouble_throws_for_zero_String_with_GetFieldValue() => TestGetValue(DbType.String, ValueKind.Zero, x => x.GetFieldValue(0), 0.0d); + + public override async Task GetDouble_throws_for_zero_String_with_GetFieldValueAsync() => await TestGetValueAsync(DbType.String, ValueKind.Zero, async x => await x.GetFieldValueAsync(0), 0.0d); + + public override void GetFloat_throws_for_one_String() => TestGetValue(DbType.String, ValueKind.One, x => x.GetFloat(0), 1.0f); + + public override void GetFloat_throws_for_zero_String() => TestGetValue(DbType.String, ValueKind.Zero, x => x.GetFloat(0), 0.0f); + + public override void GetFloat_throws_for_zero_String_with_GetFieldValue() => TestGetValue(DbType.String, ValueKind.Zero, x => x.GetFieldValue(0), 0.0f); + + public override async Task GetFloat_throws_for_zero_String_with_GetFieldValueAsync() => await TestGetValueAsync(DbType.String, ValueKind.Zero, async x => await x.GetFieldValueAsync(0), 0.0f); + + public override void GetInt16_throws_for_one_String() => TestGetValue(DbType.String, ValueKind.One, x => x.GetInt16(0), (short) 1); + + public override void GetInt16_throws_for_zero_String() => TestGetValue(DbType.String, ValueKind.Zero, x => x.GetInt16(0), (short) 0); + + public override void GetInt16_throws_for_zero_String_with_GetFieldValue() => TestGetValue(DbType.String, ValueKind.Zero, x => x.GetFieldValue(0), (short) 0); + + public override async Task GetInt16_throws_for_zero_String_with_GetFieldValueAsync() => await TestGetValueAsync(DbType.String, ValueKind.Zero, async x => await x.GetFieldValueAsync(0), (short) 0); + + public override void GetInt32_throws_for_one_String() => TestGetValue(DbType.String, ValueKind.One, x => x.GetInt32(0), 1); + + public override void GetInt32_throws_for_zero_String() => TestGetValue(DbType.String, ValueKind.Zero, x => x.GetInt32(0), 0); + + public override void GetInt32_throws_for_zero_String_with_GetFieldValue() => TestGetValue(DbType.String, ValueKind.Zero, x => x.GetFieldValue(0), 0); + + public override async Task GetInt32_throws_for_zero_String_with_GetFieldValueAsync() => await TestGetValueAsync(DbType.String, ValueKind.Zero, async x => await x.GetFieldValueAsync(0), 0); + + public override void GetInt64_throws_for_one_String() => TestGetValue(DbType.String, ValueKind.One, x => x.GetInt64(0), 1L); + + public override void GetInt64_throws_for_zero_String() => TestGetValue(DbType.String, ValueKind.Zero, x => x.GetInt64(0), 0L); + + public override void GetInt64_throws_for_zero_String_with_GetFieldValue() => TestGetValue(DbType.String, ValueKind.Zero, x => x.GetFieldValue(0), 0L); + + public override async Task GetInt64_throws_for_zero_String_with_GetFieldValueAsync() => await TestGetValueAsync(DbType.String, ValueKind.Zero, async x => await x.GetFieldValueAsync(0), 0L); + + public override void GetString_throws_for_maximum_Boolean() => TestGetValue(DbType.Boolean, ValueKind.Maximum, x => x.GetString(0), "True"); + + public override void GetString_throws_for_maximum_Decimal() => TestGetValue(DbType.Decimal, ValueKind.Maximum, x => x.GetString(0), "99999999999999999999.999999999999999"); + + public override void GetString_throws_for_maximum_Double() => TestGetValue(DbType.Double, ValueKind.Maximum, x => x.GetString(0), "1.79E+308"); + + public override void GetString_throws_for_maximum_Int64() => TestGetValue(DbType.Int64, ValueKind.Maximum, x => x.GetString(0), long.MaxValue.ToString(CultureInfo.InvariantCulture)); + + public override void GetString_throws_for_maximum_Single() => TestGetValue(DbType.Single, ValueKind.Maximum, x => x.GetString(0), 3.40e38f.ToString(CultureInfo.InvariantCulture)); + + public override void GetString_throws_for_minimum_Boolean() => TestGetValue(DbType.Boolean, ValueKind.Minimum, x => x.GetString(0), "False"); + + public override void GetString_throws_for_minimum_Decimal() => TestGetValue(DbType.Decimal, ValueKind.Minimum, x => x.GetString(0), "0.000000000000001"); + + public override void GetString_throws_for_minimum_Double() => TestGetValue(DbType.Double, ValueKind.Minimum, x => x.GetString(0), "2.23E-308"); + + public override void GetString_throws_for_minimum_Int64() => TestGetValue(DbType.Int64, ValueKind.Minimum, x => x.GetString(0), long.MinValue.ToString(CultureInfo.InvariantCulture)); + + public override void GetString_throws_for_minimum_Single() => TestGetValue(DbType.Single, ValueKind.Minimum, x => x.GetString(0), "1.18E-38"); + + public override void GetString_throws_for_one_Boolean() => TestGetValue(DbType.Boolean, ValueKind.One, x => x.GetString(0), "True"); + + public override void GetString_throws_for_one_Decimal() => TestGetValue(DbType.Decimal, ValueKind.One, x => x.GetString(0), "1"); + + public override void GetString_throws_for_one_Double() => TestGetValue(DbType.Double, ValueKind.One, x => x.GetString(0), "1"); + + public override void GetString_throws_for_one_Guid() => TestGetValue(DbType.Guid, ValueKind.One, x => x.GetString(0), "11111111-1111-1111-1111-111111111111"); + + public override void GetString_throws_for_one_Int64() => TestGetValue(DbType.Int64, ValueKind.One, x => x.GetString(0), "1"); + + public override void GetString_throws_for_one_Single() => TestGetValue(DbType.Single, ValueKind.One, x => x.GetString(0), "1"); + + public override void GetString_throws_for_zero_Boolean() => TestGetValue(DbType.Boolean, ValueKind.Zero, x => x.GetString(0), "False"); + + public override void GetString_throws_for_zero_Decimal() => TestGetValue(DbType.Decimal, ValueKind.Zero, x => x.GetString(0), "0"); + + public override void GetString_throws_for_zero_Double() => TestGetValue(DbType.Double, ValueKind.Zero, x => x.GetString(0), "0"); + + public override void GetString_throws_for_zero_Guid() => TestGetValue(DbType.Guid, ValueKind.Zero, x => x.GetString(0), "00000000-0000-0000-0000-000000000000"); + + public override void GetString_throws_for_zero_Int64() => TestGetValue(DbType.Int64, ValueKind.Zero, x => x.GetString(0), "0"); + + public override void GetString_throws_for_zero_Single() => TestGetValue(DbType.Single, ValueKind.Zero, x => x.GetString(0), "0"); + + public override void GetDouble_throws_for_one_String_with_GetFieldValue() => TestGetValue(DbType.String, ValueKind.One, x => x.GetFieldValue(0), 1.0d); + + public override async Task GetDouble_throws_for_one_String_with_GetFieldValueAsync() => await TestGetValueAsync(DbType.String, ValueKind.One, async x => await x.GetFieldValueAsync(0), 1.0d); + + public override void GetFloat_throws_for_one_String_with_GetFieldValue() => TestGetValue(DbType.String, ValueKind.One, x => x.GetFieldValue(0), 1.0f); + + public override async Task GetFloat_throws_for_one_String_with_GetFieldValueAsync() => await TestGetValueAsync(DbType.String, ValueKind.One, async x => await x.GetFieldValueAsync(0), 1.0f); + + public override void GetInt16_throws_for_one_String_with_GetFieldValue() => TestGetValue(DbType.String, ValueKind.One, x => x.GetFieldValue(0), (short) 1); + + public override async Task GetInt16_throws_for_one_String_with_GetFieldValueAsync() => await TestGetValueAsync(DbType.String, ValueKind.One, async x => await x.GetFieldValueAsync(0), (short) 1); + + public override void GetInt32_throws_for_one_String_with_GetFieldValue() => TestGetValue(DbType.String, ValueKind.One, x => x.GetFieldValue(0), 1); + + public override async Task GetInt32_throws_for_one_String_with_GetFieldValueAsync() => await TestGetValueAsync(DbType.String, ValueKind.One, async x => await x.GetFieldValueAsync(0), 1); + + public override void GetInt64_throws_for_one_String_with_GetFieldValue() => TestGetValue(DbType.String, ValueKind.One, x => x.GetFieldValue(0), 1L); + + public override async Task GetInt64_throws_for_one_String_with_GetFieldValueAsync() => await TestGetValueAsync(DbType.String, ValueKind.One, async x => await x.GetFieldValueAsync(0), 1L); + +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/ParameterTests.cs b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/ParameterTests.cs new file mode 100644 index 00000000..1a33a87b --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/ParameterTests.cs @@ -0,0 +1,83 @@ +// Copyright 2025 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 +// +// https://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. + +using AdoNet.Specification.Tests; +using Google.Cloud.Spanner.V1; +using Google.Cloud.SpannerLib.MockServer; +using Xunit; +using TypeCode = Google.Cloud.Spanner.V1.TypeCode; + +namespace Google.Cloud.Spanner.DataProvider.SpecificationTests; + +public class ParameterTests(DbFactoryFixture fixture) : ParameterTestBase(fixture) +{ + protected override Task OnInitializeAsync() + { + Fixture.Reset(); + return base.OnInitializeAsync(); + } + + [Fact(Skip = "Spanner assumes that it is a positional parameter if it has no name")] + public override void Bind_requires_set_name() + { + } + + [Fact(Skip = "Unknown parameters are converted to strings")] + public override void Bind_throws_when_unknown() + { + } + + [Fact] + public override void Bind_works_with_byte_array() + { + Fixture.MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT @Parameter;", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Bytes}, "Parameter", new byte[]{1,2,3,4})); + base.Bind_works_with_byte_array(); + + var requests = Fixture.MockServerFixture.SpannerMock.Requests.OfType(); + var request = Assert.Single(requests); + Assert.Equal(Convert.ToBase64String(new byte[]{1,2,3,4}), request.Params.Fields["Parameter"].StringValue); + // The parameter value should be sent as an untyped string. + Assert.False(request.ParamTypes.ContainsKey("Parameter")); + } + + [Fact] + public override void Bind_works_with_stream() + { + Fixture.MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT @Parameter;", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Bytes}, "Parameter", new byte[]{1,2,3,4})); + base.Bind_works_with_stream(); + + var requests = Fixture.MockServerFixture.SpannerMock.Requests.OfType(); + var request = Assert.Single(requests); + Assert.Equal(Convert.ToBase64String(new byte[]{1,2,3,4}), request.Params.Fields["Parameter"].StringValue); + // The parameter value should be sent as an untyped string. + Assert.False(request.ParamTypes.ContainsKey("Parameter")); + } + + [Fact] + public override void Bind_works_with_string() + { + Fixture.MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT @Parameter;", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.String}, "Parameter", "test")); + base.Bind_works_with_string(); + + var requests = Fixture.MockServerFixture.SpannerMock.Requests.OfType(); + var request = Assert.Single(requests); + Assert.Equal("test", request.Params.Fields["Parameter"].StringValue); + // The parameter value should be sent as an untyped string. + Assert.False(request.ParamTypes.ContainsKey("Parameter")); + } + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/README.md b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/README.md new file mode 100644 index 00000000..9bbfd808 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/README.md @@ -0,0 +1,5 @@ +# Spanner ADO.NET Data Provider Specification Tests + +Specification tests for ADO.NET Data Provider for Spanner. + +__ALPHA: Not for production use__ diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/TransactionTests.cs b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/TransactionTests.cs new file mode 100644 index 00000000..faf8b719 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/TransactionTests.cs @@ -0,0 +1,19 @@ +// Copyright 2025 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 +// +// https://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. + +using AdoNet.Specification.Tests; + +namespace Google.Cloud.Spanner.DataProvider.SpecificationTests; + +public class TransactionTests(DbFactoryFixture fixture) : TransactionTestBase(fixture); \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/appsettings.json b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/appsettings.json new file mode 100644 index 00000000..2a8537d3 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Microsoft": "Warning" + } + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/spanner-ado-net-specification-tests.csproj b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/spanner-ado-net-specification-tests.csproj new file mode 100644 index 00000000..ca134215 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/spanner-ado-net-specification-tests.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + Google.Cloud.Spanner.DataProvider.SpecificationTests + enable + enable + + false + true + Google.Cloud.Spanner.DataProvider.SpecificationTests + default + + + + + + + + + + + + + + + + + + + diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs new file mode 100644 index 00000000..d4db3812 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs @@ -0,0 +1,57 @@ +// Copyright 2025 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 +// +// https://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. + +using System; +using Google.Cloud.SpannerLib.MockServer; + +namespace Google.Cloud.Spanner.DataProvider.Tests; + +public abstract class AbstractMockServerTests +{ + static AbstractMockServerTests() + { + AppDomain.CurrentDomain.ProcessExit += (_, _) => + { + SpannerPool.CloseSpannerLib(); + }; + } + + protected SpannerMockServerFixture Fixture; + + protected string ConnectionString => $"{Fixture.Host}:{Fixture.Port}/projects/p1/instances/i1/databases/d1;UsePlainText=true"; + + [OneTimeSetUp] + public void Setup() + { + Fixture = new SpannerMockServerFixture(); + } + + [OneTimeTearDown] + public void Teardown() + { + Fixture.Dispose(); + } + + [SetUp] + public void SetupResults() + { + Fixture.SpannerMock.AddOrUpdateStatementResult("SELECT 1", StatementResult.CreateSelect1ResultSet()); + } + + [TearDown] + public void Reset() + { + Fixture.SpannerMock.Reset(); + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/BasicTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/BasicTests.cs new file mode 100644 index 00000000..613b443b --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/BasicTests.cs @@ -0,0 +1,111 @@ +using System; +using System.Data.Common; +using System.Linq; +using System.Text.Json; +using Google.Cloud.Spanner.V1; +using Google.Cloud.SpannerLib.MockServer; + +namespace Google.Cloud.Spanner.DataProvider.Tests; + +public class BasicTests : AbstractMockServerTests +{ + [Test] + public void TestOpenConnection() + { + var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + connection.Open(); + connection.Close(); + } + + [Test] + public void TestExecuteQuery() + { + using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + connection.Open(); + + using var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT 1"; + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + Assert.That(reader.GetInt64(0), Is.EqualTo(1)); + } + } + + [Test] + public void TestExecuteParameterizedQuery() + { + Fixture.SpannerMock.AddOrUpdateStatementResult("SELECT $1", StatementResult.CreateSelect1ResultSet()); + using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + connection.Open(); + + using var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT $1"; + var param = cmd.CreateParameter(); + param.ParameterName = "p1"; + param.Value = 1; + cmd.Parameters.Add(param); + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + Assert.That(reader.GetInt64(0), Is.EqualTo(1)); + } + } + + [Test] + public void TestInsertAllDataTypes() + { + var sql = "insert into all_types (col_bool, col_bytes, col_date, col_interval, col_json, col_int64, col_float32, col_float64, col_numeric, col_string, col_timestamp) " + + "values (@col_bool, @col_bytes, @col_date, @col_interval, @col_json, @col_int64, @col_float32, @col_float64, @col_numeric, @col_string, @col_timestamp)"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(1)); + + using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + connection.Open(); + + using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + AddParameter(cmd, "col_bool", true); + AddParameter(cmd, "col_bytes", new byte[] { 1, 2, 3 }); + AddParameter(cmd, "col_date", new DateOnly(2025, 8, 25)); + AddParameter(cmd, "col_interval", TimeSpan.FromHours(1)); + AddParameter(cmd, "col_json", JsonDocument.Parse("{\"key\":\"value\"}")); + AddParameter(cmd, "col_int64", 10); + AddParameter(cmd, "col_float32", 3.14f); + AddParameter(cmd, "col_float64", 3.14d); + AddParameter(cmd, "col_numeric", 10.1m); + AddParameter(cmd, "col_string", "hello"); + AddParameter(cmd, "col_timestamp", DateTime.Parse("2025-08-25T16:30:55Z")); + + var updateCount = cmd.ExecuteNonQuery(); + Assert.That(updateCount, Is.EqualTo(1)); + + var requests = Fixture.SpannerMock.Requests.OfType().ToList(); + Assert.That(requests, Has.Count.EqualTo(1)); + var request = requests.First(); + Assert.That(request.Params.Fields, Has.Count.EqualTo(11)); + Assert.That(request.Params.Fields["col_bool"].BoolValue, Is.EqualTo(true)); + Assert.That(request.Params.Fields["col_bytes"].StringValue, Is.EqualTo(Convert.ToBase64String(new byte[]{1,2,3}))); + Assert.That(request.Params.Fields["col_date"].StringValue, Is.EqualTo("2025-08-25")); + Assert.That(request.Params.Fields["col_interval"].StringValue, Is.EqualTo("PT1H")); + Assert.That(request.Params.Fields["col_int64"].StringValue, Is.EqualTo("10")); + Assert.That(request.Params.Fields["col_float32"].NumberValue, Is.EqualTo(3.14f)); + Assert.That(request.Params.Fields["col_float64"].NumberValue, Is.EqualTo(3.14d)); + Assert.That(request.Params.Fields["col_numeric"].StringValue, Is.EqualTo("10.1")); + Assert.That(request.Params.Fields["col_string"].StringValue, Is.EqualTo("hello")); + Assert.That(request.Params.Fields["col_timestamp"].StringValue, Is.EqualTo("2025-08-25T16:30:55.0000000Z")); + + Assert.That(request.ParamTypes.Count, Is.EqualTo(0)); + } + + private void AddParameter(DbCommand command, string name, object value) + { + var param = command.CreateParameter(); + param.ParameterName = name; + param.Value = value; + command.Parameters.Add(param); + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/BatchTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/BatchTests.cs new file mode 100644 index 00000000..6590020c --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/BatchTests.cs @@ -0,0 +1,272 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Data.Common; +using System.Text.Json; +using Google.Cloud.Spanner.V1; +using Google.Cloud.SpannerLib.MockServer; +using Google.Protobuf.WellKnownTypes; + +namespace Google.Cloud.Spanner.DataProvider.Tests; + +public class BatchTests : AbstractMockServerTests +{ + [TestCase(1, false)] + [TestCase(2, false)] + [TestCase(5, false)] + [TestCase(1, true)] + [TestCase(2, true)] + [TestCase(5, true)] + public async Task TestAllParameterTypes(int numCommands, bool executeAsync) + { + await using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + await connection.OpenAsync(); + + const string insert = "insert into my_table values (@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12)"; + Fixture.SpannerMock.AddOrUpdateStatementResult(insert, StatementResult.CreateUpdateCount(1)); + + await using var batch = connection.CreateBatch(); + + for (var i = 0; i < numCommands; i++) + { + var command = batch.CreateBatchCommand(); + command.CommandText = insert; + // TODO: + // - PROTO + // - STRUCT + AddParameter(command, "p1", true); + AddParameter(command, "p2", new byte[] { 1, 2, 3 }); + AddParameter(command, "p3", new DateOnly(2025, 10, 2)); + AddParameter(command, "p4", new TimeSpan(1, 2, 3, 4, 5, 6)); + AddParameter(command, "p5", JsonDocument.Parse("{\"key\": \"value\"}")); + AddParameter(command, "p6", 9.99m); + AddParameter(command, "p7", "test"); + AddParameter(command, "p8", new DateTime(2025, 10, 2, 15, 57, 31, 999, DateTimeKind.Utc)); + AddParameter(command, "p9", Guid.Parse("5555990c-b259-4539-bd22-5a9293cf10ac")); + AddParameter(command, "p10", 3.14d); + AddParameter(command, "p11", 3.14f); + AddParameter(command, "p12", DBNull.Value); + + AddParameter(command, "p13", new bool?[] { true, false, null }); + AddParameter(command, "p14", new byte[]?[] { [1, 2, 3], null }); + AddParameter(command, "p15", new DateOnly?[] { new DateOnly(2025, 10, 2), null }); + AddParameter(command, "p16", new TimeSpan?[] { new TimeSpan(1, 2, 3, 4, 5, 6), null }); + AddParameter(command, "p17", new[] { JsonDocument.Parse("{\"key\": \"value\"}"), null }); + AddParameter(command, "p18", new decimal?[] { 9.99m, null }); + AddParameter(command, "p19", new[] { "test", null }); + AddParameter(command, "p20", + new DateTime?[] { new DateTime(2025, 10, 2, 15, 57, 31, 999, DateTimeKind.Utc), null }); + AddParameter(command, "p21", new Guid?[] { Guid.Parse("5555990c-b259-4539-bd22-5a9293cf10ac"), null }); + AddParameter(command, "p22", new double?[] { 3.14d, null }); + AddParameter(command, "p23", new float?[] { 3.14f, null }); + + batch.BatchCommands.Add(command); + } + + int affected; + if (executeAsync) + { + affected = await batch.ExecuteNonQueryAsync(); + } + else + { + // ReSharper disable once MethodHasAsyncOverload + affected = batch.ExecuteNonQuery(); + } + Assert.That(affected, Is.EqualTo(numCommands)); + foreach (var command in batch.BatchCommands) + { + Assert.That(command.RecordsAffected, Is.EqualTo(1)); + } + + var requests = Fixture.SpannerMock.Requests.ToList(); + Assert.That(requests.OfType().Count, Is.EqualTo(1)); + Assert.That(requests.OfType().Count, Is.EqualTo(1)); + var request = requests.OfType().Single(); + Assert.That(request.Statements.Count, Is.EqualTo(numCommands)); + foreach (var statement in request.Statements) + { + // The driver does not send any parameter types, unless it is explicitly asked to do so. + Assert.That(statement.ParamTypes.Count, Is.EqualTo(0)); + Assert.That(statement.Params.Fields.Count, Is.EqualTo(23)); + var fields = statement.Params.Fields; + Assert.That(fields["p1"].HasBoolValue, Is.True); + Assert.That(fields["p1"].BoolValue, Is.True); + Assert.That(fields["p2"].HasStringValue, Is.True); + Assert.That(fields["p2"].StringValue, Is.EqualTo(Convert.ToBase64String(new byte[] { 1, 2, 3 }))); + Assert.That(fields["p3"].HasStringValue, Is.True); + Assert.That(fields["p3"].StringValue, Is.EqualTo("2025-10-02")); + Assert.That(fields["p4"].HasStringValue, Is.True); + Assert.That(fields["p4"].StringValue, Is.EqualTo("P1DT2H3M4.005006S")); + Assert.That(fields["p5"].HasStringValue, Is.True); + Assert.That(fields["p5"].StringValue, Is.EqualTo("{\"key\": \"value\"}")); + Assert.That(fields["p6"].HasStringValue, Is.True); + Assert.That(fields["p6"].StringValue, Is.EqualTo("9.99")); + Assert.That(fields["p7"].HasStringValue, Is.True); + Assert.That(fields["p7"].StringValue, Is.EqualTo("test")); + Assert.That(fields["p8"].HasStringValue, Is.True); + Assert.That(fields["p8"].StringValue, Is.EqualTo("2025-10-02T15:57:31.9990000Z")); + Assert.That(fields["p9"].HasStringValue, Is.True); + Assert.That(fields["p9"].StringValue, Is.EqualTo("5555990c-b259-4539-bd22-5a9293cf10ac")); + Assert.That(fields["p10"].HasNumberValue, Is.True); + Assert.That(fields["p10"].NumberValue, Is.EqualTo(3.14d)); + Assert.That(fields["p11"].HasNumberValue, Is.True); + Assert.That(fields["p11"].NumberValue, Is.EqualTo(3.14f)); + Assert.That(fields["p12"].HasNullValue, Is.True); + + Assert.That(fields["p13"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p13"].ListValue.Values.Count, Is.EqualTo(3)); + Assert.That(fields["p13"].ListValue.Values[0].HasBoolValue, Is.True); + Assert.That(fields["p13"].ListValue.Values[0].BoolValue, Is.True); + Assert.That(fields["p13"].ListValue.Values[1].HasBoolValue, Is.True); + Assert.That(fields["p13"].ListValue.Values[1].BoolValue, Is.False); + Assert.That(fields["p13"].ListValue.Values[2].HasNullValue, Is.True); + + Assert.That(fields["p14"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p14"].ListValue.Values.Count, Is.EqualTo(2)); + Assert.That(fields["p14"].ListValue.Values[0].HasStringValue, Is.True); + Assert.That(fields["p14"].ListValue.Values[0].StringValue, + Is.EqualTo(Convert.ToBase64String(new byte[] { 1, 2, 3 }))); + Assert.That(fields["p14"].ListValue.Values[1].HasNullValue, Is.True); + + Assert.That(fields["p15"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p15"].ListValue.Values.Count, Is.EqualTo(2)); + Assert.That(fields["p15"].ListValue.Values[0].HasStringValue, Is.True); + Assert.That(fields["p15"].ListValue.Values[0].StringValue, Is.EqualTo("2025-10-02")); + Assert.That(fields["p15"].ListValue.Values[1].HasNullValue, Is.True); + + Assert.That(fields["p16"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p16"].ListValue.Values.Count, Is.EqualTo(2)); + Assert.That(fields["p16"].ListValue.Values[0].HasStringValue, Is.True); + Assert.That(fields["p16"].ListValue.Values[0].StringValue, Is.EqualTo("P1DT2H3M4.005006S")); + Assert.That(fields["p16"].ListValue.Values[1].HasNullValue, Is.True); + + Assert.That(fields["p17"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p17"].ListValue.Values.Count, Is.EqualTo(2)); + Assert.That(fields["p17"].ListValue.Values[0].HasStringValue, Is.True); + Assert.That(fields["p17"].ListValue.Values[0].StringValue, Is.EqualTo("{\"key\": \"value\"}")); + Assert.That(fields["p17"].ListValue.Values[1].HasNullValue, Is.True); + + Assert.That(fields["p18"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p18"].ListValue.Values.Count, Is.EqualTo(2)); + Assert.That(fields["p18"].ListValue.Values[0].HasStringValue, Is.True); + Assert.That(fields["p18"].ListValue.Values[0].StringValue, Is.EqualTo("9.99")); + Assert.That(fields["p18"].ListValue.Values[1].HasNullValue, Is.True); + + Assert.That(fields["p19"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p19"].ListValue.Values.Count, Is.EqualTo(2)); + Assert.That(fields["p19"].ListValue.Values[0].HasStringValue, Is.True); + Assert.That(fields["p19"].ListValue.Values[0].StringValue, Is.EqualTo("test")); + Assert.That(fields["p19"].ListValue.Values[1].HasNullValue, Is.True); + + Assert.That(fields["p20"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p20"].ListValue.Values.Count, Is.EqualTo(2)); + Assert.That(fields["p20"].ListValue.Values[0].HasStringValue, Is.True); + Assert.That(fields["p20"].ListValue.Values[0].StringValue, Is.EqualTo("2025-10-02T15:57:31.9990000Z")); + Assert.That(fields["p20"].ListValue.Values[1].HasNullValue, Is.True); + + Assert.That(fields["p21"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p21"].ListValue.Values.Count, Is.EqualTo(2)); + Assert.That(fields["p21"].ListValue.Values[0].HasStringValue, Is.True); + Assert.That(fields["p21"].ListValue.Values[0].StringValue, + Is.EqualTo("5555990c-b259-4539-bd22-5a9293cf10ac")); + Assert.That(fields["p21"].ListValue.Values[1].HasNullValue, Is.True); + + Assert.That(fields["p22"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p22"].ListValue.Values.Count, Is.EqualTo(2)); + Assert.That(fields["p22"].ListValue.Values[0].HasNumberValue, Is.True); + Assert.That(fields["p22"].ListValue.Values[0].NumberValue, Is.EqualTo(3.14d)); + Assert.That(fields["p22"].ListValue.Values[1].HasNullValue, Is.True); + + Assert.That(fields["p23"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p23"].ListValue.Values.Count, Is.EqualTo(2)); + Assert.That(fields["p23"].ListValue.Values[0].HasNumberValue, Is.True); + Assert.That(fields["p23"].ListValue.Values[0].NumberValue, Is.EqualTo(3.14f)); + Assert.That(fields["p23"].ListValue.Values[1].HasNullValue, Is.True); + } + } + + [TestCase(true)] + [TestCase(false)] + public async Task TestEmptyBatch(bool executeAsync) + { + await using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + await connection.OpenAsync(); + + await using var batch = connection.CreateBatch(); + int affected; + if (executeAsync) + { + affected = await batch.ExecuteNonQueryAsync(); + } + else + { + // ReSharper disable once MethodHasAsyncOverload + affected = batch.ExecuteNonQuery(); + } + Assert.That(affected, Is.EqualTo(0)); + } + + [TestCase(true)] + [TestCase(false)] + public async Task TestExecuteReader(bool executeAsync) + { + await using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + await connection.OpenAsync(); + + await using var batch = connection.CreateBatch(); + var command = batch.CreateBatchCommand(); + command.CommandText = "select * from my_table"; + if (executeAsync) + { + Assert.ThrowsAsync(() => batch.ExecuteReaderAsync()); + } + else + { + Assert.Throws(() => batch.ExecuteReader()); + } + } + + [TestCase(true)] + [TestCase(false)] + public async Task TestExecuteScalar(bool executeAsync) + { + await using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + await connection.OpenAsync(); + + await using var batch = connection.CreateBatch(); + var command = batch.CreateBatchCommand(); + command.CommandText = "select * from my_table"; + if (executeAsync) + { + Assert.ThrowsAsync(() => batch.ExecuteScalarAsync()); + } + else + { + Assert.Throws(() => batch.ExecuteScalar()); + } + } + + private static void AddParameter(DbBatchCommand command, string name, object? value) + { + var parameter = command.CreateParameter(); + parameter.ParameterName = name; + parameter.Value = value; + command.Parameters.Add(parameter); + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs new file mode 100644 index 00000000..02df9764 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs @@ -0,0 +1,178 @@ +// Copyright 2025 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 +// +// https://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. + +using System; +using System.Data.Common; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Google.Cloud.Spanner.V1; +using Google.Cloud.SpannerLib.MockServer; +using Google.Protobuf.WellKnownTypes; + +namespace Google.Cloud.Spanner.DataProvider.Tests; + +public class CommandTests : AbstractMockServerTests +{ + [Test] + public async Task TestAllParameterTypes() + { + var insert = "insert into my_table values (@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12)"; + Fixture.SpannerMock.AddOrUpdateStatementResult(insert, StatementResult.CreateUpdateCount(1)); + + await using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + await connection.OpenAsync(); + + await using var command = connection.CreateCommand(); + command.CommandText = insert; + // TODO: + // - PROTO + // - STRUCT + AddParameter(command, "p1", true); + AddParameter(command, "p2", new byte[] {1, 2, 3}); + AddParameter(command, "p3", new DateOnly(2025, 10, 2)); + AddParameter(command, "p4", new TimeSpan(1, 2, 3, 4, 5, 6)); + AddParameter(command, "p5", JsonDocument.Parse("{\"key\": \"value\"}")); + AddParameter(command, "p6", 9.99m); + AddParameter(command, "p7", "test"); + AddParameter(command, "p8", new DateTime(2025, 10, 2, 15, 57, 31, 999, DateTimeKind.Utc)); + AddParameter(command, "p9", Guid.Parse("5555990c-b259-4539-bd22-5a9293cf10ac")); + AddParameter(command, "p10", 3.14d); + AddParameter(command, "p11", 3.14f); + AddParameter(command, "p12", DBNull.Value); + + AddParameter(command, "p13", new bool?[]{true, false, null}); + AddParameter(command, "p14", new byte[]?[]{ [1,2,3], null }); + AddParameter(command, "p15", new DateOnly?[] { new DateOnly(2025, 10, 2), null }); + AddParameter(command, "p16", new TimeSpan?[] { new TimeSpan(1, 2, 3, 4, 5, 6), null }); + AddParameter(command, "p17", new [] { JsonDocument.Parse("{\"key\": \"value\"}"), null }); + AddParameter(command, "p18", new decimal?[] { 9.99m, null }); + AddParameter(command, "p19", new [] { "test", null }); + AddParameter(command, "p20", new DateTime?[] { new DateTime(2025, 10, 2, 15, 57, 31, 999, DateTimeKind.Utc), null }); + AddParameter(command, "p21", new Guid?[] { Guid.Parse("5555990c-b259-4539-bd22-5a9293cf10ac"), null }); + AddParameter(command, "p22", new double?[] { 3.14d, null }); + AddParameter(command, "p23", new float?[] { 3.14f, null }); + + await command.ExecuteNonQueryAsync(); + + var requests = Fixture.SpannerMock.Requests.ToList(); + Assert.That(requests.OfType().Count, Is.EqualTo(1)); + Assert.That(requests.OfType().Count, Is.EqualTo(1)); + var request = requests.OfType().Single(); + // The driver does not send any parameter types, unless it is explicitly asked to do so. + Assert.That(request.ParamTypes.Count, Is.EqualTo(0)); + Assert.That(request.Params.Fields.Count, Is.EqualTo(23)); + var fields = request.Params.Fields; + Assert.That(fields["p1"].HasBoolValue, Is.True); + Assert.That(fields["p1"].BoolValue, Is.True); + Assert.That(fields["p2"].HasStringValue, Is.True); + Assert.That(fields["p2"].StringValue, Is.EqualTo(Convert.ToBase64String(new byte[]{1,2,3}))); + Assert.That(fields["p3"].HasStringValue, Is.True); + Assert.That(fields["p3"].StringValue, Is.EqualTo("2025-10-02")); + Assert.That(fields["p4"].HasStringValue, Is.True); + Assert.That(fields["p4"].StringValue, Is.EqualTo("P1DT2H3M4.005006S")); + Assert.That(fields["p5"].HasStringValue, Is.True); + Assert.That(fields["p5"].StringValue, Is.EqualTo("{\"key\": \"value\"}")); + Assert.That(fields["p6"].HasStringValue, Is.True); + Assert.That(fields["p6"].StringValue, Is.EqualTo("9.99")); + Assert.That(fields["p7"].HasStringValue, Is.True); + Assert.That(fields["p7"].StringValue, Is.EqualTo("test")); + Assert.That(fields["p8"].HasStringValue, Is.True); + Assert.That(fields["p8"].StringValue, Is.EqualTo("2025-10-02T15:57:31.9990000Z")); + Assert.That(fields["p9"].HasStringValue, Is.True); + Assert.That(fields["p9"].StringValue, Is.EqualTo("5555990c-b259-4539-bd22-5a9293cf10ac")); + Assert.That(fields["p10"].HasNumberValue, Is.True); + Assert.That(fields["p10"].NumberValue, Is.EqualTo(3.14d)); + Assert.That(fields["p11"].HasNumberValue, Is.True); + Assert.That(fields["p11"].NumberValue, Is.EqualTo(3.14f)); + Assert.That(fields["p12"].HasNullValue, Is.True); + + Assert.That(fields["p13"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p13"].ListValue.Values.Count, Is.EqualTo(3)); + Assert.That(fields["p13"].ListValue.Values[0].HasBoolValue, Is.True); + Assert.That(fields["p13"].ListValue.Values[0].BoolValue, Is.True); + Assert.That(fields["p13"].ListValue.Values[1].HasBoolValue, Is.True); + Assert.That(fields["p13"].ListValue.Values[1].BoolValue, Is.False); + Assert.That(fields["p13"].ListValue.Values[2].HasNullValue, Is.True); + + Assert.That(fields["p14"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p14"].ListValue.Values.Count, Is.EqualTo(2)); + Assert.That(fields["p14"].ListValue.Values[0].HasStringValue, Is.True); + Assert.That(fields["p14"].ListValue.Values[0].StringValue, Is.EqualTo(Convert.ToBase64String(new byte[]{1,2,3}))); + Assert.That(fields["p14"].ListValue.Values[1].HasNullValue, Is.True); + + Assert.That(fields["p15"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p15"].ListValue.Values.Count, Is.EqualTo(2)); + Assert.That(fields["p15"].ListValue.Values[0].HasStringValue, Is.True); + Assert.That(fields["p15"].ListValue.Values[0].StringValue, Is.EqualTo("2025-10-02")); + Assert.That(fields["p15"].ListValue.Values[1].HasNullValue, Is.True); + + Assert.That(fields["p16"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p16"].ListValue.Values.Count, Is.EqualTo(2)); + Assert.That(fields["p16"].ListValue.Values[0].HasStringValue, Is.True); + Assert.That(fields["p16"].ListValue.Values[0].StringValue, Is.EqualTo("P1DT2H3M4.005006S")); + Assert.That(fields["p16"].ListValue.Values[1].HasNullValue, Is.True); + + Assert.That(fields["p17"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p17"].ListValue.Values.Count, Is.EqualTo(2)); + Assert.That(fields["p17"].ListValue.Values[0].HasStringValue, Is.True); + Assert.That(fields["p17"].ListValue.Values[0].StringValue, Is.EqualTo("{\"key\": \"value\"}")); + Assert.That(fields["p17"].ListValue.Values[1].HasNullValue, Is.True); + + Assert.That(fields["p18"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p18"].ListValue.Values.Count, Is.EqualTo(2)); + Assert.That(fields["p18"].ListValue.Values[0].HasStringValue, Is.True); + Assert.That(fields["p18"].ListValue.Values[0].StringValue, Is.EqualTo("9.99")); + Assert.That(fields["p18"].ListValue.Values[1].HasNullValue, Is.True); + + Assert.That(fields["p19"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p19"].ListValue.Values.Count, Is.EqualTo(2)); + Assert.That(fields["p19"].ListValue.Values[0].HasStringValue, Is.True); + Assert.That(fields["p19"].ListValue.Values[0].StringValue, Is.EqualTo("test")); + Assert.That(fields["p19"].ListValue.Values[1].HasNullValue, Is.True); + + Assert.That(fields["p20"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p20"].ListValue.Values.Count, Is.EqualTo(2)); + Assert.That(fields["p20"].ListValue.Values[0].HasStringValue, Is.True); + Assert.That(fields["p20"].ListValue.Values[0].StringValue, Is.EqualTo("2025-10-02T15:57:31.9990000Z")); + Assert.That(fields["p20"].ListValue.Values[1].HasNullValue, Is.True); + + Assert.That(fields["p21"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p21"].ListValue.Values.Count, Is.EqualTo(2)); + Assert.That(fields["p21"].ListValue.Values[0].HasStringValue, Is.True); + Assert.That(fields["p21"].ListValue.Values[0].StringValue, Is.EqualTo("5555990c-b259-4539-bd22-5a9293cf10ac")); + Assert.That(fields["p21"].ListValue.Values[1].HasNullValue, Is.True); + + Assert.That(fields["p22"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p22"].ListValue.Values.Count, Is.EqualTo(2)); + Assert.That(fields["p22"].ListValue.Values[0].HasNumberValue, Is.True); + Assert.That(fields["p22"].ListValue.Values[0].NumberValue, Is.EqualTo(3.14d)); + Assert.That(fields["p22"].ListValue.Values[1].HasNullValue, Is.True); + + Assert.That(fields["p23"].KindCase, Is.EqualTo(Value.KindOneofCase.ListValue)); + Assert.That(fields["p23"].ListValue.Values.Count, Is.EqualTo(2)); + Assert.That(fields["p23"].ListValue.Values[0].HasNumberValue, Is.True); + Assert.That(fields["p23"].ListValue.Values[0].NumberValue, Is.EqualTo(3.14f)); + Assert.That(fields["p23"].ListValue.Values[1].HasNullValue, Is.True); + } + + private void AddParameter(DbCommand command, string name, object? value) + { + var parameter = command.CreateParameter(); + parameter.ParameterName = name; + parameter.Value = value; + command.Parameters.Add(parameter); + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionTests.cs new file mode 100644 index 00000000..b4a33abb --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionTests.cs @@ -0,0 +1,177 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Linq; +using Google.Cloud.Spanner.V1; +using Google.Cloud.SpannerLib; +using Google.Cloud.SpannerLib.MockServer; +using Grpc.Core; +using TypeCode = Google.Cloud.Spanner.V1.TypeCode; + +namespace Google.Cloud.Spanner.DataProvider.Tests; + +public class ConnectionTests : AbstractMockServerTests +{ + [Test] + public void TestOpenConnection() + { + var connection = new SpannerConnection { ConnectionString = ConnectionString }; + connection.Open(); + connection.Close(); + } + + [Test] + public void TestExecute() + { + var sql = "update all_types set col_float8=1 where col_bigint=1"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(1)); + using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + connection.Open(); + + var command = connection.CreateCommand(); + command.CommandText = sql; + var updateCount = command.ExecuteNonQuery(); + Assert.That(updateCount, Is.EqualTo(1)); + } + + [Test] + public void TestQuery() + { + var sql = "select col_varchar from all_types where col_varchar is not null limit 10"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.String}, "col_varchar", "value1", "value2", "value3")); + using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + connection.Open(); + + using var command = connection.CreateCommand(); + command.CommandText = sql; + var rowCount = 0; + using (var reader = command.ExecuteReader()) + { + while (reader.Read()) + { + rowCount++; + Assert.That(reader.GetString(0), Is.EqualTo($"value{rowCount}")); + } + } + Assert.That(rowCount, Is.EqualTo(3)); + } + + [Test] + public void TestParameterizedQuery() + { + var sql = "select * from all_types where col_varchar=@p1"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSelect1ResultSet()); + using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + connection.Open(); + + using var command = connection.CreateCommand(); + command.CommandText = sql; + command.Parameters.Add("2de7b24590e00a58fa7358c9531301c5"); + using (var reader = command.ExecuteReader()) + { + for (int i = 0; i < reader.FieldCount; i++) + { + Assert.That(reader.GetFieldType(i), Is.Not.Null); + } + while (reader.Read()) + { + for (int i = 0; i < reader.FieldCount; i++) + { + Assert.That(reader.GetValue(i), Is.Not.Null); + } + } + } + var requests = Fixture.SpannerMock.Requests.OfType().ToList(); + Assert.That(requests, Has.Count.EqualTo(1)); + var request = requests.First(); + Assert.That(request.Params.Fields, Has.Count.EqualTo(1)); + } + + [Test] + public void TestTransaction() + { + var sql = "select * from all_types where col_varchar=$1"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSelect1ResultSet()); + using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + connection.Open(); + + using var transaction = connection.BeginTransaction(); + using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.CommandText = sql; + command.Parameters.Add("2de7b24590e00a58fa7358c9531301c5"); + using (var reader = command.ExecuteReader()) + { + for (int i = 0; i < reader.FieldCount; i++) + { + Assert.That(reader.GetFieldType(i), Is.Not.Null); + } + while (reader.Read()) + { + for (int i = 0; i < reader.FieldCount; i++) + { + Assert.That(reader.GetValue(i), Is.Not.Null); + } + } + } + transaction.Commit(); + } + + [Test] + public void TestDisableInternalRetries() + { + var sql = "update my_table set value=@p1 where id=@p2 and version=1"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(1)); + using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString + ";retryAbortsInternally=false"; + connection.Open(); + + using var transaction = connection.BeginTransaction(); + using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.CommandText = sql; + command.Parameters.Add("2de7b24590e00a58fa7358c9531301c5"); + command.Parameters.Add(1L); + command.ExecuteNonQuery(); + Fixture.SpannerMock.AddOrUpdateExecutionTime(nameof(Fixture.SpannerMock.Commit), ExecutionTime.CreateException(StatusCode.Aborted, "Transaction was aborted")); + Assert.Throws(transaction.Commit); + + var requests = Fixture.SpannerMock.Requests.OfType().ToList(); + Assert.That(requests.Count, Is.EqualTo(1)); + } + + [Test] + public void TestBatchDml() + { + var sql1 = "update all_types set col_float8=1 where col_bigint=1"; + var sql2 = "update all_types set col_float8=2 where col_bigint=2"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql1, StatementResult.CreateUpdateCount(2)); + Fixture.SpannerMock.AddOrUpdateStatementResult(sql2, StatementResult.CreateUpdateCount(3)); + using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + connection.Open(); + + using var command1 = connection.CreateCommand(); + command1.CommandText = sql1; + using var command2 = connection.CreateCommand(); + command2.CommandText = sql2; + var affected = connection.ExecuteBatchDml([command1, command2]); + Assert.That(affected, Is.EqualTo(new long[] { 2, 3 })); + } + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/README.md b/drivers/spanner-ado-net/spanner-ado-net-tests/README.md new file mode 100644 index 00000000..8e93c1e4 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/README.md @@ -0,0 +1,5 @@ +# Spanner ADO.NET Data Provider Tests + +Tests for ADO.NET Data Provider for Spanner. + +__ALPHA: Not for production use__ diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/TransactionTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/TransactionTests.cs new file mode 100644 index 00000000..3a0353d1 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/TransactionTests.cs @@ -0,0 +1,182 @@ +// Copyright 2025 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 +// +// https://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. + +using Google.Cloud.Spanner.V1; +using Google.Cloud.SpannerLib.MockServer; + +namespace Google.Cloud.Spanner.DataProvider.Tests; + +public class TransactionTests : AbstractMockServerTests +{ + [Test] + public async Task TestReadWriteTransaction() + { + const string sql = "update my_table set my_column=@value where id=@id"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(1L)); + + await using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + await connection.OpenAsync(); + await using var transaction = await connection.BeginTransactionAsync(); + await using var command = connection.CreateCommand(); + command.CommandText = sql; + var paramId = command.CreateParameter(); + paramId.ParameterName = "id"; + paramId.Value = 1; + command.Parameters.Add(paramId); + var paramValue = command.CreateParameter(); + paramValue.ParameterName = "value"; + paramValue.Value = "One"; + command.Parameters.Add(paramValue); + var updateCount = await command.ExecuteNonQueryAsync(); + await transaction.CommitAsync(); + + Assert.That(updateCount, Is.EqualTo(1)); + var requests = Fixture.SpannerMock.Requests.ToList(); + // The transaction should use inline-begin. + Assert.That(requests.OfType().Count(), Is.EqualTo(0)); + Assert.That(requests.OfType().Count(), Is.EqualTo(1)); + Assert.That(requests.OfType().Count(), Is.EqualTo(1)); + var executeRequest = requests.OfType().First(); + Assert.That(executeRequest.Transaction, Is.EqualTo(new TransactionSelector + { + Begin = new TransactionOptions + { + ReadWrite = new TransactionOptions.Types.ReadWrite(), + } + })); + } + + [Test] + public async Task TestReadOnlyTransaction() + { + const string sql = "select value from my_table where id=@id"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = V1.TypeCode.String}, "value", "One")); + + await using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + await connection.OpenAsync(); + await using var transaction = connection.BeginReadOnlyTransaction(); + await using var command = connection.CreateCommand(); + command.CommandText = sql; + var paramId = command.CreateParameter(); + paramId.ParameterName = "id"; + paramId.Value = 1; + command.Parameters.Add(paramId); + await using var reader = await command.ExecuteReaderAsync(); + Assert.That(await reader.ReadAsync()); + Assert.That(reader.FieldCount, Is.EqualTo(1)); + Assert.That(reader.GetValue(0), Is.EqualTo("One")); + Assert.That(await reader.ReadAsync(), Is.False); + + // We must commit the transaction in order to end it. + await transaction.CommitAsync(); + + var requests = Fixture.SpannerMock.Requests.ToList(); + // The transaction should use inline-begin. + Assert.That(requests.OfType().Count(), Is.EqualTo(0)); + Assert.That(requests.OfType().Count(), Is.EqualTo(1)); + // Committing a read-only transaction is a no-op on Spanner. + Assert.That(requests.OfType().Count(), Is.EqualTo(0)); + var executeRequest = requests.OfType().First(); + Assert.That(executeRequest.Transaction, Is.EqualTo(new TransactionSelector + { + Begin = new TransactionOptions + { + ReadOnly = new TransactionOptions.Types.ReadOnly + { + Strong = true, + ReturnReadTimestamp = true, + }, + } + })); + } + + [Ignore("Needs a fix in SpannerLib")] + [Test] + public async Task TestTransactionTag() + { + const string select = "select value from my_table where id=@id"; + Fixture.SpannerMock.AddOrUpdateStatementResult(select, StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = V1.TypeCode.String}, "value", "one")); + const string update = "update my_table set my_column=@value where id=@id"; + Fixture.SpannerMock.AddOrUpdateStatementResult(update, StatementResult.CreateUpdateCount(1L)); + + await using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + await connection.OpenAsync(); + await using var setTagCommand = connection.CreateCommand(); + setTagCommand.CommandText = "set transaction_tag='test_tag'"; + await setTagCommand.ExecuteNonQueryAsync(); + await using var transaction = await connection.BeginTransactionAsync(); + + await using var command = connection.CreateCommand(); + command.CommandText = select; + var selectParamId = command.CreateParameter(); + selectParamId.ParameterName = "id"; + selectParamId.Value = 1; + command.Parameters.Add(selectParamId); + await using var reader = await command.ExecuteReaderAsync(); + Assert.That(await reader.ReadAsync()); + Assert.That(reader.FieldCount, Is.EqualTo(1)); + Assert.That(reader.GetValue(0), Is.EqualTo("one")); + Assert.That(await reader.ReadAsync(), Is.False); + + await using var updateCommand = connection.CreateCommand(); + updateCommand.CommandText = update; + var paramId = updateCommand.CreateParameter(); + paramId.ParameterName = "id"; + paramId.Value = 1; + updateCommand.Parameters.Add(paramId); + var paramValue = updateCommand.CreateParameter(); + paramValue.ParameterName = "value"; + paramValue.Value = "One"; + updateCommand.Parameters.Add(paramValue); + var updateCount = await updateCommand.ExecuteNonQueryAsync(); + await transaction.CommitAsync(); + + Assert.That(updateCount, Is.EqualTo(1)); + var requests = Fixture.SpannerMock.Requests.ToList(); + // The transaction should use inline-begin. + Assert.That(requests.OfType().Count(), Is.EqualTo(0)); + Assert.That(requests.OfType().Count(), Is.EqualTo(2)); + Assert.That(requests.OfType().Count(), Is.EqualTo(1)); + var selectRequest = requests.OfType().First(); + Assert.That(selectRequest.Transaction, Is.EqualTo(new TransactionSelector + { + Begin = new TransactionOptions + { + ReadWrite = new TransactionOptions.Types.ReadWrite(), + } + })); + Assert.That(selectRequest.RequestOptions.TransactionTag, Is.EqualTo("test_tag")); + var updateRequest = requests.OfType().Single(request => request.Sql == update); + Assert.That(updateRequest.RequestOptions.TransactionTag, Is.EqualTo("test_tag")); + var commitRequest = requests.OfType().Single(); + Assert.That(commitRequest.RequestOptions.TransactionTag, Is.EqualTo("test_tag")); + + // The next transaction should not use the tag. + await using var tx2 = await connection.BeginTransactionAsync(); + await using var command2 = connection.CreateCommand(); + command2.CommandText = update; + command2.Parameters.Add(paramId); + command2.Parameters.Add(paramValue); + await command2.ExecuteNonQueryAsync(); + await tx2.CommitAsync(); + + var lastRequest = requests.OfType().Last(request => request.Sql == update); + Assert.That(lastRequest.RequestOptions.TransactionTag, Is.Null); + var lastCommitRequest = requests.OfType().Last(); + Assert.That(lastCommitRequest.RequestOptions.TransactionTag, Is.Null); + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/appsettings.json b/drivers/spanner-ado-net/spanner-ado-net-tests/appsettings.json new file mode 100644 index 00000000..2a8537d3 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Microsoft": "Warning" + } + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/spanner-ado-net-tests.csproj b/drivers/spanner-ado-net/spanner-ado-net-tests/spanner-ado-net-tests.csproj new file mode 100644 index 00000000..051168cf --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/spanner-ado-net-tests.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + Google.Cloud.Spanner.DataProvider.Tests + enable + enable + + false + true + Google.Cloud.Spanner.DataProvider.Tests + default + + + + + + + + + + + + + + + + + + + + diff --git a/drivers/spanner-ado-net/spanner-ado-net.sln b/drivers/spanner-ado-net/spanner-ado-net.sln new file mode 100644 index 00000000..bfaca2ca --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spanner-ado-net", "spanner-ado-net\spanner-ado-net.csproj", "{C01E227F-E396-45E7-A82F-478EFA9AC0A6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spanner-ado-net-tests", "spanner-ado-net-tests\spanner-ado-net-tests.csproj", "{56052199-927F-46F5-8D0F-4826360E70B8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spanner-ado-net-specification-tests", "spanner-ado-net-specification-tests\spanner-ado-net-specification-tests.csproj", "{97D93DB7-CEB6-4C21-B4C2-A5A98D3FD59C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spanner-ado-net-samples", "spanner-ado-net-samples\spanner-ado-net-samples.csproj", "{537A257C-0228-418F-9DD5-A46324E591AE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spanner-ado-net-benchmarks", "spanner-ado-net-benchmarks\spanner-ado-net-benchmarks.csproj", "{2C70D969-A8AA-440B-81D8-532C327F237E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C01E227F-E396-45E7-A82F-478EFA9AC0A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C01E227F-E396-45E7-A82F-478EFA9AC0A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C01E227F-E396-45E7-A82F-478EFA9AC0A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C01E227F-E396-45E7-A82F-478EFA9AC0A6}.Release|Any CPU.Build.0 = Release|Any CPU + {56052199-927F-46F5-8D0F-4826360E70B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {56052199-927F-46F5-8D0F-4826360E70B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {56052199-927F-46F5-8D0F-4826360E70B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {56052199-927F-46F5-8D0F-4826360E70B8}.Release|Any CPU.Build.0 = Release|Any CPU + {97D93DB7-CEB6-4C21-B4C2-A5A98D3FD59C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97D93DB7-CEB6-4C21-B4C2-A5A98D3FD59C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97D93DB7-CEB6-4C21-B4C2-A5A98D3FD59C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97D93DB7-CEB6-4C21-B4C2-A5A98D3FD59C}.Release|Any CPU.Build.0 = Release|Any CPU + {537A257C-0228-418F-9DD5-A46324E591AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {537A257C-0228-418F-9DD5-A46324E591AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {537A257C-0228-418F-9DD5-A46324E591AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {537A257C-0228-418F-9DD5-A46324E591AE}.Release|Any CPU.Build.0 = Release|Any CPU + {2C70D969-A8AA-440B-81D8-532C327F237E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C70D969-A8AA-440B-81D8-532C327F237E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C70D969-A8AA-440B-81D8-532C327F237E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C70D969-A8AA-440B-81D8-532C327F237E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/drivers/spanner-ado-net/spanner-ado-net/AssemblyInfo.cs b/drivers/spanner-ado-net/spanner-ado-net/AssemblyInfo.cs new file mode 100644 index 00000000..9c773122 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; +[assembly:InternalsVisibleTo("Google.Cloud.Spanner.DataProvider.Tests")] +[assembly:InternalsVisibleTo("Google.Cloud.Spanner.DataProvider.SpecificationTests")] diff --git a/drivers/spanner-ado-net/spanner-ado-net/README.md b/drivers/spanner-ado-net/spanner-ado-net/README.md new file mode 100644 index 00000000..fbcfda13 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/README.md @@ -0,0 +1,5 @@ +# Spanner ADO.NET Data Provider + +ADO.NET Data Provider for Spanner. + +__ALPHA: Not for production use__ diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs new file mode 100644 index 00000000..30ca2cab --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs @@ -0,0 +1,129 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Google.Api.Gax; +using Google.Cloud.Spanner.V1; + +namespace Google.Cloud.Spanner.DataProvider; + +/// +/// SpannerBatch is the Spanner-specific implementation of DbBatch. SpannerBatch supports batches of DML or DDL +/// statements. Note that all statements in a batch must be of the same type. Batches of queries or DML statements with +/// a THEN RETURN / RETURNING clause are not supported. +/// +public class SpannerBatch : DbBatch +{ + private SpannerConnection SpannerConnection => (SpannerConnection)Connection!; + protected override SpannerBatchCommandCollection DbBatchCommands { get; } = new(); + public override int Timeout { get; set; } + protected override DbConnection? DbConnection { get; set; } + protected override DbTransaction? DbTransaction { get; set; } + + public SpannerBatch() + {} + + internal SpannerBatch(SpannerConnection connection) + { + Connection = GaxPreconditions.CheckNotNull(connection, nameof(connection)); + } + + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) + { + throw new System.NotImplementedException(); + } + + protected override Task ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + + private List CreateStatements() + { + var statements = new List(DbBatchCommands.Count); + foreach (var command in DbBatchCommands) + { + var spannerParams = ((SpannerParameterCollection)command.Parameters).CreateSpannerParams(); + var queryParams = spannerParams.Item1; + var paramTypes = spannerParams.Item2; + var batchStatement = new ExecuteBatchDmlRequest.Types.Statement + { + Sql = command.CommandText, + Params = queryParams, + }; + batchStatement.ParamTypes.Add(paramTypes); + statements.Add(batchStatement); + } + return statements; + } + + public override int ExecuteNonQuery() + { + if (DbBatchCommands.Count == 0) + { + return 0; + } + var statements = CreateStatements(); + var results = SpannerConnection.LibConnection!.ExecuteBatch(statements); + DbBatchCommands.SetAffected(results); + return (int) results.Sum(); + } + + public override async Task ExecuteNonQueryAsync(CancellationToken cancellationToken = default) + { + if (DbBatchCommands.Count == 0) + { + return 0; + } + var statements = CreateStatements(); + var results = await SpannerConnection.LibConnection!.ExecuteBatchAsync(statements); + DbBatchCommands.SetAffected(results); + return (int) results.Sum(); + } + + public override object? ExecuteScalar() + { + throw new System.NotImplementedException(); + } + + public override Task ExecuteScalarAsync(CancellationToken cancellationToken = default) + { + throw new System.NotImplementedException(); + } + + public override void Prepare() + { + throw new System.NotImplementedException(); + } + + public override Task PrepareAsync(CancellationToken cancellationToken = default) + { + throw new System.NotImplementedException(); + } + + public override void Cancel() + { + throw new System.NotImplementedException(); + } + + protected override DbBatchCommand CreateDbBatchCommand() + { + return new SpannerBatchCommand(); + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerBatchCommand.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatchCommand.cs new file mode 100644 index 00000000..4e93a7cd --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatchCommand.cs @@ -0,0 +1,34 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Data; +using System.Data.Common; + +namespace Google.Cloud.Spanner.DataProvider; + +public class SpannerBatchCommand : DbBatchCommand +{ + public override string CommandText { get; set; } = ""; + public override CommandType CommandType { get; set; } + + internal int InternalRecordsAffected; + public override int RecordsAffected => InternalRecordsAffected; + protected override DbParameterCollection DbParameterCollection { get; } = new SpannerParameterCollection(); + public override bool CanCreateParameter => true; + + public override DbParameter CreateParameter() + { + return new SpannerParameter(); + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerBatchCommandCollection.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatchCommandCollection.cs new file mode 100644 index 00000000..84f91f9e --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatchCommandCollection.cs @@ -0,0 +1,95 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Collections.Generic; +using System.Data.Common; +using Google.Api.Gax; + +namespace Google.Cloud.Spanner.DataProvider; + +public class SpannerBatchCommandCollection : DbBatchCommandCollection +{ + private readonly List _commands = new (); + public override int Count => _commands.Count; + public override bool IsReadOnly => false; + + internal void SetAffected(long[] affected) + { + for (var i = 0; i < _commands.Count; i++) + { + _commands[i].InternalRecordsAffected = (int) affected[i]; + } + } + + public override IEnumerator GetEnumerator() + { + return _commands.GetEnumerator(); + } + + public override void Add(DbBatchCommand item) + { + GaxPreconditions.CheckNotNull(item, nameof(item)); + GaxPreconditions.CheckArgument(item is SpannerBatchCommand, nameof(item), "Item must be a SpannerBatchCommand"); + _commands.Add((SpannerBatchCommand)item); + } + + public override void Clear() + { + _commands.Clear(); + } + + public override bool Contains(DbBatchCommand item) + { + GaxPreconditions.CheckArgument(item is SpannerBatchCommand, nameof(item), "Item must be a SpannerBatchCommand"); + return _commands.Contains((SpannerBatchCommand)item); + } + + public override void CopyTo(DbBatchCommand[] array, int arrayIndex) + { + throw new System.NotImplementedException(); + } + + public override bool Remove(DbBatchCommand item) + { + GaxPreconditions.CheckArgument(item is SpannerBatchCommand, nameof(item), "Item must be a SpannerBatchCommand"); + return _commands.Remove((SpannerBatchCommand)item); + } + + public override int IndexOf(DbBatchCommand item) + { + GaxPreconditions.CheckArgument(item is SpannerBatchCommand, nameof(item), "Item must be a SpannerBatchCommand"); + return _commands.IndexOf((SpannerBatchCommand)item); + } + + public override void Insert(int index, DbBatchCommand item) + { + GaxPreconditions.CheckArgument(item is SpannerBatchCommand, nameof(item), "Item must be a SpannerBatchCommand"); + _commands.Insert(index, (SpannerBatchCommand)item); + } + + public override void RemoveAt(int index) + { + _commands.RemoveAt(index); + } + + protected override SpannerBatchCommand GetBatchCommand(int index) + { + return _commands[index]; + } + + protected override void SetBatchCommand(int index, DbBatchCommand batchCommand) + { + _commands[index] = (SpannerBatchCommand)batchCommand; + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs new file mode 100644 index 00000000..912c11ac --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs @@ -0,0 +1,260 @@ +// Copyright 2025 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 +// +// https://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. + +using System; +using System.Data; +using System.Data.Common; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Google.Api.Gax; +using Google.Cloud.Spanner.V1; +using Google.Cloud.SpannerLib; +using Google.Protobuf.WellKnownTypes; +using static Google.Cloud.Spanner.DataProvider.SpannerDbException; + +namespace Google.Cloud.Spanner.DataProvider; + +public class SpannerCommand : DbCommand +{ + private SpannerConnection SpannerConnection => (SpannerConnection)Connection!; + + private string _commandText = ""; + [AllowNull] public override string CommandText { get => _commandText; set => _commandText = value ?? ""; } + + public override int CommandTimeout { get; set; } + public override CommandType CommandType { get; set; } = CommandType.Text; + public override UpdateRowSource UpdatedRowSource { get; set; } + protected override DbConnection? DbConnection { get; set; } + protected override DbParameterCollection DbParameterCollection { get; } = new SpannerParameterCollection(); + protected override DbTransaction? DbTransaction { get; set; } + public override bool DesignTimeVisible { get; set; } + + private bool HasTransaction => DbTransaction is SpannerTransaction; + private readonly Mutation? _mutation; + + public TransactionOptions.Types.ReadOnly? SingleUseReadOnlyTransactionOptions { get; set; } + public RequestOptions? RequestOptions { get; set; } + + public SpannerCommand() {} + + internal SpannerCommand(SpannerConnection connection) + { + Connection = GaxPreconditions.CheckNotNull(connection, nameof(connection)); + } + + internal SpannerCommand(SpannerConnection connection, Mutation mutation) + { + Connection = GaxPreconditions.CheckNotNull(connection, nameof(connection)); + _mutation = mutation; + } + + public override void Cancel() + { + // TODO: Implement in Spanner lib + } + + internal ExecuteSqlRequest BuildStatement(ExecuteSqlRequest.Types.QueryMode mode = ExecuteSqlRequest.Types.QueryMode.Normal) + { + GaxPreconditions.CheckState(!(HasTransaction && SingleUseReadOnlyTransactionOptions != null), + "Cannot set both a transaction and single-use read-only options"); + var spannerParams = ((SpannerParameterCollection)DbParameterCollection).CreateSpannerParams(); + var queryParams = spannerParams.Item1; + var paramTypes = spannerParams.Item2; + var statement = new ExecuteSqlRequest + { + Sql = CommandText, + Params = queryParams, + RequestOptions = RequestOptions, + QueryMode = mode, + }; + statement.ParamTypes.Add(paramTypes); + if (SingleUseReadOnlyTransactionOptions != null) + { + statement.Transaction = new TransactionSelector + { + SingleUse = new TransactionOptions + { + ReadOnly = SingleUseReadOnlyTransactionOptions, + }, + }; + } + + return statement; + } + + private Mutation BuildMutation() + { + GaxPreconditions.CheckNotNull(_mutation, nameof(_mutation)); + GaxPreconditions.CheckNotNull(SpannerConnection, nameof(SpannerConnection)); + GaxPreconditions.CheckState(!(HasTransaction && SingleUseReadOnlyTransactionOptions != null), + "Cannot set both a transaction and single-use read-only options"); + + var mutation = _mutation!.Clone(); + Mutation.Types.Write? write = null; + Mutation.Types.Delete? delete = mutation.OperationCase == Mutation.OperationOneofCase.Delete + ? mutation.Delete + : null; + switch (mutation.OperationCase) + { + case Mutation.OperationOneofCase.Insert: + write = mutation.Insert; + break; + case Mutation.OperationOneofCase.Update: + write = mutation.Update; + break; + case Mutation.OperationOneofCase.InsertOrUpdate: + write = mutation.InsertOrUpdate; + break; + case Mutation.OperationOneofCase.Replace: + write = mutation.Replace; + break; + } + + var values = new ListValue(); + for (var index = 0; index < DbParameterCollection.Count; index++) + { + var param = DbParameterCollection[index]; + if (param is SpannerParameter spannerParameter) + { + if (write != null) + { + var name = param.ParameterName; + if (name.StartsWith("@")) + { + name = name[1..]; + } + + write.Columns.Add(name); + } + + values.Values.Add(spannerParameter.ConvertToProto()); + } + else + { + throw new ArgumentException("parameter is not a SpannerParameter: " + param.ParameterName); + } + } + + write?.Values.Add(values); + if (delete != null) + { + delete.KeySet = new KeySet(); + delete.KeySet.Keys.Add(values); + } + + return mutation; + } + + private void ExecuteMutation() + { + GaxPreconditions.CheckState(_mutation != null, "Cannot execute mutation"); + var mutations = new BatchWriteRequest.Types.MutationGroup + { + Mutations = { BuildMutation() } + }; + SpannerConnection.LibConnection!.WriteMutations(mutations); + } + + private Rows Execute(ExecuteSqlRequest.Types.QueryMode mode = ExecuteSqlRequest.Types.QueryMode.Normal) + { + CheckCommandStateForExecution(); + return TranslateException(() => SpannerConnection.LibConnection!.Execute(BuildStatement(mode))); + } + + private Task ExecuteAsync(CancellationToken cancellationToken) + { + CheckCommandStateForExecution(); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return TranslateException(() => SpannerConnection.LibConnection!.ExecuteAsync(BuildStatement())); + } + + private void CheckCommandStateForExecution() + { + GaxPreconditions.CheckState(!string.IsNullOrEmpty(_commandText), "Cannot execute empty command"); + GaxPreconditions.CheckNotNull(SpannerConnection, nameof(SpannerConnection)); + GaxPreconditions.CheckState(Transaction == null || Transaction.Connection == SpannerConnection, + "The transaction that has been set for this command is from a different connection"); + } + + public override int ExecuteNonQuery() + { + if (_mutation != null) + { + ExecuteMutation(); + return 1; + } + + var rows = Execute(); + try + { + return (int)rows.UpdateCount; + } + finally + { + rows.Close(); + } + } + + public override object? ExecuteScalar() + { + GaxPreconditions.CheckState(_mutation == null, "Cannot execute mutations with ExecuteScalar()"); + var rows = Execute(); + using var reader = new SpannerDataReader(SpannerConnection, rows, CommandBehavior.Default); + if (reader.Read()) + { + if (reader.FieldCount > 0) + { + return reader.GetValue(0); + } + } + + return null; + } + + public override void Prepare() + { + Execute(ExecuteSqlRequest.Types.QueryMode.Plan); + } + + protected override DbParameter CreateDbParameter() + { + return new SpannerParameter(); + } + + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) + { + GaxPreconditions.CheckState(_mutation == null, "Cannot execute mutations with ExecuteDbDataReader()"); + var rows = Execute(); + return new SpannerDataReader(SpannerConnection, rows, behavior); + } + + protected override async Task ExecuteDbDataReaderAsync(CommandBehavior behavior, + CancellationToken cancellationToken) + { + GaxPreconditions.CheckState(_mutation == null, "Cannot execute mutations with ExecuteDbDataReader()"); + try + { + var rows = await ExecuteAsync(cancellationToken); + return new SpannerDataReader(SpannerConnection, rows, behavior); + } + catch (SpannerException exception) + { + throw new SpannerDbException(exception); + } + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommandBuilder.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommandBuilder.cs new file mode 100644 index 00000000..c1727c0d --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommandBuilder.cs @@ -0,0 +1,46 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Data; +using System.Data.Common; + +namespace Google.Cloud.Spanner.DataProvider; + +public class SpannerCommandBuilder : DbCommandBuilder +{ + protected override void ApplyParameterInfo(DbParameter parameter, DataRow row, StatementType statementType, bool whereClause) + { + throw new System.NotImplementedException(); + } + + protected override string GetParameterName(int parameterOrdinal) + { + throw new System.NotImplementedException(); + } + + protected override string GetParameterName(string parameterName) + { + throw new System.NotImplementedException(); + } + + protected override string GetParameterPlaceholder(int parameterOrdinal) + { + throw new System.NotImplementedException(); + } + + protected override void SetRowUpdatingHandler(DbDataAdapter adapter) + { + throw new System.NotImplementedException(); + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs new file mode 100644 index 00000000..4b128772 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs @@ -0,0 +1,290 @@ +// Copyright 2025 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 +// +// https://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. + +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Google.Cloud.Spanner.Common.V1; +using Google.Cloud.Spanner.V1; +using Google.Cloud.SpannerLib; + +namespace Google.Cloud.Spanner.DataProvider; + +public class SpannerConnection : DbConnection +{ + public bool UseSharedLibrary { get; set; } + + private string _connectionString = string.Empty; + + [AllowNull] + public override string ConnectionString { + get => _connectionString; + set + { + AssertClosed(); + if (!IsValidConnectionString(value)) + { + throw new ArgumentException($"Invalid connection string: {value}"); + } + _connectionString = value ?? string.Empty; + } + } + + public override string Database + { + get + { + // TODO: Move this to SpannerLib. + if (String.IsNullOrWhiteSpace(ConnectionString)) + { + return ""; + } + var startIndex = ConnectionString.IndexOf("projects/", StringComparison.Ordinal); + if (startIndex == -1) + { + throw new ArgumentException($"Invalid database name in connection string: {ConnectionString}"); + } + + var endIndex = ConnectionString.IndexOf('?'); + if (endIndex == -1) + { + endIndex = ConnectionString.IndexOf(';'); + } + if (endIndex == -1) + { + endIndex = ConnectionString.Length; + } + var name = ConnectionString.Substring(startIndex, endIndex); + if (DatabaseName.TryParse(name, false, out var result)) + { + return result.DatabaseId; + } + throw new ArgumentException($"Invalid database name in connection string: {ConnectionString}"); + } + } + + private ConnectionState InternalState + { + get => _state; + set + { + var originalState = _state; + _state = value; + OnStateChange(new StateChangeEventArgs(originalState, _state)); + } + } + + public override ConnectionState State => InternalState; + protected override DbProviderFactory DbProviderFactory => SpannerFactory.Instance; + + public override string DataSource { get; } = ""; + public override string ServerVersion + { + get + { + AssertOpen(); + return Assembly.GetAssembly(typeof(Connection))?.GetName().Version?.ToString() ?? ""; + } + } + + public override bool CanCreateBatch => true; + + private bool _disposed; + private ConnectionState _state = ConnectionState.Closed; + private SpannerPool? Pool { get; set; } + + private Connection? _libConnection; + + internal Connection? LibConnection + { + get + { + AssertOpen(); + return _libConnection; + } + } + + private SpannerTransaction? _transaction; + + protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) + { + return BeginTransaction(new TransactionOptions + { + IsolationLevel = SpannerTransaction.TranslateIsolationLevel(isolationLevel), + }); + } + + public DbTransaction BeginReadOnlyTransaction() + { + return BeginTransaction(new TransactionOptions + { + ReadOnly = new TransactionOptions.Types.ReadOnly(), + }); + } + + public DbTransaction BeginTransaction(TransactionOptions transactionOptions) + { + EnsureOpen(); + if (_transaction != null) + { + throw new InvalidOperationException("This connection has a transaction."); + } + _transaction = new SpannerTransaction(this, transactionOptions); + return _transaction; + } + + internal void ClearTransaction() + { + _transaction = null; + } + + public override void ChangeDatabase(string databaseName) + { + throw new NotImplementedException(); + } + + public override void Close() + { + if (InternalState == ConnectionState.Closed) + { + return; + } + + InternalState = ConnectionState.Closed; + _libConnection?.Close(); + _libConnection = null; + } + + protected override void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + if (disposing) + { + Close(); + } + base.Dispose(disposing); + _disposed = true; + } + + public override void Open() + { + AssertClosed(); + if (ConnectionString == string.Empty) + { + throw new InvalidOperationException("Connection string is empty"); + } + + InternalState = ConnectionState.Connecting; + Pool = SpannerPool.GetOrCreate(ConnectionString); + _libConnection = Pool.CreateConnection(); + InternalState = ConnectionState.Open; + } + + private void EnsureOpen() + { + if (InternalState == ConnectionState.Closed) + { + Open(); + } + } + + private void AssertOpen() + { + if (InternalState != ConnectionState.Open) + { + throw new InvalidOperationException("Connection is not open"); + } + } + + private void AssertClosed() + { + if (InternalState != ConnectionState.Closed) + { + throw new InvalidOperationException("Connection is not closed"); + } + } + + private bool IsValidConnectionString(string? connectionString) + { + // TODO: Move to Spanner lib. + return string.IsNullOrEmpty(connectionString) || connectionString.Contains("projects/"); + } + + public CommitResponse? WriteMutations(BatchWriteRequest.Types.MutationGroup mutations) + { + EnsureOpen(); + return LibConnection!.WriteMutations(mutations); + } + + public Task WriteMutationsAsync(BatchWriteRequest.Types.MutationGroup mutations, CancellationToken cancellationToken = default) + { + EnsureOpen(); + return LibConnection!.WriteMutationsAsync(mutations, cancellationToken); + } + + protected override DbCommand CreateDbCommand() + { + return new SpannerCommand(this); + } + + protected override DbBatch CreateDbBatch() + { + return new SpannerBatch(this); + } + + public long[] ExecuteBatchDml(List commands) + { + EnsureOpen(); + var statements = new List(commands.Count); + foreach (var command in commands) + { + if (command is SpannerCommand spannerCommand) + { + var statement = spannerCommand.BuildStatement(); + var batchStatement = new ExecuteBatchDmlRequest.Types.Statement + { + Sql = statement.Sql, + Params = statement.Params, + }; + batchStatement.ParamTypes.Add(statement.ParamTypes); + statements.Add(batchStatement); + } + } + return LibConnection!.ExecuteBatch(statements); + } + + public DbCommand CreateInsertCommand(string table) + { + return new SpannerCommand(this, new Mutation { Insert = new Mutation.Types.Write { Table = table } }); + } + + public DbCommand CreateUpdateCommand(string table) + { + return new SpannerCommand(this, new Mutation { Update = new Mutation.Types.Write { Table = table } }); + } + + public DbCommand CreateDeleteCommand(string table) + { + return new SpannerCommand(this, new Mutation { Delete = new Mutation.Types.Delete { Table = table } }); + } + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnectionStringBuilder.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnectionStringBuilder.cs new file mode 100644 index 00000000..acd93cce --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnectionStringBuilder.cs @@ -0,0 +1,22 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Data.Common; + +namespace Google.Cloud.Spanner.DataProvider; + +public class SpannerConnectionStringBuilder : DbConnectionStringBuilder +{ + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataAdapter.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataAdapter.cs new file mode 100644 index 00000000..74dfceab --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataAdapter.cs @@ -0,0 +1,22 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Data.Common; + +namespace Google.Cloud.Spanner.DataProvider; + +public class SpannerDataAdapter : DbDataAdapter +{ + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs new file mode 100644 index 00000000..465ebc8d --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs @@ -0,0 +1,825 @@ +// Copyright 2025 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 +// +// https://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. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Globalization; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using Google.Api.Gax; +using Google.Cloud.Spanner.V1; +using Google.Cloud.SpannerLib; +using Google.Protobuf.WellKnownTypes; +using TypeCode = Google.Cloud.Spanner.V1.TypeCode; + +namespace Google.Cloud.Spanner.DataProvider; + +public class SpannerDataReader : DbDataReader +{ + private readonly SpannerConnection _connection; + private readonly CommandBehavior _commandBehavior; + private bool IsSingleRow => _commandBehavior.HasFlag(CommandBehavior.SingleRow); + private Rows LibRows { get; } + private bool _closed; + private bool _hasReadData; + private bool _hasData; + + public override int FieldCount + { + get + { + CheckNotClosed(); + return LibRows.Metadata?.RowType.Fields.Count ?? 0; + } + } + + public override object this[int ordinal] => GetFieldValue(ordinal); + public override object this[string name] => this[GetOrdinal(name)]; + + public override int RecordsAffected + { + get + { + CheckNotClosed(); + return (int)LibRows.UpdateCount; + } + } + + public override bool HasRows + { + get + { + CheckNotClosed(); + if (LibRows.Metadata?.RowType.Fields.Count == 0) + { + return false; + } + if (_hasReadData) + { + return _hasData; + } + return CheckForRows(); + } + } + public override bool IsClosed => _closed; + public override int Depth => 0; + + private ListValue? _currentRow; + private ListValue? _tempRow; + + internal SpannerDataReader(SpannerConnection connection, Rows libRows, CommandBehavior behavior) + { + _connection = connection; + LibRows = libRows; + _commandBehavior = behavior; + } + + private void CheckNotClosed() + { + GaxPreconditions.CheckState(!_closed, "Reader has been closed"); + } + + public override void Close() + { + if (_closed) + { + return; + } + + _closed = true; + LibRows.Close(); + if (_commandBehavior.HasFlag(CommandBehavior.CloseConnection)) + { + _connection.Close(); + } + } + + public override bool Read() + { + if (!InternalRead()) + { + _hasReadData = true; + _currentRow = LibRows.Next(); + } + return _currentRow != null; + } + + public override async Task ReadAsync(CancellationToken cancellationToken) + { + if (!InternalRead()) + { + _hasReadData = true; + _currentRow = await LibRows.NextAsync(); + } + return _currentRow != null; + } + + private bool InternalRead() + { + CheckNotClosed(); + if (_tempRow != null) + { + _currentRow = _tempRow; + _tempRow = null; + _hasReadData = true; + return true; + } + if (IsSingleRow && _hasReadData) + { + _currentRow = null; + return true; + } + return false; + } + + private bool CheckForRows() + { + _tempRow = LibRows.Next(); + return _tempRow != null; + } + + public override DataTable? GetSchemaTable() + { + CheckNotClosed(); + var metadata = LibRows.Metadata; + if (metadata?.RowType == null || metadata.RowType.Fields.Count == 0) + { + return null; + } + var table = new DataTable("SchemaTable"); + + table.Columns.Add("ColumnName", typeof(string)); + table.Columns.Add("ColumnOrdinal", typeof(int)); + table.Columns.Add("ColumnSize", typeof(int)); + table.Columns.Add("NumericPrecision", typeof(int)); + table.Columns.Add("NumericScale", typeof(int)); + table.Columns.Add("IsUnique", typeof(bool)); + table.Columns.Add("IsKey", typeof(bool)); + table.Columns.Add("BaseServerName", typeof(string)); + table.Columns.Add("BaseCatalogName", typeof(string)); + table.Columns.Add("BaseColumnName", typeof(string)); + table.Columns.Add("BaseSchemaName", typeof(string)); + table.Columns.Add("BaseTableName", typeof(string)); + table.Columns.Add("DataType", typeof(System.Type)); + table.Columns.Add("AllowDBNull", typeof(bool)); + table.Columns.Add("ProviderType", typeof(int)); + table.Columns.Add("IsAliased", typeof(bool)); + table.Columns.Add("IsExpression", typeof(bool)); + table.Columns.Add("IsIdentity", typeof(bool)); + table.Columns.Add("IsAutoIncrement", typeof(bool)); + table.Columns.Add("IsRowVersion", typeof(bool)); + table.Columns.Add("IsHidden", typeof(bool)); + table.Columns.Add("IsLong", typeof(bool)); + table.Columns.Add("IsReadOnly", typeof(bool)); + table.Columns.Add("ProviderSpecificDataType", typeof(System.Type)); + table.Columns.Add("DataTypeName", typeof(string)); + + var ordinal = 0; + foreach (var column in metadata.RowType.Fields) + { + ordinal++; + var row = table.NewRow(); + row["ColumnName"] = column.Name; + row["ColumnOrdinal"] = ordinal; + row["ColumnSize"] = -1; + row["NumericPrecision"] = 0; + row["NumericScale"] = 0; + row["IsUnique"] = false; + row["IsKey"] = false; + row["BaseServerName"] = ""; + row["BaseCatalogName"] = ""; + row["BaseColumnName"] = ""; + row["BaseSchemaName"] = ""; + row["BaseTableName"] = ""; + row["DataType"] = TypeConversion.GetSystemType(column.Type); + row["AllowDBNull"] = true; + row["ProviderType"] = (int)column.Type.Code; + row["IsAliased"] = false; + row["IsExpression"] = false; + row["IsIdentity"] = false; + row["IsAutoIncrement"] = false; + row["IsRowVersion"] = false; + row["IsHidden"] = false; + row["IsLong"] = false; + row["IsReadOnly"] = false; + row["DataTypeName"] = column.Type.Code.ToString(); + + table.Rows.Add(row); + } + return table; + } + + public override string GetString(int ordinal) + { + var value = GetProtoValue(ordinal); + CheckNotNull(ordinal); + if (value.HasStringValue) + { + return value.StringValue; + } + if (value.HasNumberValue) + { + var type = GetSpannerType(ordinal); + if (type.Code == TypeCode.Float32) + { + return ((float) value.NumberValue).ToString(CultureInfo.InvariantCulture); + } + return value.NumberValue.ToString(CultureInfo.InvariantCulture); + } + if (value.HasBoolValue) + { + return value.BoolValue.ToString(); + } + throw new InvalidCastException("not a valid string value"); + } + + public override bool GetBoolean(int ordinal) + { + var value = GetProtoValue(ordinal); + CheckNotNull(ordinal); + if (value.HasStringValue) + { + try + { + return bool.Parse(value.StringValue); + } + catch (Exception exception) + { + throw new InvalidCastException(exception.Message, exception); + } + } + if (value.HasBoolValue) + { + return value.BoolValue; + } + throw new InvalidCastException("not a valid bool value"); + } + + public override byte GetByte(int ordinal) + { + CheckValidPosition(); + CheckNotNull(ordinal); + throw new InvalidCastException("not a valid byte value"); + } + + public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length) + { + CheckValidPosition(); + CheckValidOrdinal(ordinal); + GaxPreconditions.CheckState(LibRows.Metadata!.RowType.Fields[ordinal].Type.Code == TypeCode.Bytes, + "Spanner only supports conversion to byte arrays for columns of type BYTES."); + GaxPreconditions.CheckArgumentRange(bufferOffset, nameof(bufferOffset), 0, buffer?.Length ?? 0); + GaxPreconditions.CheckArgumentRange(length, nameof(length), 0, buffer?.Length ?? int.MaxValue); + if (buffer != null) + { + GaxPreconditions.CheckArgumentRange(bufferOffset + length, nameof(length), 0, buffer.Length); + } + + var bytes = IsDBNull(ordinal) ? null : GetFieldValue(ordinal); + if (buffer == null) + { + // Return the length of the value if `buffer` is null: + // https://docs.microsoft.com/en-us/dotnet/api/system.data.idatarecord.getbytes?view=netstandard-2.1#remarks + return bytes?.Length ?? 0; + } + + var copyLength = Math.Min(length, (bytes?.Length ?? 0) - (int)dataOffset); + if (copyLength < 0) + { + // Read nothing and just return. + return 0; + } + + if (bytes != null) + { + Array.Copy(bytes, (int)dataOffset, buffer, bufferOffset, copyLength); + } + + return copyLength; + } + + public override char GetChar(int ordinal) + { + var value = GetProtoValue(ordinal); + CheckNotNull(ordinal); + var type = GetSpannerType(ordinal); + if (type.Code != TypeCode.String) + { + throw new InvalidCastException("not a valid char value"); + } + if (value.HasStringValue) + { + if (value.StringValue.Length == 1) + { + return value.StringValue[0]; + } + } + throw new InvalidCastException("not a valid char value"); + } + + public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length) + { + var value = GetProtoValue(ordinal); + if (value.HasNullValue) + { + return 0; + } + if (!value.HasStringValue) + { + throw new DataException("not a valid type for getting as chars"); + } + if (buffer == null) + { + // Return the length of the value if `buffer` is null: + // https://docs.microsoft.com/en-us/dotnet/api/system.data.idatarecord.getbytes?view=netstandard-2.1#remarks + return value.StringValue.ToCharArray().Length; + } + GaxPreconditions.CheckArgumentRange(bufferOffset, nameof(bufferOffset), 0, buffer.Length); + GaxPreconditions.CheckArgumentRange(length, nameof(length), 0, buffer.Length - bufferOffset); + + var intDataOffset = (int)dataOffset; + var sourceLength = Math.Min(length, value.StringValue.Length - intDataOffset); + var destLength = Math.Min(length, buffer.Length - bufferOffset); + destLength = Math.Min(destLength, sourceLength); + + if (destLength <= 0) + { + return 0; + } + if (bufferOffset + destLength > buffer.Length) + { + return 0; + } + + // TODO: Optimize + var chars = value.StringValue.ToCharArray(intDataOffset, sourceLength); + if (intDataOffset >= chars.Length) + { + return 0; + } + + Array.Copy(chars, 0, buffer, bufferOffset, destLength); + + return destLength; + } + + public override string GetDataTypeName(int ordinal) + { + CheckValidOrdinal(ordinal); + return GetTypeName(LibRows.Metadata!.RowType.Fields[ordinal].Type); + } + + private static string GetTypeName(Google.Cloud.Spanner.V1.Type type) + { + if (type.Code == TypeCode.Array) + { + return type.Code.GetOriginalName() + "<" + type.ArrayElementType.Code.GetOriginalName() + ">"; + } + return type.Code.GetOriginalName(); + } + + public override DateTime GetDateTime(int ordinal) + { + var value = GetProtoValue(ordinal); + CheckNotNull(ordinal); + var type = GetSpannerType(ordinal); + if (type.Code == TypeCode.Date) + { + var date = DateOnly.Parse(value.StringValue); + return date.ToDateTime(TimeOnly.MinValue); + } + if (value.HasStringValue) + { + try + { + return XmlConvert.ToDateTime(value.StringValue, XmlDateTimeSerializationMode.Utc); + } + catch (Exception exception) + { + throw new InvalidCastException(exception.Message, exception); + } + } + throw new InvalidCastException("not a valid DateTime value"); + } + + public override decimal GetDecimal(int ordinal) + { + var value = GetProtoValue(ordinal); + CheckNotNull(ordinal); + if (value.HasStringValue) + { + try + { + return decimal.Parse(value.StringValue, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign | NumberStyles.AllowExponent, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidCastException(exception.Message, exception); + } + } + throw new InvalidCastException("not a valid decimal value"); + } + + public override double GetDouble(int ordinal) + { + var value = GetProtoValue(ordinal); + CheckNotNull(ordinal); + if (value.HasStringValue) + { + try + { + return double.Parse(value.StringValue, + NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign | NumberStyles.AllowExponent, + CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidCastException(exception.Message, exception); + } + } + if (value.HasNumberValue) + { + return value.NumberValue; + } + throw new InvalidCastException("not a valid double value"); + } + + public override System.Type GetFieldType(int ordinal) + { + CheckValidOrdinal(ordinal); + return GetClrType(LibRows.Metadata!.RowType.Fields[ordinal].Type); + } + + private static System.Type GetClrType(Google.Cloud.Spanner.V1.Type type) + { + return type.Code switch + { + TypeCode.Array => typeof(List<>).MakeGenericType(GetClrType(type.ArrayElementType)), + TypeCode.Bool => typeof(bool), + TypeCode.Bytes => typeof(byte[]), + TypeCode.Date => typeof(DateOnly), + TypeCode.Enum => typeof(int), + TypeCode.Float32 => typeof(float), + TypeCode.Float64 => typeof(double), + TypeCode.Int64 => typeof(long), + TypeCode.Interval => typeof(TimeSpan), + TypeCode.Json => typeof(string), + TypeCode.Numeric => typeof(decimal), + TypeCode.Proto => typeof(byte[]), + TypeCode.String => typeof(string), + TypeCode.Timestamp => typeof(DateTime), + TypeCode.Uuid => typeof(Guid), + _ => typeof(Value) + }; + } + + public override float GetFloat(int ordinal) + { + var value = GetProtoValue(ordinal); + CheckNotNull(ordinal); + if (value.HasStringValue) + { + try + { + return float.Parse(value.StringValue, + NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign | NumberStyles.AllowExponent, + CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidCastException(exception.Message, exception); + } + } + var type = GetSpannerType(ordinal); + if (type.Code == TypeCode.Float32) + { + return (float)value.NumberValue; + } + throw new InvalidCastException("not a valid float value"); + } + + public override Guid GetGuid(int ordinal) + { + var value = GetProtoValue(ordinal); + CheckNotNull(ordinal); + if (value.HasStringValue) + { + try + { + return Guid.Parse(value.StringValue); + } + catch (Exception exception) + { + throw new InvalidCastException(exception.Message, exception); + } + } + throw new InvalidCastException("not a valid Guid value"); + } + + public override short GetInt16(int ordinal) + { + var value = GetProtoValue(ordinal); + CheckNotNull(ordinal); + if (value.HasStringValue) + { + try + { + return short.Parse(value.StringValue, + NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign | NumberStyles.AllowExponent, + CultureInfo.InvariantCulture); + } + catch (OverflowException) + { + throw; + } + catch (Exception exception) + { + throw new InvalidCastException(exception.Message, exception); + } + } + if (value.HasNumberValue) + { + return (short)value.NumberValue; + } + throw new InvalidCastException("not a valid Int16 value"); + } + + public override int GetInt32(int ordinal) + { + var value = GetProtoValue(ordinal); + CheckNotNull(ordinal); + if (value.HasStringValue) + { + try + { + return int.Parse(value.StringValue, + NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign | NumberStyles.AllowExponent, + CultureInfo.InvariantCulture); + } + catch (OverflowException) + { + throw; + } + catch (Exception exception) + { + throw new InvalidCastException(exception.Message, exception); + } + } + if (value.HasNumberValue) + { + return (int)value.NumberValue; + } + throw new InvalidCastException("not a valid Int32 value"); + } + + public override long GetInt64(int ordinal) + { + var value = GetProtoValue(ordinal); + CheckNotNull(ordinal); + if (value.HasStringValue) + { + try + { + return long.Parse(value.StringValue, + NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign | NumberStyles.AllowExponent, + CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidCastException(exception.Message, exception); + } + } + if (value.HasNumberValue) + { + return (long)value.NumberValue; + } + throw new InvalidCastException("not a valid Int64 value"); + } + + public override string GetName(int ordinal) + { + CheckValidOrdinal(ordinal); + return LibRows.Metadata!.RowType.Fields[ordinal].Name; + } + + public override int GetOrdinal(string name) + { + CheckNotClosed(); + for (var i = 0; i < LibRows.Metadata?.RowType.Fields.Count; i++) + { + if (Equals(LibRows.Metadata?.RowType.Fields[i].Name, name)) + { + return i; + } + } + throw new IndexOutOfRangeException($"No column with name {name} found"); + } + + public override T GetFieldValue(int ordinal) + { + CheckNotClosed(); + CheckValidOrdinal(ordinal); + if (typeof(T) == typeof(Stream)) + { + CheckNotNull(ordinal); + return (T)(object)GetStream(ordinal); + } + if (typeof(T) == typeof(TextReader)) + { + CheckNotNull(ordinal); + return (T)(object)GetTextReader(ordinal); + } + if (typeof(T) == typeof(char)) + { + return (T)(object)GetChar(ordinal); + } + if (typeof(T) == typeof(DateTime)) + { + return (T)(object)GetDateTime(ordinal); + } + if (typeof(T) == typeof(double)) + { + return (T)(object)GetDouble(ordinal); + } + if (typeof(T) == typeof(float)) + { + return (T)(object)GetFloat(ordinal); + } + if (typeof(T) == typeof(Int16)) + { + return (T)(object)GetInt16(ordinal); + } + if (typeof(T) == typeof(int)) + { + return (T)(object)GetInt32(ordinal); + } + if (typeof(T) == typeof(long)) + { + return (T)(object)GetInt64(ordinal); + } + + return base.GetFieldValue(ordinal); + } + + public override object GetValue(int ordinal) + { + CheckValidOrdinal(ordinal); + CheckValidPosition(); + var type = LibRows.Metadata!.RowType.Fields[ordinal].Type; + var value = _currentRow!.Values[ordinal]; + return GetUnderlyingValue(type, value); + } + + private static object GetUnderlyingValue(Google.Cloud.Spanner.V1.Type type, Value value) + { + if (value.HasNullValue) + { + return DBNull.Value; + } + + switch (type.Code) + { + case TypeCode.Array: + var listType = typeof(List<>).MakeGenericType(GetClrType(type.ArrayElementType)); + var list = (IList)Activator.CreateInstance(listType); + foreach (var element in value.ListValue.Values) + { + list.Add(GetUnderlyingValue(type.ArrayElementType, element)); + } + return list; + case TypeCode.Bool: + return value.BoolValue; + case TypeCode.Bytes: + return Convert.FromBase64String(value.StringValue); + case TypeCode.Date: + return DateOnly.Parse(value.StringValue); + case TypeCode.Enum: + return long.Parse(value.StringValue); + case TypeCode.Float32: + return (float)value.NumberValue; + case TypeCode.Float64: + return value.NumberValue; + case TypeCode.Int64: + return long.Parse(value.StringValue); + case TypeCode.Interval: + return TimeSpan.Parse(value.StringValue); + case TypeCode.Json: + return value.StringValue; + case TypeCode.Numeric: + return decimal.Parse(value.StringValue, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture); + case TypeCode.Proto: + return Convert.FromBase64String(value.StringValue); + case TypeCode.String: + return value.StringValue; + case TypeCode.Timestamp: + return XmlConvert.ToDateTime(value.StringValue, XmlDateTimeSerializationMode.Utc); + case TypeCode.Uuid: + return Guid.Parse(value.StringValue); + } + if (value.HasBoolValue) + { + return value.BoolValue; + } + if (value.HasStringValue) + { + return value.StringValue; + } + if (value.HasNumberValue) + { + return value.NumberValue; + } + return value; + } + + private Value GetProtoValue(int ordinal) + { + CheckValidOrdinal(ordinal); + CheckValidPosition(); + return _currentRow!.Values[ordinal]; + } + + private V1.Type GetSpannerType(int ordinal) + { + CheckValidOrdinal(ordinal); + return LibRows.Metadata?.RowType.Fields[ordinal].Type ?? throw new DataException("metadata not found"); + } + + public override int GetValues(object[] values) + { + CheckValidPosition(); + GaxPreconditions.CheckNotNull(values, nameof(values)); + + var count = Math.Min(FieldCount, values.Length); + for (var i = 0; i < count; i++) + { + values[i] = this[i]; + } + + return count; + } + + public override bool IsDBNull(int ordinal) + { + var value = GetProtoValue(ordinal); + return value.HasNullValue; + } + + public override bool NextResult() + { + CheckNotClosed(); + return false; + } + + public override IEnumerator GetEnumerator() + { + CheckNotClosed(); + return new DbEnumerator(this); + } + + private void CheckValidPosition() + { + CheckNotClosed(); + if (_currentRow == null) + { + throw new InvalidOperationException("DataReader is before the first row or after the last row"); + } + } + + private void CheckValidOrdinal(int ordinal) + { + CheckNotClosed(); + var metadata = LibRows.Metadata; + GaxPreconditions.CheckState(metadata != null && metadata.RowType.Fields.Count > 0, "This reader does not contain any rows"); + + // Check that the ordinal is within the range of the columns in the query. + if (ordinal < 0 || ordinal >= metadata!.RowType.Fields.Count) + { + throw new IndexOutOfRangeException("ordinal is out of range"); + } + } + + private void CheckNotNull(int ordinal) + { + if (_currentRow?.Values[ordinal]?.HasNullValue ?? false) + { + throw new InvalidCastException("Value is null"); + } + } + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataSource.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataSource.cs new file mode 100644 index 00000000..162b7d0c --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataSource.cs @@ -0,0 +1,33 @@ +// Copyright 2025 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 +// +// https://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. + +using System; +using System.Data.Common; + +namespace Google.Cloud.Spanner.DataProvider; + +public class SpannerDataSource : DbDataSource +{ + public override string ConnectionString { get; } + + public static SpannerDataSource Create(string connectionString) + { + throw new NotImplementedException(); + } + + protected override DbConnection CreateDbConnection() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerDbException.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerDbException.cs new file mode 100644 index 00000000..eb03b77d --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerDbException.cs @@ -0,0 +1,42 @@ +// Copyright 2025 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 +// +// https://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. + +using System; +using System.Data.Common; +using Google.Cloud.SpannerLib; + +namespace Google.Cloud.Spanner.DataProvider; + +public class SpannerDbException : DbException +{ + internal static T TranslateException(Func func) + { + try + { + return func(); + } + catch (SpannerException exception) + { + throw new SpannerDbException(exception); + } + } + + private SpannerException SpannerException { get; } + + internal SpannerDbException(SpannerException spannerException) : base(spannerException.Message, spannerException.Status.Code) + { + SpannerException = spannerException; + } + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerFactory.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerFactory.cs new file mode 100644 index 00000000..6bfe1967 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerFactory.cs @@ -0,0 +1,94 @@ +// Copyright 2025 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 +// +// https://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. + +using System; +using System.Data.Common; + +namespace Google.Cloud.Spanner.DataProvider; + +public class SpannerFactory : DbProviderFactory, IServiceProvider +{ + /// + /// Gets an instance of the . + /// This can be used to retrieve strongly typed data objects. + /// + public static readonly SpannerFactory Instance = new(); + + SpannerFactory() {} + + /// + /// Returns a strongly typed instance. + /// + public override DbCommand CreateCommand() => new SpannerCommand(); + + /// + /// Returns a strongly typed instance. + /// + public override DbConnection CreateConnection() => new SpannerConnection(); + + /// + /// Returns a strongly typed instance. + /// + public override DbParameter CreateParameter() => new SpannerParameter(); + + /// + /// Returns a strongly typed instance. + /// + public override DbConnectionStringBuilder CreateConnectionStringBuilder() => new SpannerConnectionStringBuilder(); + + /// + /// Returns a strongly typed instance. + /// + public override DbCommandBuilder CreateCommandBuilder() => new SpannerCommandBuilder(); + + /// + /// Returns a strongly typed instance. + /// + public override DbDataAdapter CreateDataAdapter() => new SpannerDataAdapter(); + + /// + /// Specifies whether the specific supports the class. + /// + public override bool CanCreateDataAdapter => true; + + /// + /// Specifies whether the specific supports the class. + /// + public override bool CanCreateCommandBuilder => true; + + /// + public override bool CanCreateBatch => true; + + /// + public override DbBatch CreateBatch() => new SpannerBatch(); + + /// + public override DbBatchCommand CreateBatchCommand() => new SpannerBatchCommand(); + + /// + public override DbDataSource CreateDataSource(string connectionString) + => SpannerDataSource.Create(connectionString); + + #region IServiceProvider Members + + /// + /// Gets the service object of the specified type. + /// + /// An object that specifies the type of service object to get. + /// A service object of type serviceType, or null if there is no service object of type serviceType. + public object? GetService(System.Type serviceType) => null; + + #endregion + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerParameter.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerParameter.cs new file mode 100644 index 00000000..f1e7e2d0 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerParameter.cs @@ -0,0 +1,186 @@ +// Copyright 2025 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 +// +// https://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. + +using System; +using System.Collections; +using System.Data; +using System.Data.Common; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Xml; +using Google.Api.Gax; +using Google.Cloud.Spanner.V1; +using Google.Protobuf.WellKnownTypes; +using TypeCode = Google.Cloud.Spanner.V1.TypeCode; + +namespace Google.Cloud.Spanner.DataProvider; + +public class SpannerParameter : DbParameter +{ + private DbType? _dbType; + + public override DbType DbType + { + get => _dbType ?? DbType.String; + set => _dbType = value; + } + + /// + /// SpannerParameterType overrides the standard DbType property with a specific Spanner type. + /// Use this property if you need to set a specific Spanner type that is not supported by DbType, such as + /// one of the Spanner array types. + /// + public V1.Type? SpannerParameterType { get; set; } + + public override ParameterDirection Direction { get; set; } = ParameterDirection.Input; + public override bool IsNullable { get; set; } + + private string _name = ""; + [AllowNull] public override string ParameterName + { + get => _name; + set => _name = value ?? ""; + } + + private string _sourceColumn = ""; + + [AllowNull] + public override string SourceColumn + { + get => _sourceColumn; + set => _sourceColumn = value ?? ""; + } + public override object? Value { get; set; } + public override bool SourceColumnNullMapping { get; set; } + public override int Size { get; set; } + public override DataRowVersion SourceVersion + { + get => DataRowVersion.Current; + set { } + } + + public override void ResetDbType() + { + _dbType = null; + } + + internal Value ConvertToProto() + { + GaxPreconditions.CheckState(Value != null, $"Parameter {ParameterName} has no value"); + return ConvertToProto(Value!); + } + + internal Google.Cloud.Spanner.V1.Type? GetSpannerType() + { + return SpannerParameterType ?? TypeConversion.GetSpannerType(_dbType); + } + + private Value ConvertToProto(object value) + { + var type = GetSpannerType(); + return ConvertToProto(value, type); + } + + private static Value ConvertToProto(object? value, Google.Cloud.Spanner.V1.Type? type) + { + var proto = new Value(); + switch (value) + { + case null: + case DBNull: + proto.NullValue = NullValue.NullValue; + break; + case bool b: + proto.BoolValue = b; + break; + case double d: + proto.NumberValue = d; + break; + case float f: + proto.NumberValue = f; + break; + case string str: + proto.StringValue = str; + break; + case Regex regex: + proto.StringValue = regex.ToString(); + break; + case byte b: + proto.StringValue = b.ToString(); + break; + case byte[] bytes: + proto.StringValue = Convert.ToBase64String(bytes); + break; + case MemoryStream memoryStream: + // TODO: Optimize this + proto.StringValue = Convert.ToBase64String(memoryStream.ToArray()); + break; + case short s: + proto.StringValue = s.ToString(); + break; + case int i: + proto.StringValue = i.ToString(); + break; + case long l: + proto.StringValue = l.ToString(); + break; + case decimal d: + proto.StringValue = d.ToString(CultureInfo.InvariantCulture); + break; + case SpannerNumeric num: + proto.StringValue = num.ToString(); + break; + case DateOnly d: + proto.StringValue = d.ToString("O"); + break; + case SpannerDate d: + proto.StringValue = d.ToString(); + break; + case DateTime d: + // Some framework pass DATE values as DateTime. + if (type?.Code == TypeCode.Date) + { + proto.StringValue = d.Date.ToString("yyyy-MM-dd"); + } + else + { + proto.StringValue = d.ToUniversalTime().ToString("O"); + } + break; + case TimeSpan t: + proto.StringValue = XmlConvert.ToString(t); + break; + case JsonDocument jd: + proto.StringValue = jd.RootElement.ToString(); + break; + case IEnumerable list: + var elementType = type?.ArrayElementType; + proto.ListValue = new ListValue(); + foreach (var item in list) + { + proto.ListValue.Values.Add(ConvertToProto(item, elementType)); + } + break; + default: + // Unknown type. Just try to send it as a string. + proto.StringValue = value.ToString(); + break; + } + return proto; + } + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerParameterCollection.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerParameterCollection.cs new file mode 100644 index 00000000..9fc6734d --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerParameterCollection.cs @@ -0,0 +1,218 @@ +// Copyright 2025 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 +// +// https://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. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data.Common; +using Google.Api.Gax; +using Google.Protobuf.Collections; +using Google.Protobuf.WellKnownTypes; + +namespace Google.Cloud.Spanner.DataProvider; + +public class SpannerParameterCollection : DbParameterCollection +{ + private readonly List _params = new (); + public override int Count => _params.Count; + public override object SyncRoot => _params; + + public override int Add(object value) + { + GaxPreconditions.CheckNotNull(value, nameof(value)); + var index = _params.Count; + if (value is SpannerParameter spannerParameter) + { + _params.Add(spannerParameter); + } + else + { + _params.Add(new SpannerParameter { ParameterName = "p" + (index + 1), Value = value }); + } + + return index; + } + + public override void Clear() + { + _params.Clear(); + } + + public override bool Contains(object value) + { + return _params.Find(p => Equals(p.Value, value)) != null; + } + + public override int IndexOf(object value) + { + if (value is SpannerParameter spannerParameter) + { + return _params.IndexOf(spannerParameter); + } + return _params.FindIndex(p => Equals(p.Value, value)); + } + + public override void Insert(int index, object value) + { + GaxPreconditions.CheckNotNull(value, nameof(value)); + if (value is SpannerParameter spannerParameter) + { + _params.Insert(index, spannerParameter); + } + else + { + _params.Insert(index, new SpannerParameter { ParameterName = "p" + (index + 1), Value = value }); + } + } + + public override void Remove(object value) + { + GaxPreconditions.CheckNotNull(value, nameof(value)); + var index = IndexOf(value); + if (index > -1) + { + _params.RemoveAt(index); + } + } + + public override void RemoveAt(int index) + { + _params.RemoveAt(index); + } + + public override void RemoveAt(string parameterName) + { + var index = _params.FindIndex(p => Equals(p.ParameterName, parameterName)); + if (index > -1) + { + _params.RemoveAt(index); + } + } + + protected override void SetParameter(int index, DbParameter value) + { + GaxPreconditions.CheckNotNull(value, nameof(value)); + if (value is SpannerParameter spannerParameter) + { + _params[index] = spannerParameter; + } + else + { + throw new ArgumentException("value is not a SpannerParameter"); + } + } + + protected override void SetParameter(string parameterName, DbParameter value) + { + GaxPreconditions.CheckNotNull(value, nameof(value)); + if (value is SpannerParameter spannerParameter) + { + var index = _params.FindIndex(p => Equals(p.ParameterName, parameterName)); + if (index > -1) + { + _params[index] = spannerParameter; + } + } + else + { + throw new ArgumentException("value is not a SpannerParameter"); + } + } + + public override int IndexOf(string parameterName) + { + return _params.FindIndex(p => Equals(p.ParameterName, parameterName)); + } + + public override bool Contains(string value) + { + return _params.Find(p => Equals(p.Value, value)) != null; + } + + public override void CopyTo(Array array, int index) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + if (array.Length < _params.Count + index) + { + throw new ArgumentOutOfRangeException( + nameof(array), "There is not enough space in the array to copy values."); + } + + foreach (var item in _params) + { + array.SetValue(item, index); + index++; + } + } + + public override IEnumerator GetEnumerator() + { + return _params.GetEnumerator(); + } + + protected override DbParameter GetParameter(int index) + { + return _params[index]; + } + + protected override DbParameter GetParameter(string parameterName) + { + return _params.Find(p => Equals(p.ParameterName, parameterName)); + } + + public override void AddRange(Array values) + { + foreach (var value in values) + { + Add(value); + } + } + + internal Tuple> CreateSpannerParams() + { + var queryParams = new Struct(); + var paramTypes = new MapField(); + for (var index = 0; index < Count; index++) + { + var param = this[index]; + if (param is SpannerParameter spannerParameter) + { + var name = param.ParameterName; + if (name.StartsWith("@")) + { + name = name[1..]; + } + else if (name.StartsWith("$")) + { + name = "p" + name[1..]; + } + queryParams.Fields.Add(name, spannerParameter.ConvertToProto()); + var paramType = spannerParameter.GetSpannerType(); + if (paramType != null) + { + paramTypes.Add(name, paramType); + } + } + else + { + throw new InvalidOperationException("parameter is not a SpannerParameter: " + param.ParameterName); + } + } + return Tuple.Create(queryParams, paramTypes); + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerPool.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerPool.cs new file mode 100644 index 00000000..6d401971 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerPool.cs @@ -0,0 +1,97 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Google.Cloud.SpannerLib; +using Google.Cloud.SpannerLib.Grpc; +using Google.Cloud.SpannerLib.Native.Impl; + +namespace Google.Cloud.Spanner.DataProvider; + +internal class SpannerPool +{ + private static ISpannerLib? _gRpcSpannerLib; + + private static ISpannerLib GrpcSpannerLib + { + get + { + _gRpcSpannerLib ??= new GrpcLibSpanner(); + return _gRpcSpannerLib; + } + } + + private static ISpannerLib? _nativeSpannerLib; + + private static ISpannerLib NativeSpannerLib + { + get + { + _nativeSpannerLib ??= new SharedLibSpanner(); + return _nativeSpannerLib; + } + } + + private static readonly ConcurrentDictionary Pools = new(); + + [MethodImpl(MethodImplOptions.Synchronized)] + internal static SpannerPool GetOrCreate(string dsn, bool useNativeLibrary = false) + { + if (Pools.TryGetValue(dsn, out var value)) + { + return value; + } + var pool = Pool.Create(useNativeLibrary ? NativeSpannerLib : GrpcSpannerLib, dsn); + var spannerPool = new SpannerPool(dsn, pool); + Pools[dsn] = spannerPool; + return spannerPool; + } + + [MethodImpl(MethodImplOptions.Synchronized)] + internal static void CloseSpannerLib() + { + foreach (var pool in Pools.Values) + { + pool.Close(); + } + Pools.Clear(); + GrpcSpannerLib.Dispose(); + _gRpcSpannerLib = null; + NativeSpannerLib.Dispose(); + _nativeSpannerLib = null; + } + + private readonly string _dsn; + + private readonly Pool _libPool; + + private SpannerPool(string dsn, Pool libPool) + { + _dsn = dsn; + _libPool = libPool; + } + + internal void Close() + { + _libPool.Close(); + Pools.Remove(_dsn, out _); + } + + internal Connection CreateConnection() + { + return _libPool.CreateConnection(); + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs new file mode 100644 index 00000000..5852fd21 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs @@ -0,0 +1,149 @@ +// Copyright 2025 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 +// +// https://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. + +using System; +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Google.Api.Gax; +using Google.Cloud.Spanner.V1; + +namespace Google.Cloud.Spanner.DataProvider; + +public class SpannerTransaction : DbTransaction +{ + private SpannerConnection? _spannerConnection; + + protected override DbConnection? DbConnection => _spannerConnection; + public override IsolationLevel IsolationLevel { get; } + private SpannerLib.Connection LibConnection { get; } + + private bool _disposed; + + internal SpannerTransaction(SpannerConnection connection, TransactionOptions options) + { + _spannerConnection = connection; + IsolationLevel = TranslateIsolationLevel(options.IsolationLevel); + LibConnection = connection.LibConnection!; + LibConnection.BeginTransaction(options); + } + + internal static TransactionOptions.Types.IsolationLevel TranslateIsolationLevel(IsolationLevel isolationLevel) + { + return isolationLevel switch + { + IsolationLevel.Chaos => throw new NotSupportedException(), + IsolationLevel.ReadUncommitted => throw new NotSupportedException(), + IsolationLevel.ReadCommitted => throw new NotSupportedException(), + IsolationLevel.RepeatableRead => TransactionOptions.Types.IsolationLevel.RepeatableRead, + IsolationLevel.Snapshot => TransactionOptions.Types.IsolationLevel.RepeatableRead, + IsolationLevel.Serializable => TransactionOptions.Types.IsolationLevel.Serializable, + _ => TransactionOptions.Types.IsolationLevel.Unspecified + }; + } + + private static IsolationLevel TranslateIsolationLevel(TransactionOptions.Types.IsolationLevel isolationLevel) + { + switch (isolationLevel) + { + case TransactionOptions.Types.IsolationLevel.Unspecified: + return IsolationLevel.Unspecified; + case TransactionOptions.Types.IsolationLevel.RepeatableRead: + return IsolationLevel.RepeatableRead; + case TransactionOptions.Types.IsolationLevel.Serializable: + return IsolationLevel.Serializable; + default: + throw new ArgumentOutOfRangeException(nameof(isolationLevel), isolationLevel, + "unsupported isolation level"); + } + } + + protected override void Dispose(bool disposing) + { + if (_spannerConnection != null) + { + // Do a shoot-and-forget rollback. + RollbackAsync(CancellationToken.None); + } + _disposed = true; + base.Dispose(disposing); + } + + public override ValueTask DisposeAsync() + { + if (_spannerConnection != null) + { + // Do a shoot-and-forget rollback. + RollbackAsync(CancellationToken.None); + } + _disposed = true; + return base.DisposeAsync(); + } + + private void CheckDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } + + public override void Commit() + { + EndTransaction(() => LibConnection.Commit()); + } + + public override Task CommitAsync(CancellationToken cancellationToken = default) + { + return EndTransactionAsync(() => LibConnection.CommitAsync(cancellationToken)); + } + + public override void Rollback() + { + EndTransaction(() => LibConnection.Rollback()); + } + + public override Task RollbackAsync(CancellationToken cancellationToken = default) + { + return EndTransactionAsync(() => LibConnection.RollbackAsync(cancellationToken)); + } + + private void EndTransaction(Action endTransactionMethod) + { + CheckDisposed(); + GaxPreconditions.CheckState(_spannerConnection is not null, "This transaction is no longer active"); + try + { + endTransactionMethod(); + } + finally + { + _spannerConnection?.ClearTransaction(); + _spannerConnection = null; + } + } + + private Task EndTransactionAsync(Func endTransactionMethod) + { + CheckDisposed(); + GaxPreconditions.CheckState(_spannerConnection is not null, "This transaction is no longer active"); + try + { + return endTransactionMethod(); + } + finally + { + _spannerConnection?.ClearTransaction(); + _spannerConnection = null; + } + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/TypeConversion.cs b/drivers/spanner-ado-net/spanner-ado-net/TypeConversion.cs new file mode 100644 index 00000000..c31c4fc5 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/TypeConversion.cs @@ -0,0 +1,87 @@ +// Copyright 2025 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 +// +// https://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. + +using System; +using System.Collections.Generic; +using System.Data; +using TypeCode = Google.Cloud.Spanner.V1.TypeCode; + +namespace Google.Cloud.Spanner.DataProvider; + +internal static class TypeConversion +{ + private static readonly Dictionary SDbTypeToSpannerTypeMapping = new (); + + static TypeConversion() + { + SDbTypeToSpannerTypeMapping[DbType.Date] = new V1.Type { Code = TypeCode.Date }; + SDbTypeToSpannerTypeMapping[DbType.Binary] = new V1.Type { Code = TypeCode.Bytes }; + SDbTypeToSpannerTypeMapping[DbType.Boolean] = new V1.Type { Code = TypeCode.Bool }; + SDbTypeToSpannerTypeMapping[DbType.Double] = new V1.Type { Code = TypeCode.Float64 }; + SDbTypeToSpannerTypeMapping[DbType.Single] = new V1.Type { Code = TypeCode.Float32 }; + SDbTypeToSpannerTypeMapping[DbType.Guid] = new V1.Type { Code = TypeCode.Uuid }; + + var numericType = new V1.Type { Code = TypeCode.Numeric }; + SDbTypeToSpannerTypeMapping[DbType.Decimal] = numericType; + SDbTypeToSpannerTypeMapping[DbType.VarNumeric] = numericType; + + var timestampType = new V1.Type { Code = TypeCode.Timestamp }; + SDbTypeToSpannerTypeMapping[DbType.DateTime] = timestampType; + SDbTypeToSpannerTypeMapping[DbType.DateTime2] = timestampType; + SDbTypeToSpannerTypeMapping[DbType.DateTimeOffset] = timestampType; + + var int64Type = new V1.Type { Code = TypeCode.Int64 }; + SDbTypeToSpannerTypeMapping[DbType.Byte] = int64Type; + SDbTypeToSpannerTypeMapping[DbType.Int16] = int64Type; + SDbTypeToSpannerTypeMapping[DbType.Int32] = int64Type; + SDbTypeToSpannerTypeMapping[DbType.Int64] = int64Type; + SDbTypeToSpannerTypeMapping[DbType.SByte] = int64Type; + SDbTypeToSpannerTypeMapping[DbType.UInt16] = int64Type; + SDbTypeToSpannerTypeMapping[DbType.UInt32] = int64Type; + SDbTypeToSpannerTypeMapping[DbType.UInt64] = int64Type; + + var stringType = new V1.Type { Code = TypeCode.String }; + SDbTypeToSpannerTypeMapping[DbType.String] = stringType; + SDbTypeToSpannerTypeMapping[DbType.StringFixedLength] = stringType; + SDbTypeToSpannerTypeMapping[DbType.AnsiString] = stringType; + SDbTypeToSpannerTypeMapping[DbType.AnsiStringFixedLength] = stringType; + } + + internal static V1.Type? GetSpannerType(DbType? dbType) + { + return dbType == null ? null : SDbTypeToSpannerTypeMapping.GetValueOrDefault(dbType.Value); + } + + internal static System.Type GetSystemType(V1.Type type) + { + return type.Code switch + { + TypeCode.Bool => typeof(bool), + TypeCode.Bytes => typeof(byte[]), + TypeCode.Date => typeof(DateOnly), + TypeCode.Enum => typeof(long), + TypeCode.Float32 => typeof(float), + TypeCode.Float64 => typeof(double), + TypeCode.Int64 => typeof(long), + TypeCode.Interval => typeof(TimeSpan), + TypeCode.Json => typeof(string), + TypeCode.Numeric => typeof(decimal), + TypeCode.Proto => typeof(byte[]), + TypeCode.String => typeof(string), + TypeCode.Timestamp => typeof(DateTime), + TypeCode.Uuid => typeof(Guid), + _ => typeof(string) + }; + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/publish.sh b/drivers/spanner-ado-net/spanner-ado-net/publish.sh new file mode 100644 index 00000000..4425ca25 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/publish.sh @@ -0,0 +1,11 @@ +VERSION=$(date -u +"1.0.0-alpha.%Y%m%d%H%M%S") + +echo "Publishing as version $VERSION" +sed -i "" "s|.*|$VERSION|g" spanner-ado-net.csproj + +rm -rf bin/Release +dotnet pack +dotnet nuget push \ + bin/Release/Alpha.Google.Cloud.Spanner.DataProvider.*.nupkg \ + --api-key $NUGET_API_KEY \ + --source https://api.nuget.org/v3/index.json diff --git a/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj b/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj new file mode 100644 index 00000000..d27cbaf3 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + Google.Cloud.Spanner.DataProvider + enable + default + Google.Cloud.Spanner.DataProvider + Alpha.Google.Cloud.Spanner.DataProvider + .NET Data Provider for Spanner + Google + 1.0.0-alpha.20251003170157 + ADO.NET Data Provider. + +Alpha version: Not for production use + Apache v2.0 + https://github.com/googleapis/go-sql-spanner/drivers/spanner-ado-net/LICENSE + https://github.com/googleapis/go-sql-spanner/drivers/spanner-ado-net + + + + + + + + + + From a0441c551229f3571c8e2e02cf8ce6847d39643e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 16 Oct 2025 09:56:14 +0200 Subject: [PATCH 04/29] test: add more tests --- .../AbstractMockServerTests.cs | 1 - .../spanner-ado-net-tests/BasicTests.cs | 16 +- .../ConnectionStringBuilderTests.cs | 171 ++++++++ .../spanner-ado-net-tests/ConnectionTests.cs | 86 +++- .../spanner-ado-net/SpannerCommand.cs | 6 + .../SpannerConnectionStringBuilder.cs | 407 +++++++++++++++++- 6 files changed, 681 insertions(+), 6 deletions(-) create mode 100644 drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionStringBuilderTests.cs diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs index d4db3812..b0e5889d 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using Google.Cloud.SpannerLib.MockServer; namespace Google.Cloud.Spanner.DataProvider.Tests; diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/BasicTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/BasicTests.cs index 613b443b..6962038a 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/BasicTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/BasicTests.cs @@ -1,6 +1,18 @@ -using System; +// Copyright 2025 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 +// +// https://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. + using System.Data.Common; -using System.Linq; using System.Text.Json; using Google.Cloud.Spanner.V1; using Google.Cloud.SpannerLib.MockServer; diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionStringBuilderTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionStringBuilderTests.cs new file mode 100644 index 00000000..d413edd8 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionStringBuilderTests.cs @@ -0,0 +1,171 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Data; + +namespace Google.Cloud.Spanner.DataProvider.Tests; + +public class ConnectionStringBuilderTests +{ + [Test] + public void Basic() + { + var builder = new SpannerConnectionStringBuilder(); + Assert.That(builder.Keys, Is.Empty); + Assert.That(builder.Count, Is.EqualTo(0)); + Assert.False(builder.ContainsKey("host")); + builder.Host = "myhost"; + Assert.That(builder["host"], Is.EqualTo("myhost")); + Assert.That(builder.Count, Is.EqualTo(1)); + Assert.That(builder.ConnectionString, Is.EqualTo("Host=myhost")); + builder.Remove("HOST"); + Assert.That(builder["host"], Is.EqualTo("")); + Assert.That(builder.Count, Is.EqualTo(0)); + } + + [Test] + public void TryGetValue() + { + var builder = new SpannerConnectionStringBuilder + { + ConnectionString = "Host=myhost" + }; + Assert.That(builder.TryGetValue("Host", out var value), Is.True); + Assert.That(value, Is.EqualTo("myhost")); + Assert.That(builder.TryGetValue("SomethingUnknown", out value), Is.False); + } + + [Test] + public void Remove() + { + var builder = new SpannerConnectionStringBuilder + { + UsePlainText = true + }; + Assert.That(builder["Use plain text"], Is.True); + builder.Remove("UsePlainText"); + Assert.That(builder.ConnectionString, Is.EqualTo("")); + } + + [Test] + public void Clear() + { + var builder = new SpannerConnectionStringBuilder { Host = "myhost" }; + builder.Clear(); + Assert.That(builder.Count, Is.EqualTo(0)); + Assert.That(builder["host"], Is.EqualTo("")); + Assert.That(builder.Host, Is.Empty); + } + + [Test] + public void RemovingResetsToDefault() + { + var builder = new SpannerConnectionStringBuilder(); + Assert.That(builder.Port, Is.EqualTo(SpannerConnectionStringOption.Port.DefaultValue)); + builder.Port = 8; + builder.Remove("Port"); + Assert.That(builder.Port, Is.EqualTo(SpannerConnectionStringOption.Port.DefaultValue)); + } + + [Test] + public void SettingToNullResetsToDefault() + { + var builder = new SpannerConnectionStringBuilder(); + Assert.That(builder.Port, Is.EqualTo(SpannerConnectionStringOption.Port.DefaultValue)); + builder.Port = 8; + builder["Port"] = null; + Assert.That(builder.Port, Is.EqualTo(SpannerConnectionStringOption.Port.DefaultValue)); + } + + [Test] + public void Enum() + { + var builder = new SpannerConnectionStringBuilder + { + ConnectionString = "DefaultIsolationLevel=Serializable" + }; + Assert.That(builder.DefaultIsolationLevel, Is.EqualTo(IsolationLevel.Serializable)); + Assert.That(builder.Count, Is.EqualTo(1)); + } + + [Test] + public void EnumCaseInsensitive() + { + var builder = new SpannerConnectionStringBuilder + { + ConnectionString = "defaultisolationlevel=repeatable read" + }; + Assert.That(builder.DefaultIsolationLevel, Is.EqualTo(IsolationLevel.RepeatableRead)); + Assert.That(builder.Count, Is.EqualTo(1)); + } + + [Test] + public void Clone() + { + var builder = new SpannerConnectionStringBuilder + { + Host = "myhost" + }; + var builder2 = builder.Clone(); + Assert.That(builder2.Host, Is.EqualTo("myhost")); + Assert.That(builder2["Host"], Is.EqualTo("myhost")); + Assert.That(builder.Port, Is.EqualTo(SpannerConnectionStringOption.Port.DefaultValue)); + } + + [Test] + public void ConversionErrorThrows() + { + // ReSharper disable once CollectionNeverQueried.Local + var builder = new SpannerConnectionStringBuilder(); + Assert.That(() => builder["Port"] = "hello", + Throws.Exception.TypeOf().With.Message.Contains("Port")); + } + + [Test] + public void InvalidConnectionStringThrows() + { + var builder = new SpannerConnectionStringBuilder(); + Assert.That(() => builder.ConnectionString = "Server=127.0.0.1;User Id=npgsql_tests;Pooling:false", + Throws.Exception.TypeOf()); + } + + [Test] + public void ConnectionStringToProperties() + { + var builder = new SpannerConnectionStringBuilder + { + ConnectionString = "Host=localhost;Port=80;UsePlainText=true;DefaultIsolationLevel=Repeatable read", + }; + Assert.That(builder.Host, Is.EqualTo("localhost")); + Assert.That(builder.Port, Is.EqualTo(80)); + Assert.That(builder.UsePlainText, Is.True); + Assert.That(builder.DefaultIsolationLevel, Is.EqualTo(IsolationLevel.RepeatableRead)); + } + + [Test] + public void PropertiesToConnectionString() + { + var builder = new SpannerConnectionStringBuilder + { + Host = "localhost", + Port = 80, + UsePlainText = true, + DefaultIsolationLevel = IsolationLevel.RepeatableRead, + DataSource = "projects/project1/instances/instance1/databases/database1" + }; + Assert.That(builder.ConnectionString, Is.EqualTo("Data Source=projects/project1/instances/instance1/databases/database1;Host=localhost;Port=80;UsePlainText=True;DefaultIsolationLevel=RepeatableRead")); + Assert.That(builder.SpannerLibConnectionString, Is.EqualTo("localhost:80/projects/project1/instances/instance1/databases/database1;UsePlainText=True;DefaultIsolationLevel=RepeatableRead")); + } + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionTests.cs index b4a33abb..30597b36 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionTests.cs @@ -12,11 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Linq; +using System.Data; using Google.Cloud.Spanner.V1; using Google.Cloud.SpannerLib; using Google.Cloud.SpannerLib.MockServer; +using Google.Rpc; using Grpc.Core; +using Status = Grpc.Core.Status; using TypeCode = Google.Cloud.Spanner.V1.TypeCode; namespace Google.Cloud.Spanner.DataProvider.Tests; @@ -174,4 +176,86 @@ public void TestBatchDml() Assert.That(affected, Is.EqualTo(new long[] { 2, 3 })); } + [Test] + public async Task TestBasicLifecycle() + { + await using var conn = new SpannerConnection(); + conn.ConnectionString = ConnectionString; + + var eventConnecting = false; + var eventOpen = false; + var eventClosed = false; + + conn.StateChange += (_, e) => + { + if (e is { OriginalState: ConnectionState.Closed, CurrentState: ConnectionState.Connecting }) + eventConnecting = true; + + if (e is { OriginalState: ConnectionState.Connecting, CurrentState: ConnectionState.Open }) + eventOpen = true; + + if (e is { OriginalState: ConnectionState.Open, CurrentState: ConnectionState.Closed }) + eventClosed = true; + }; + + Assert.That(conn.State, Is.EqualTo(ConnectionState.Closed)); + Assert.That(eventConnecting, Is.False); + Assert.That(eventOpen, Is.False); + + await conn.OpenAsync(); + + Assert.That(conn.State, Is.EqualTo(ConnectionState.Open)); + Assert.That(eventConnecting, Is.True); + Assert.That(eventOpen, Is.True); + + await using (var cmd = new SpannerCommand("SELECT 1", conn)) + await using (var reader = await cmd.ExecuteReaderAsync()) + { + await reader.ReadAsync(); + Assert.That(conn.State, Is.EqualTo(ConnectionState.Open)); + } + + Assert.That(conn.State, Is.EqualTo(ConnectionState.Open)); + + await conn.CloseAsync(); + + Assert.That(conn.State, Is.EqualTo(ConnectionState.Closed)); + Assert.That(eventClosed, Is.True); + } + + [Test] + [Ignore("SpannerLib should support a connect_timeout property to make this test quicker")] + public async Task TestInvalidHost() + { + await using var conn = new SpannerConnection(); + conn.ConnectionString = $"{Fixture.Host}_invalid:{Fixture.Port}/projects/p1/instances/i1/databases/d1;UsePlainText=true"; + var exception = Assert.Throws(() => conn.Open()); + Assert.That(exception.Code, Is.EqualTo(Code.DeadlineExceeded)); + } + + [Test] + public async Task TestInvalidDatabase() + { + // Close all current pools to ensure that we get a fresh pool. + SpannerPool.CloseSpannerLib(); + // TODO: Make this a public property in the mock server. + const string detectDialectQuery = + "select option_value from information_schema.database_options where option_name='database_dialect'"; + Fixture.SpannerMock.AddOrUpdateStatementResult(detectDialectQuery, StatementResult.CreateException(new RpcException(new Status(StatusCode.NotFound, "Database not found")))); + await using var conn = new SpannerConnection(); + conn.ConnectionString = ConnectionString; + var exception = Assert.Throws(() => conn.Open()); + Assert.That(exception.Code, Is.EqualTo(Code.NotFound)); + } + + [Test] + public async Task TestConnectWithConnectionStringBuilder() + { + var builder = new SpannerConnectionStringBuilder(); + builder.DataSource = "projects/my-project/instances/my-instance/databases/my-database"; + builder.Host = Fixture.Host; + builder.Port = (uint) Fixture.Port; + builder.UsePlainText = true; + } + } \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs index 912c11ac..4b9ad3d6 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs @@ -54,6 +54,12 @@ internal SpannerCommand(SpannerConnection connection) Connection = GaxPreconditions.CheckNotNull(connection, nameof(connection)); } + public SpannerCommand(string commandText, SpannerConnection connection) + { + Connection = GaxPreconditions.CheckNotNull(connection, nameof(connection)); + _commandText = GaxPreconditions.CheckNotNull(commandText, nameof(commandText)); + } + internal SpannerCommand(SpannerConnection connection, Mutation mutation) { Connection = GaxPreconditions.CheckNotNull(connection, nameof(connection)); diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnectionStringBuilder.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnectionStringBuilder.cs index acd93cce..bbfdca6e 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnectionStringBuilder.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnectionStringBuilder.cs @@ -12,11 +12,414 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; +using System.Collections; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; using System.Data.Common; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Text; namespace Google.Cloud.Spanner.DataProvider; public class SpannerConnectionStringBuilder : DbConnectionStringBuilder { - -} \ No newline at end of file + /// + /// The fully qualified name of the Spanner database to connect to. + /// Example: projects/my-project/instances/my-instance/databases/my-database + /// + [Category("Connection")] + [Description("The fully qualified name of the database to use. This property takes precedence over any Project, Instance, or Database that has been set in the connection string.")] + [DisplayName("Data Source")] + public string DataSource + { + get => SpannerConnectionStringOption.DataSource.GetValue(this); + set => SpannerConnectionStringOption.DataSource.SetValue(this, value); + } + + /// + /// The name of the Spanner instance to connect to. + /// + [Category("Connection")] + [Description("The name of the Google Cloud project to use.")] + [DisplayName("Project")] + public string Project + { + get => SpannerConnectionStringOption.Project.GetValue(this); + set => SpannerConnectionStringOption.Project.SetValue(this, value); + } + + /// + /// The name of the Spanner instance to connect to. + /// + [Category("Connection")] + [Description("The name of the Spanner instance to use.")] + [DisplayName("Instance")] + public string Instance + { + get => SpannerConnectionStringOption.Instance.GetValue(this); + set => SpannerConnectionStringOption.Instance.SetValue(this, value); + } + + /// + /// The name of the Spanner database to connect to. + /// + [Category("Connection")] + [Description("The name of the database to use")] + [DisplayName("Database")] + public string Database + { + get => SpannerConnectionStringOption.Database.GetValue(this); + set => SpannerConnectionStringOption.Database.SetValue(this, value); + } + + /// + /// The hostname or IP address of the Spanner server to connect to. + /// + [Category("Connection")] + [Description("The hostname or IP address of the Spanner server to connect to.")] + [DefaultValue("")] + [DisplayName("Host")] + public string Host + { + get => SpannerConnectionStringOption.Host.GetValue(this); + set => SpannerConnectionStringOption.Host.SetValue(this, value); + } + + /// + /// The TCP port of the Spanner server to connect to. + /// + [Category("Connection")] + [DefaultValue(443u)] + [Description("The TCP port of the Spanner server to connect to.")] + [DisplayName("Port")] + public uint Port + { + get => SpannerConnectionStringOption.Port.GetValue(this); + set => SpannerConnectionStringOption.Port.SetValue(this, value); + } + + /// + /// Whether to use plain text communication with the server. The default is SSL. + /// + [Category("Connection")] + [DefaultValue(false)] + [Description("Whether to use plain text or SSL (default).")] + [DisplayName("UsePlainText")] + public bool UsePlainText + { + get => SpannerConnectionStringOption.UsePlainText.GetValue(this); + set => SpannerConnectionStringOption.UsePlainText.SetValue(this, value); + } + + /// + /// The hostname or IP address of the Spanner server to connect to. + /// + [Category("Transaction")] + [Description("The default isolation level to use for transactions on this connection.")] + [DefaultValue(IsolationLevel.Unspecified)] + [DisplayName("DefaultIsolationLevel")] + public IsolationLevel DefaultIsolationLevel + { + get => SpannerConnectionStringOption.DefaultIsolationLevel.GetValue(this); + set => SpannerConnectionStringOption.DefaultIsolationLevel.SetValue(this, value); + } + + /// + /// Returns an that contains the keys in the . + /// + public override ICollection Keys => base.Keys.Cast().OrderBy(static x => SpannerConnectionStringOption.OptionNames.IndexOf(x)).ToList(); + + /// + /// Whether this contains a set option with the specified name. + /// + /// The option name. + /// true if an option with that name is set; otherwise, false. + public override bool ContainsKey(string keyword) => + SpannerConnectionStringOption.TryGetOptionForKey(keyword) is { } option && base.ContainsKey(option.Key); + + /// + /// Removes the option with the specified name. + /// + /// The option name. + public override bool Remove(string keyword) => + SpannerConnectionStringOption.TryGetOptionForKey(keyword) is { } option && base.Remove(option.Key); + + /// + /// Retrieves an option value by name. + /// + /// The option name. + /// That option's value, if set. + [AllowNull] + public override object this[string key] + { + get => SpannerConnectionStringOption.GetOptionForKey(key).GetObject(this); + set + { + var option = SpannerConnectionStringOption.GetOptionForKey(key); + if (value is null) + base[option.Key] = null; + else + option.SetObject(this, value); + } + } + + public SpannerConnectionStringBuilder() + { + } + + public SpannerConnectionStringBuilder(string connectionString) + { + ConnectionString = connectionString; + } + + internal void DoSetValue(string key, object? value) => base[key] = value; + + internal SpannerConnectionStringBuilder Clone() => new(ConnectionString); + + internal string SpannerLibConnectionString + { + get + { + var builder = new StringBuilder(); + if (Host != "") + { + builder.Append(Host); + if (Port != 443) + { + builder.Append(":"); + builder.Append(Port); + } + builder.Append('/'); + } + if (DataSource != "") + { + builder.Append(DataSource); + } + else if (Project != "" && Instance != "" && Database != "") + { + builder.Append("projects/").Append(Project); + builder.Append("/instances/").Append(Instance); + builder.Append("/databases/").Append(Database); + } + else + { + throw new ArgumentException("Invalid connection string. Either Data Source or Project, Instance, and Database must be specified."); + } + foreach (var key in Keys.Cast().Where(key => SpannerConnectionStringOption.SOptions.ContainsKey(key))) + { + var option = SpannerConnectionStringOption.SOptions[key]; + if (option.SpannerLibKey != "") + { + builder.Append(';').Append(option.SpannerLibKey).Append('=').Append(this[key]); + } + } + return builder.ToString(); + } + } + +} + +internal abstract class SpannerConnectionStringOption +{ + public static List OptionNames { get; } = []; + + // Connection Options + public static readonly SpannerConnectionStringReferenceOption DataSource; + public static readonly SpannerConnectionStringReferenceOption Host; + public static readonly SpannerConnectionStringValueOption Port; + public static readonly SpannerConnectionStringReferenceOption Project; + public static readonly SpannerConnectionStringReferenceOption Instance; + public static readonly SpannerConnectionStringReferenceOption Database; + + // SSL/TLS Options + public static readonly SpannerConnectionStringValueOption UsePlainText; + + // Transaction Options + public static readonly SpannerConnectionStringValueOption DefaultIsolationLevel; + + public static SpannerConnectionStringOption? TryGetOptionForKey(string key) => SOptions.GetValueOrDefault(key); + + public static SpannerConnectionStringOption GetOptionForKey(string key) => + TryGetOptionForKey(key) ?? throw new ArgumentException($"Option '{key}' not supported."); + + public string Key => _keys[0]; + public IReadOnlyList Keys => _keys; + + internal string SpannerLibKey { get; } + + public abstract object GetObject(SpannerConnectionStringBuilder builder); + public abstract void SetObject(SpannerConnectionStringBuilder builder, object value); + + protected SpannerConnectionStringOption(IReadOnlyList keys) : this(keys, keys[0]) + { + } + + protected SpannerConnectionStringOption(IReadOnlyList keys, string spannerLibKey) + { + _keys = keys; + SpannerLibKey = spannerLibKey; + } + + private static void AddOption(Dictionary options, SpannerConnectionStringOption option) + { + foreach (var key in option._keys) + { + options.Add(key, option); + } + OptionNames.Add(option._keys[0]); + } + + static SpannerConnectionStringOption() + { + var options = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Base Options + AddOption(options, DataSource = new( + keys: ["Data Source", "DataSource"], + spannerLibKey: "", + defaultValue: "")); + + AddOption(options, Host = new( + keys: ["Host", "Server"], + spannerLibKey: "", + defaultValue: "")); + + AddOption(options, Port = new( + keys: ["Port"], + spannerLibKey: "", + defaultValue: 443u)); + + AddOption(options, Project = new( + keys: ["Project"], + spannerLibKey: "", + defaultValue: "")); + + AddOption(options, Instance = new( + keys: ["Instance"], + spannerLibKey: "", + defaultValue: "")); + + AddOption(options, Database = new( + keys: ["Database", "Initial Catalog"], + spannerLibKey: "", + defaultValue: "")); + + // SSL/TLS Options + AddOption(options, UsePlainText = new( + keys: ["UsePlainText", "Use plain text", "Plain text", "use_plain_text"], + defaultValue: false)); + + // Transaction Options + AddOption(options, DefaultIsolationLevel = new( + keys: ["DefaultIsolationLevel", "default_isolation_level"], + defaultValue: IsolationLevel.Unspecified)); + + SOptions = options.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); + } + + internal static readonly FrozenDictionary SOptions; + + private readonly IReadOnlyList _keys; +} + +internal sealed class SpannerConnectionStringValueOption : SpannerConnectionStringOption + where T : struct +{ + public SpannerConnectionStringValueOption(IReadOnlyList keys, T defaultValue, Func? coerce = null) + : this(keys, keys[0], defaultValue, coerce) + { + } + + public SpannerConnectionStringValueOption(IReadOnlyList keys, string spannerLibKey, T defaultValue, Func? coerce = null) + : base(keys, spannerLibKey) + { + DefaultValue = defaultValue; + _coerce = coerce; + } + + public T DefaultValue { get; } + + public T GetValue(SpannerConnectionStringBuilder builder) => + builder.TryGetValue(Key, out var objectValue) ? ChangeType(objectValue) : DefaultValue; + + public void SetValue(SpannerConnectionStringBuilder builder, T value) => + builder.DoSetValue(Key, _coerce is null ? value : _coerce(value)); + + public override object GetObject(SpannerConnectionStringBuilder builder) => GetValue(builder); + + public override void SetObject(SpannerConnectionStringBuilder builder, object value) => SetValue(builder, ChangeType(value)); + + private T ChangeType(object objectValue) + { + if (typeof(T) == typeof(bool) && objectValue is string booleanString) + { + if (string.Equals(booleanString, "yes", StringComparison.OrdinalIgnoreCase)) + { + return (T)(object)true; + } + if (string.Equals(booleanString, "on", StringComparison.OrdinalIgnoreCase)) + { + return (T)(object)true; + } + if (string.Equals(booleanString, "no", StringComparison.OrdinalIgnoreCase)) + { + return (T)(object)false; + } + if (string.Equals(booleanString, "off", StringComparison.OrdinalIgnoreCase)) + { + return (T)(object)false; + } + } + + if (typeof(T).IsEnum && objectValue is string enumString) + { + enumString = enumString.Trim().Replace("_", "").Replace(" ", ""); + return (T)Enum.Parse(typeof(T), enumString, ignoreCase: true); + } + + try + { + return (T) Convert.ChangeType(objectValue, typeof(T), CultureInfo.InvariantCulture); + } + catch (Exception ex) + { + var exceptionMessage = string.Create(CultureInfo.InvariantCulture, $"Invalid value '{objectValue}' for '{Key}' connection string option."); + throw new ArgumentException(exceptionMessage, ex); + } + } + + private readonly Func? _coerce; +} + +internal sealed class SpannerConnectionStringReferenceOption : SpannerConnectionStringOption + where T : class +{ + public SpannerConnectionStringReferenceOption(IReadOnlyList keys, string spannerLibKey, T defaultValue, Func? coerce = null) + : base(keys, spannerLibKey) + { + DefaultValue = defaultValue; + _coerce = coerce; + } + + public T DefaultValue { get; } + + public T GetValue(SpannerConnectionStringBuilder builder) => + builder.TryGetValue(Key, out var objectValue) ? ChangeType(objectValue) : DefaultValue; + + public void SetValue(SpannerConnectionStringBuilder builder, T? value) => + builder.DoSetValue(Key, _coerce is null ? value : _coerce(value)); + + public override object GetObject(SpannerConnectionStringBuilder builder) => GetValue(builder); + + public override void SetObject(SpannerConnectionStringBuilder builder, object value) => SetValue(builder, ChangeType(value)); + + private static T ChangeType(object objectValue) => + (T) Convert.ChangeType(objectValue, typeof(T), CultureInfo.InvariantCulture); + + private readonly Func? _coerce; +} From 1d86660b5bb063d469f08e4e597562a904a06258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Mon, 20 Oct 2025 16:21:49 +0200 Subject: [PATCH 05/29] test: add more tests --- .../DbFactoryFixture.cs | 2 +- ...spanner-ado-net-specification-tests.csproj | 5 +- .../AbstractMockServerTests.cs | 2 +- .../ConnectionStringBuilderTests.cs | 128 +++++++++++++++++- .../spanner-ado-net-tests.csproj | 5 +- .../spanner-ado-net/SpannerConnection.cs | 72 +++++----- .../SpannerConnectionStringBuilder.cs | 75 ++++++++-- 7 files changed, 240 insertions(+), 49 deletions(-) diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DbFactoryFixture.cs b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DbFactoryFixture.cs index 7675bf42..6c4e39e2 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DbFactoryFixture.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DbFactoryFixture.cs @@ -34,7 +34,7 @@ static DbFactoryFixture() internal readonly SpannerMockServerFixture MockServerFixture = new (); public DbProviderFactory Factory => SpannerFactory.Instance; - public string ConnectionString => $"{MockServerFixture.Host}:{MockServerFixture.Port}/projects/p1/instances/i1/databases/d1;UsePlainText=true"; + public string ConnectionString => $"Host={MockServerFixture.Host};Port={MockServerFixture.Port};Data Source=projects/p1/instances/i1/databases/d1;UsePlainText=true"; public IReadOnlyCollection SupportedDbTypes { get; } = [ DbType.Binary, diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/spanner-ado-net-specification-tests.csproj b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/spanner-ado-net-specification-tests.csproj index ca134215..8f0930d8 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/spanner-ado-net-specification-tests.csproj +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/spanner-ado-net-specification-tests.csproj @@ -15,7 +15,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs index b0e5889d..4f900872 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs @@ -28,7 +28,7 @@ static AbstractMockServerTests() protected SpannerMockServerFixture Fixture; - protected string ConnectionString => $"{Fixture.Host}:{Fixture.Port}/projects/p1/instances/i1/databases/d1;UsePlainText=true"; + protected string ConnectionString => $"Host={Fixture.Host};Port={Fixture.Port};Data Source=projects/p1/instances/i1/databases/d1;UsePlainText=true"; [OneTimeSetUp] public void Setup() diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionStringBuilderTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionStringBuilderTests.cs index d413edd8..f30965d3 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionStringBuilderTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionStringBuilderTests.cs @@ -13,10 +13,15 @@ // limitations under the License. using System.Data; +using Google.Cloud.SpannerLib; +using Google.Cloud.SpannerLib.MockServer; +using Google.Rpc; +using Grpc.Core; +using Status = Grpc.Core.Status; namespace Google.Cloud.Spanner.DataProvider.Tests; -public class ConnectionStringBuilderTests +public class ConnectionStringBuilderTests : AbstractMockServerTests { [Test] public void Basic() @@ -167,5 +172,126 @@ public void PropertiesToConnectionString() Assert.That(builder.ConnectionString, Is.EqualTo("Data Source=projects/project1/instances/instance1/databases/database1;Host=localhost;Port=80;UsePlainText=True;DefaultIsolationLevel=RepeatableRead")); Assert.That(builder.SpannerLibConnectionString, Is.EqualTo("localhost:80/projects/project1/instances/instance1/databases/database1;UsePlainText=True;DefaultIsolationLevel=RepeatableRead")); } + + [Test] + public void RequiredConnectionStringProperties() + { + using var connection = new SpannerConnection(); + Assert.Throws(() => connection.ConnectionString = "Host=localhost;Port=80"); + } + + [Test] + public void FailedConnectThenSucceed() + { + // Close all current pools to ensure that we get a fresh pool. + SpannerPool.CloseSpannerLib(); + // TODO: Make this a public property in the mock server. + const string detectDialectQuery = + "select option_value from information_schema.database_options where option_name='database_dialect'"; + Fixture.SpannerMock.AddOrUpdateStatementResult(detectDialectQuery, StatementResult.CreateException(new RpcException(new Status(StatusCode.NotFound, "Database not found")))); + using var conn = new SpannerConnection(); + conn.ConnectionString = ConnectionString; + var exception = Assert.Throws(() => conn.Open()); + Assert.That(exception.Code, Is.EqualTo(Code.NotFound)); + Assert.That(conn.State, Is.EqualTo(ConnectionState.Closed)); + + // Remove the error and retry. + Fixture.SpannerMock.AddOrUpdateStatementResult(detectDialectQuery, StatementResult.CreateResultSet(new List> + { + Tuple.Create(V1.TypeCode.String, "option_value") + }, new List + { + new object[] { "GOOGLE_STANDARD_SQL" } + })); + conn.Open(); + Assert.That(conn.State, Is.EqualTo(ConnectionState.Open)); + } + + [Test] + [Ignore("Needs connect_timeout property")] + public void OpenTimeout() + { + // TODO: Add connect_timeout property. + var builder = new SpannerConnectionStringBuilder + { + Host = Fixture.Host, + Port = (uint) Fixture.Port, + UsePlainText = true, + DataSource = "projects/project1/instances/instance1/databases/database1", + //ConnectTimeout = TimeSpan.FromMicroseconds(1), + }; + using var connection = new SpannerConnection(); + connection.ConnectionString = builder.ConnectionString; + var exception = Assert.Throws(() => connection.Open()); + Assert.That(exception.ErrorCode, Is.EqualTo((int) Code.DeadlineExceeded)); + } + + [Test] + [Ignore("OpenAsync must be implemented")] + public async Task OpenCancel() + { + // Close all current pools to ensure that we get a fresh pool. + SpannerPool.CloseSpannerLib(); + Fixture.SpannerMock.AddOrUpdateExecutionTime(nameof(Fixture.SpannerMock.CreateSession), ExecutionTime.FromMillis(20, 0)); + var builder = new SpannerConnectionStringBuilder + { + Host = Fixture.Host, + Port = (uint) Fixture.Port, + UsePlainText = true, + DataSource = "projects/project1/instances/instance1/databases/database1", + }; + await using var connection = new SpannerConnection(); + connection.ConnectionString = builder.ConnectionString; + var tokenSource = new CancellationTokenSource(5); + // TODO: Implement actual async opening of connections + Assert.ThrowsAsync(async () => await connection.OpenAsync(tokenSource.Token)); + Assert.That(connection.State, Is.EqualTo(ConnectionState.Closed)); + } + + [Test] + public void DataSourceProperty() + { + using var conn = new SpannerConnection(); + Assert.That(conn.DataSource, Is.EqualTo(string.Empty)); + + var builder = new SpannerConnectionStringBuilder(ConnectionString); + + conn.ConnectionString = builder.ConnectionString; + Assert.That(conn.DataSource, Is.EqualTo("projects/p1/instances/i1/databases/d1")); + } + + [Test] + public void SettingConnectionStringWhileOpenThrows() + { + using var conn = new SpannerConnection(); + conn.ConnectionString = ConnectionString; + conn.Open(); + Assert.That(() => conn.ConnectionString = "", Throws.Exception.TypeOf()); + } + + [Test] + public void EmptyConstructor() + { + var conn = new SpannerConnection(); + Assert.That(conn.ConnectionTimeout, Is.EqualTo(15)); + Assert.That(conn.ConnectionString, Is.SameAs(string.Empty)); + Assert.That(() => conn.Open(), Throws.Exception.TypeOf()); + } + + [Test] + public void Constructor_with_null_connection_string() + { + var conn = new SpannerConnection(null); + Assert.That(conn.ConnectionString, Is.SameAs(string.Empty)); + Assert.That(() => conn.Open(), Throws.Exception.TypeOf()); + } + + [Test] + public void Constructor_with_empty_connection_string() + { + var conn = new NpgsqlConnection(""); + Assert.That(conn.ConnectionString, Is.SameAs(string.Empty)); + Assert.That(() => conn.Open(), Throws.Exception.TypeOf()); + } } \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/spanner-ado-net-tests.csproj b/drivers/spanner-ado-net/spanner-ado-net-tests/spanner-ado-net-tests.csproj index 051168cf..f1e0a28c 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/spanner-ado-net-tests.csproj +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/spanner-ado-net-tests.csproj @@ -14,7 +14,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs index 4b128772..3fe2c811 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs @@ -20,6 +20,7 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Google.Api.Gax; using Google.Cloud.Spanner.Common.V1; using Google.Cloud.Spanner.V1; using Google.Cloud.SpannerLib; @@ -31,6 +32,8 @@ public class SpannerConnection : DbConnection public bool UseSharedLibrary { get; set; } private string _connectionString = string.Empty; + + private SpannerConnectionStringBuilder? _connectionStringBuilder; [AllowNull] public override string ConnectionString { @@ -38,11 +41,18 @@ public override string ConnectionString { set { AssertClosed(); - if (!IsValidConnectionString(value)) + if (string.IsNullOrWhiteSpace(value)) + { + _connectionStringBuilder = null; + _connectionString = string.Empty; + } + else { - throw new ArgumentException($"Invalid connection string: {value}"); + var builder = new SpannerConnectionStringBuilder(value); + builder.CheckValid(); + _connectionStringBuilder = builder; + _connectionString = value; } - _connectionString = value ?? string.Empty; } } @@ -50,32 +60,21 @@ public override string Database { get { - // TODO: Move this to SpannerLib. - if (String.IsNullOrWhiteSpace(ConnectionString)) + if (string.IsNullOrWhiteSpace(ConnectionString) || _connectionStringBuilder == null) { return ""; } - var startIndex = ConnectionString.IndexOf("projects/", StringComparison.Ordinal); - if (startIndex == -1) - { - throw new ArgumentException($"Invalid database name in connection string: {ConnectionString}"); - } - - var endIndex = ConnectionString.IndexOf('?'); - if (endIndex == -1) - { - endIndex = ConnectionString.IndexOf(';'); - } - if (endIndex == -1) + if (!string.IsNullOrEmpty(_connectionStringBuilder.DataSource)) { - endIndex = ConnectionString.Length; + return _connectionStringBuilder.DataSource; } - var name = ConnectionString.Substring(startIndex, endIndex); - if (DatabaseName.TryParse(name, false, out var result)) + if (!string.IsNullOrEmpty(_connectionStringBuilder.Project) && + !string.IsNullOrEmpty(_connectionStringBuilder.Instance) && + !string.IsNullOrEmpty(_connectionStringBuilder.Project)) { - return result.DatabaseId; + return $"projects/{_connectionStringBuilder.Project}/instances/{_connectionStringBuilder.Instance}/databases/{_connectionStringBuilder.Database}"; } - throw new ArgumentException($"Invalid database name in connection string: {ConnectionString}"); + return ""; } } @@ -92,8 +91,9 @@ private ConnectionState InternalState public override ConnectionState State => InternalState; protected override DbProviderFactory DbProviderFactory => SpannerFactory.Instance; + + public override string DataSource => _connectionStringBuilder?.DataSource ?? string.Empty; - public override string DataSource { get; } = ""; public override string ServerVersion { get @@ -188,15 +188,23 @@ protected override void Dispose(bool disposing) public override void Open() { AssertClosed(); - if (ConnectionString == string.Empty) + if (ConnectionString == string.Empty || _connectionStringBuilder == null) { throw new InvalidOperationException("Connection string is empty"); } - - InternalState = ConnectionState.Connecting; - Pool = SpannerPool.GetOrCreate(ConnectionString); - _libConnection = Pool.CreateConnection(); - InternalState = ConnectionState.Open; + + try + { + InternalState = ConnectionState.Connecting; + Pool = SpannerPool.GetOrCreate(_connectionStringBuilder.SpannerLibConnectionString); + _libConnection = Pool.CreateConnection(); + InternalState = ConnectionState.Open; + } + catch (Exception) + { + InternalState = ConnectionState.Closed; + throw; + } } private void EnsureOpen() @@ -223,12 +231,6 @@ private void AssertClosed() } } - private bool IsValidConnectionString(string? connectionString) - { - // TODO: Move to Spanner lib. - return string.IsNullOrEmpty(connectionString) || connectionString.Contains("projects/"); - } - public CommitResponse? WriteMutations(BatchWriteRequest.Types.MutationGroup mutations) { EnsureOpen(); diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnectionStringBuilder.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnectionStringBuilder.cs index bbfdca6e..34c1b869 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnectionStringBuilder.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnectionStringBuilder.cs @@ -129,6 +129,20 @@ public IsolationLevel DefaultIsolationLevel set => SpannerConnectionStringOption.DefaultIsolationLevel.SetValue(this, value); } + /// + /// The time in seconds to wait for a connection before terminating the attempt and generating an error. + /// The default value is 15. + /// + [Category("Connection")] + [Description("The time in seconds to wait for a connection before terminating the attempt and generating an error.")] + [DefaultValue(15u)] + [DisplayName("Connection Timeout")] + public uint ConnectionTimeout + { + get => SpannerConnectionStringOption.ConnectionTimeout.GetValue(this); + set => SpannerConnectionStringOption.ConnectionTimeout.SetValue(this, value); + } + /// /// Returns an that contains the keys in the . /// @@ -157,14 +171,29 @@ public override bool Remove(string keyword) => [AllowNull] public override object this[string key] { - get => SpannerConnectionStringOption.GetOptionForKey(key).GetObject(this); + get + { + var option = SpannerConnectionStringOption.TryGetOptionForKey(key); + return option == null ? base[key] : option.GetObject(this); + } set { - var option = SpannerConnectionStringOption.GetOptionForKey(key); - if (value is null) - base[option.Key] = null; + var option = SpannerConnectionStringOption.TryGetOptionForKey(key); + if (option == null) + { + base[key] = value; + } else - option.SetObject(this, value); + { + if (value is null) + { + base[option.Key] = null; + } + else + { + option.SetObject(this, value); + } + } } } @@ -181,10 +210,26 @@ public SpannerConnectionStringBuilder(string connectionString) internal SpannerConnectionStringBuilder Clone() => new(ConnectionString); + internal void CheckValid() + { + if (string.IsNullOrEmpty(ConnectionString)) + { + throw new ArgumentException("Empty connection string"); + } + if (string.IsNullOrEmpty(DataSource)) + { + if (string.IsNullOrEmpty(Project) || string.IsNullOrEmpty(Instance) || string.IsNullOrEmpty(Database)) + { + throw new ArgumentException("The connection string must either contain a Data Source or a Project, Instance, and Database name"); + } + } + } + internal string SpannerLibConnectionString { get { + CheckValid(); var builder = new StringBuilder(); if (Host != "") { @@ -210,12 +255,19 @@ internal string SpannerLibConnectionString { throw new ArgumentException("Invalid connection string. Either Data Source or Project, Instance, and Database must be specified."); } - foreach (var key in Keys.Cast().Where(key => SpannerConnectionStringOption.SOptions.ContainsKey(key))) + foreach (var key in Keys.Cast()) { - var option = SpannerConnectionStringOption.SOptions[key]; - if (option.SpannerLibKey != "") + if (SpannerConnectionStringOption.SOptions.ContainsKey(key)) { - builder.Append(';').Append(option.SpannerLibKey).Append('=').Append(this[key]); + var option = SpannerConnectionStringOption.SOptions[key]; + if (option.SpannerLibKey != "") + { + builder.Append(';').Append(option.SpannerLibKey).Append('=').Append(this[key]); + } + } + else + { + builder.Append(';').Append(key).Append('=').Append(this[key]); } } return builder.ToString(); @@ -235,6 +287,7 @@ internal abstract class SpannerConnectionStringOption public static readonly SpannerConnectionStringReferenceOption Project; public static readonly SpannerConnectionStringReferenceOption Instance; public static readonly SpannerConnectionStringReferenceOption Database; + public static readonly SpannerConnectionStringValueOption ConnectionTimeout; // SSL/TLS Options public static readonly SpannerConnectionStringValueOption UsePlainText; @@ -309,6 +362,10 @@ static SpannerConnectionStringOption() spannerLibKey: "", defaultValue: "")); + AddOption(options, ConnectionTimeout = new( + keys: ["Connection Timeout", "ConnectionTimeout", "Connect Timeout", "connect_timeout"], + defaultValue: 15u)); + // SSL/TLS Options AddOption(options, UsePlainText = new( keys: ["UsePlainText", "Use plain text", "Plain text", "use_plain_text"], From 88ec7c1f62170f37d8db5cbe5e0ebb050ffc936c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Wed, 22 Oct 2025 17:22:17 +0200 Subject: [PATCH 06/29] test: add more tests --- .../AbstractMockServerTests.cs | 51 ++- .../spanner-ado-net-tests/CommandTests.cs | 101 ++++++ .../ConnectionStringBuilderTests.cs | 128 +------ .../spanner-ado-net-tests/ConnectionTests.cs | 331 +++++++++++++++++- .../spanner-ado-net-tests/TestUtils.cs | 19 + .../spanner-ado-net/SpannerCommand.cs | 16 +- .../spanner-ado-net/SpannerConnection.cs | 64 +++- .../SpannerConnectionStringBuilder.cs | 113 +++++- .../spanner-ado-net/SpannerDataSource.cs | 26 +- 9 files changed, 693 insertions(+), 156 deletions(-) create mode 100644 drivers/spanner-ado-net/spanner-ado-net-tests/TestUtils.cs diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs index 4f900872..049297d1 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs @@ -53,4 +53,53 @@ public void Reset() { Fixture.SpannerMock.Reset(); } -} \ No newline at end of file + + protected async Task OpenConnectionAsync() + { + var connection = new SpannerConnection(ConnectionString); + await connection.OpenAsync(); + return connection; + } + + protected SpannerDataSource CreateDataSource() + { + return CreateDataSource(_ => { }); + } + + protected SpannerDataSource CreateDataSource(Action connectionStringBuilderAction) + { + var connectionStringBuilder = new SpannerConnectionStringBuilder(ConnectionString); + connectionStringBuilderAction(connectionStringBuilder); + return SpannerDataSource.Create(connectionStringBuilder); + } + +} + +public static class SpannerConnectionExtensions +{ + public static int ExecuteNonQuery(this SpannerConnection conn, string sql, SpannerTransaction? tx = null) + { + using var command = tx == null ? new SpannerCommand(sql, conn) : new SpannerCommand(sql, conn, tx); + return command.ExecuteNonQuery(); + } + + public static object? ExecuteScalar(this SpannerConnection conn, string sql, SpannerTransaction? tx = null) + { + using var command = tx == null ? new SpannerCommand(sql, conn) : new SpannerCommand(sql, conn, tx); + return command.ExecuteScalar(); + } + + public static async Task ExecuteNonQueryAsync( + this SpannerConnection conn, string sql, SpannerTransaction? tx = null, CancellationToken cancellationToken = default) + { + await using var command = tx == null ? new SpannerCommand(sql, conn) : new SpannerCommand(sql, conn, tx); + return await command.ExecuteNonQueryAsync(cancellationToken); + } + + public static async Task ExecuteScalarAsync( + this SpannerConnection conn, string sql, SpannerTransaction? tx = null, CancellationToken cancellationToken = default) + { + await using var command = tx == null ? new SpannerCommand(sql, conn) : new SpannerCommand(sql, conn, tx); + return await command.ExecuteScalarAsync(cancellationToken); + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs index 02df9764..647f0724 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs @@ -13,8 +13,10 @@ // limitations under the License. using System; +using System.Data; using System.Data.Common; using System.Linq; +using System.Text; using System.Text.Json; using System.Threading.Tasks; using Google.Cloud.Spanner.V1; @@ -167,7 +169,106 @@ public async Task TestAllParameterTypes() Assert.That(fields["p23"].ListValue.Values[0].NumberValue, Is.EqualTo(3.14f)); Assert.That(fields["p23"].ListValue.Values[1].HasNullValue, Is.True); } + + [Test] + [TestCase(new[] { true }, TestName = "SingleQuery")] + [TestCase(new[] { false }, TestName = "SingleNonQuery")] + [TestCase(new[] { true, true }, TestName = "TwoQueries")] + [TestCase(new[] { false, false }, TestName = "TwoNonQueries")] + [TestCase(new[] { false, true }, TestName = "NonQueryQuery")] + [TestCase(new[] { true, false }, TestName = "QueryNonQuery")] + [Ignore("Requires support for multi-statements strings in the shared library")] + public async Task MultipleStatements(bool[] queries) + { + const string update = "UPDATE my_table SET name='yo' WHERE 1=0;"; + const string select = "SELECT 1;"; + Fixture.SpannerMock.AddOrUpdateStatementResult(update, StatementResult.CreateUpdateCount(0)); + + await using var conn = await OpenConnectionAsync(); + var sb = new StringBuilder(); + foreach (var query in queries) + { + sb.Append(query ? select : update); + } + var sql = sb.ToString(); + foreach (var prepare in new[] { false, true }) + { + await using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + if (prepare) + { + await cmd.PrepareAsync(); + } + await using var reader = await cmd.ExecuteReaderAsync(); + var numResultSets = queries.Count(q => q); + for (var i = 0; i < numResultSets; i++) + { + Assert.That(await reader.ReadAsync(), Is.True); + Assert.That(reader[0], Is.EqualTo(1)); + Assert.That(await reader.NextResultAsync(), Is.EqualTo(i != numResultSets - 1)); + } + } + } + [Test] + [Ignore("Requires support for multi-statements strings in the shared library")] + public async Task MultipleStatementsWithParameters([Values(PrepareOrNot.NotPrepared, PrepareOrNot.Prepared)] PrepareOrNot prepare) + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT @p1; SELECT @p2"; + var p1 = new SpannerParameter{ParameterName = "p1"}; + var p2 = new SpannerParameter{ParameterName = "p2"}; + cmd.Parameters.Add(p1); + cmd.Parameters.Add(p2); + if (prepare == PrepareOrNot.Prepared) + { + await cmd.PrepareAsync(); + } + p1.Value = 8; + p2.Value = "foo"; + await using var reader = await cmd.ExecuteReaderAsync(); + Assert.That(await reader.ReadAsync(), Is.True); + Assert.That(reader.GetInt32(0), Is.EqualTo(8)); + Assert.That(await reader.NextResultAsync(), Is.True); + Assert.That(await reader.ReadAsync(), Is.True); + Assert.That(reader.GetString(0), Is.EqualTo("foo")); + Assert.That(await reader.NextResultAsync(), Is.False); + } + + [Test] + [Ignore("Requires support for multi-statements strings in the shared library")] + public async Task SingleRowMultipleStatements([Values(PrepareOrNot.NotPrepared, PrepareOrNot.Prepared)] PrepareOrNot prepare) + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand("SELECT 1; SELECT 2", conn); + if (prepare == PrepareOrNot.Prepared) + { + await cmd.PrepareAsync(); + } + await using var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SingleRow); + Assert.That(await reader.ReadAsync(), Is.True); + Assert.That(reader.GetInt32(0), Is.EqualTo(1)); + Assert.That(await reader.ReadAsync(), Is.False); + Assert.That(await reader.NextResultAsync(), Is.False); + } + + [Test] + [Ignore("Requires support for statement_timeout in the shared library")] + public async Task Timeout() + { + Fixture.SpannerMock.AddOrUpdateExecutionTime(nameof(Fixture.SpannerMock.ExecuteStreamingSql), ExecutionTime.FromMillis(10, 0)); + + await using var dataSource = CreateDataSource(csb => csb.CommandTimeout = 1); + await using var conn = await dataSource.OpenConnectionAsync() as SpannerConnection; + await using var cmd = new SpannerCommand("SELECT 1", conn!); + Assert.That(() => cmd.ExecuteScalar(), Throws.Exception + .TypeOf() + .With.InnerException.TypeOf() + ); + Assert.That(conn!.State, Is.EqualTo(ConnectionState.Open)); + } + private void AddParameter(DbCommand command, string name, object? value) { var parameter = command.CreateParameter(); diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionStringBuilderTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionStringBuilderTests.cs index f30965d3..d413edd8 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionStringBuilderTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionStringBuilderTests.cs @@ -13,15 +13,10 @@ // limitations under the License. using System.Data; -using Google.Cloud.SpannerLib; -using Google.Cloud.SpannerLib.MockServer; -using Google.Rpc; -using Grpc.Core; -using Status = Grpc.Core.Status; namespace Google.Cloud.Spanner.DataProvider.Tests; -public class ConnectionStringBuilderTests : AbstractMockServerTests +public class ConnectionStringBuilderTests { [Test] public void Basic() @@ -172,126 +167,5 @@ public void PropertiesToConnectionString() Assert.That(builder.ConnectionString, Is.EqualTo("Data Source=projects/project1/instances/instance1/databases/database1;Host=localhost;Port=80;UsePlainText=True;DefaultIsolationLevel=RepeatableRead")); Assert.That(builder.SpannerLibConnectionString, Is.EqualTo("localhost:80/projects/project1/instances/instance1/databases/database1;UsePlainText=True;DefaultIsolationLevel=RepeatableRead")); } - - [Test] - public void RequiredConnectionStringProperties() - { - using var connection = new SpannerConnection(); - Assert.Throws(() => connection.ConnectionString = "Host=localhost;Port=80"); - } - - [Test] - public void FailedConnectThenSucceed() - { - // Close all current pools to ensure that we get a fresh pool. - SpannerPool.CloseSpannerLib(); - // TODO: Make this a public property in the mock server. - const string detectDialectQuery = - "select option_value from information_schema.database_options where option_name='database_dialect'"; - Fixture.SpannerMock.AddOrUpdateStatementResult(detectDialectQuery, StatementResult.CreateException(new RpcException(new Status(StatusCode.NotFound, "Database not found")))); - using var conn = new SpannerConnection(); - conn.ConnectionString = ConnectionString; - var exception = Assert.Throws(() => conn.Open()); - Assert.That(exception.Code, Is.EqualTo(Code.NotFound)); - Assert.That(conn.State, Is.EqualTo(ConnectionState.Closed)); - - // Remove the error and retry. - Fixture.SpannerMock.AddOrUpdateStatementResult(detectDialectQuery, StatementResult.CreateResultSet(new List> - { - Tuple.Create(V1.TypeCode.String, "option_value") - }, new List - { - new object[] { "GOOGLE_STANDARD_SQL" } - })); - conn.Open(); - Assert.That(conn.State, Is.EqualTo(ConnectionState.Open)); - } - - [Test] - [Ignore("Needs connect_timeout property")] - public void OpenTimeout() - { - // TODO: Add connect_timeout property. - var builder = new SpannerConnectionStringBuilder - { - Host = Fixture.Host, - Port = (uint) Fixture.Port, - UsePlainText = true, - DataSource = "projects/project1/instances/instance1/databases/database1", - //ConnectTimeout = TimeSpan.FromMicroseconds(1), - }; - using var connection = new SpannerConnection(); - connection.ConnectionString = builder.ConnectionString; - var exception = Assert.Throws(() => connection.Open()); - Assert.That(exception.ErrorCode, Is.EqualTo((int) Code.DeadlineExceeded)); - } - - [Test] - [Ignore("OpenAsync must be implemented")] - public async Task OpenCancel() - { - // Close all current pools to ensure that we get a fresh pool. - SpannerPool.CloseSpannerLib(); - Fixture.SpannerMock.AddOrUpdateExecutionTime(nameof(Fixture.SpannerMock.CreateSession), ExecutionTime.FromMillis(20, 0)); - var builder = new SpannerConnectionStringBuilder - { - Host = Fixture.Host, - Port = (uint) Fixture.Port, - UsePlainText = true, - DataSource = "projects/project1/instances/instance1/databases/database1", - }; - await using var connection = new SpannerConnection(); - connection.ConnectionString = builder.ConnectionString; - var tokenSource = new CancellationTokenSource(5); - // TODO: Implement actual async opening of connections - Assert.ThrowsAsync(async () => await connection.OpenAsync(tokenSource.Token)); - Assert.That(connection.State, Is.EqualTo(ConnectionState.Closed)); - } - - [Test] - public void DataSourceProperty() - { - using var conn = new SpannerConnection(); - Assert.That(conn.DataSource, Is.EqualTo(string.Empty)); - - var builder = new SpannerConnectionStringBuilder(ConnectionString); - - conn.ConnectionString = builder.ConnectionString; - Assert.That(conn.DataSource, Is.EqualTo("projects/p1/instances/i1/databases/d1")); - } - - [Test] - public void SettingConnectionStringWhileOpenThrows() - { - using var conn = new SpannerConnection(); - conn.ConnectionString = ConnectionString; - conn.Open(); - Assert.That(() => conn.ConnectionString = "", Throws.Exception.TypeOf()); - } - - [Test] - public void EmptyConstructor() - { - var conn = new SpannerConnection(); - Assert.That(conn.ConnectionTimeout, Is.EqualTo(15)); - Assert.That(conn.ConnectionString, Is.SameAs(string.Empty)); - Assert.That(() => conn.Open(), Throws.Exception.TypeOf()); - } - - [Test] - public void Constructor_with_null_connection_string() - { - var conn = new SpannerConnection(null); - Assert.That(conn.ConnectionString, Is.SameAs(string.Empty)); - Assert.That(() => conn.Open(), Throws.Exception.TypeOf()); - } - - [Test] - public void Constructor_with_empty_connection_string() - { - var conn = new NpgsqlConnection(""); - Assert.That(conn.ConnectionString, Is.SameAs(string.Empty)); - Assert.That(() => conn.Open(), Throws.Exception.TypeOf()); - } } \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionTests.cs index 30597b36..66b5ff2e 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionTests.cs @@ -13,6 +13,8 @@ // limitations under the License. using System.Data; +using System.Diagnostics.CodeAnalysis; +using System.Transactions; using Google.Cloud.Spanner.V1; using Google.Cloud.SpannerLib; using Google.Cloud.SpannerLib.MockServer; @@ -249,13 +251,330 @@ public async Task TestInvalidDatabase() } [Test] - public async Task TestConnectWithConnectionStringBuilder() + public void TestConnectWithConnectionStringBuilder() { - var builder = new SpannerConnectionStringBuilder(); - builder.DataSource = "projects/my-project/instances/my-instance/databases/my-database"; - builder.Host = Fixture.Host; - builder.Port = (uint) Fixture.Port; - builder.UsePlainText = true; + var builder = new SpannerConnectionStringBuilder + { + DataSource = "projects/my-project/instances/my-instance/databases/my-database", + Host = Fixture.Host, + Port = (uint) Fixture.Port, + UsePlainText = true + }; + using var connection = new SpannerConnection(builder); + Assert.That(connection.ConnectionString, Is.EqualTo(builder.ConnectionString)); + } + + [Test] + public void RequiredConnectionStringProperties() + { + using var connection = new SpannerConnection(); + Assert.Throws(() => connection.ConnectionString = "Host=localhost;Port=80"); + } + + [Test] + public void FailedConnectThenSucceed() + { + // Close all current pools to ensure that we get a fresh pool. + SpannerPool.CloseSpannerLib(); + // TODO: Make this a public property in the mock server. + const string detectDialectQuery = + "select option_value from information_schema.database_options where option_name='database_dialect'"; + Fixture.SpannerMock.AddOrUpdateStatementResult(detectDialectQuery, StatementResult.CreateException(new RpcException(new Status(StatusCode.NotFound, "Database not found")))); + using var conn = new SpannerConnection(); + conn.ConnectionString = ConnectionString; + var exception = Assert.Throws(() => conn.Open()); + Assert.That(exception.Code, Is.EqualTo(Code.NotFound)); + Assert.That(conn.State, Is.EqualTo(ConnectionState.Closed)); + + // Remove the error and retry. + Fixture.SpannerMock.AddOrUpdateStatementResult(detectDialectQuery, StatementResult.CreateResultSet(new List> + { + Tuple.Create(TypeCode.String, "option_value") + }, new List + { + new object[] { "GOOGLE_STANDARD_SQL" } + })); + conn.Open(); + Assert.That(conn.State, Is.EqualTo(ConnectionState.Open)); + } + + [Test] + [Ignore("Needs connect_timeout property")] + public void OpenTimeout() + { + // TODO: Add connect_timeout property. + var builder = new SpannerConnectionStringBuilder + { + Host = Fixture.Host, + Port = (uint) Fixture.Port, + UsePlainText = true, + DataSource = "projects/project1/instances/instance1/databases/database1", + //ConnectTimeout = TimeSpan.FromMicroseconds(1), + }; + using var connection = new SpannerConnection(); + connection.ConnectionString = builder.ConnectionString; + var exception = Assert.Throws(() => connection.Open()); + Assert.That(exception.ErrorCode, Is.EqualTo((int) Code.DeadlineExceeded)); + } + + [Test] + [Ignore("OpenAsync must be implemented")] + public async Task OpenCancel() + { + // Close all current pools to ensure that we get a fresh pool. + SpannerPool.CloseSpannerLib(); + Fixture.SpannerMock.AddOrUpdateExecutionTime(nameof(Fixture.SpannerMock.CreateSession), ExecutionTime.FromMillis(20, 0)); + var builder = new SpannerConnectionStringBuilder + { + Host = Fixture.Host, + Port = (uint) Fixture.Port, + UsePlainText = true, + DataSource = "projects/project1/instances/instance1/databases/database1", + }; + await using var connection = new SpannerConnection(); + connection.ConnectionString = builder.ConnectionString; + var tokenSource = new CancellationTokenSource(5); + // TODO: Implement actual async opening of connections + Assert.ThrowsAsync(async () => await connection.OpenAsync(tokenSource.Token)); + Assert.That(connection.State, Is.EqualTo(ConnectionState.Closed)); + } + + [Test] + public void DataSourceProperty() + { + using var conn = new SpannerConnection(); + Assert.That(conn.DataSource, Is.EqualTo(string.Empty)); + + var builder = new SpannerConnectionStringBuilder(ConnectionString); + + conn.ConnectionString = builder.ConnectionString; + Assert.That(conn.DataSource, Is.EqualTo("projects/p1/instances/i1/databases/d1")); + } + + [Test] + public void SettingConnectionStringWhileOpenThrows() + { + using var conn = new SpannerConnection(); + conn.ConnectionString = ConnectionString; + conn.Open(); + Assert.That(() => conn.ConnectionString = "", Throws.Exception.TypeOf()); + } + + [Test] + public void EmptyConstructor() + { + var conn = new SpannerConnection(); + Assert.That(conn.ConnectionTimeout, Is.EqualTo(15)); + Assert.That(conn.ConnectionString, Is.SameAs(string.Empty)); + Assert.That(() => conn.Open(), Throws.Exception.TypeOf()); + } + + [Test] + public void ConstructorWithNullConnectionString() + { + var conn = new SpannerConnection((string?) null); + Assert.That(conn.ConnectionString, Is.SameAs(string.Empty)); + Assert.That(() => conn.Open(), Throws.Exception.TypeOf()); + } + + [Test] + public void ConstructorWithEmptyConnectionString() + { + var conn = new SpannerConnection(""); + Assert.That(conn.ConnectionString, Is.SameAs(string.Empty)); + Assert.That(() => conn.Open(), Throws.Exception.TypeOf()); + } + + [Test] + public void SetConnectionStringToNull() + { + var conn = new SpannerConnection(ConnectionString); + conn.ConnectionString = null; + Assert.That(conn.ConnectionString, Is.SameAs(string.Empty)); + Assert.That(() => conn.Open(), Throws.Exception.TypeOf()); + } + + [Test] + public void SetConnectionStringToEmpty() + { + var conn = new SpannerConnection(ConnectionString); + conn.ConnectionString = ""; + Assert.That(conn.ConnectionString, Is.SameAs(string.Empty)); + Assert.That(() => conn.Open(), Throws.Exception.TypeOf()); + } + + [Test] + public async Task ChangeDatabase() + { + await using var conn = await OpenConnectionAsync(); + Assert.That(conn.Database, Is.EqualTo("projects/p1/instances/i1/databases/d1")); + conn.ChangeDatabase("template1"); + Assert.That(conn.Database, Is.EqualTo("projects/p1/instances/i1/databases/template1")); + } + + [Test] + public async Task ChangeDatabaseDoesNotAffectOtherConnections() + { + await using var conn1 = new SpannerConnection(ConnectionString); + await using var conn2 = new SpannerConnection(ConnectionString); + conn1.Open(); + conn1.ChangeDatabase("template1"); + Assert.That(conn1.Database, Is.EqualTo("projects/p1/instances/i1/databases/template1")); + + // Connection 2's database should not changed + conn2.Open(); + Assert.That(conn2.Database, Is.EqualTo("projects/p1/instances/i1/databases/d1")); + } + + [Test] + public void ChangeDatabaseOnClosedConnectionWorks() + { + using var conn = new SpannerConnection(ConnectionString); + Assert.That(conn.Database, Is.EqualTo("projects/p1/instances/i1/databases/d1")); + conn.ChangeDatabase("template1"); + Assert.That(conn.Database, Is.EqualTo("projects/p1/instances/i1/databases/template1")); + } + + [Test] + [Ignore("Must add search_path connection property in shared library first")] + public async Task SearchPath() + { + // TODO: Add search_path connection variable in shared library + await using var dataSource = CreateDataSource(csb => csb.SearchPath = "foo"); + await using var conn = await dataSource.OpenConnectionAsync() as SpannerConnection; + Assert.That(await conn!.ExecuteScalarAsync("SHOW VARIABLE search_path"), Contains.Substring("foo")); + } + + [Test] + public async Task SetOptions() + { + await using var dataSource = CreateDataSource(csb => csb.Options = "isolation_level=serializable;read_lock_mode=pessimistic"); + await using var conn = await dataSource.OpenConnectionAsync() as SpannerConnection; + + Assert.That(await conn!.ExecuteScalarAsync("SHOW VARIABLE isolation_level"), Is.EqualTo("Serializable")); + Assert.That(await conn!.ExecuteScalarAsync("SHOW VARIABLE read_lock_mode"), Is.EqualTo("PESSIMISTIC")); + } + + [Test] + public async Task ConnectorNotInitializedException() + { + var command = new SpannerCommand(); + command.CommandText = "SELECT 1"; + + for (var i = 0; i < 2; i++) + { + await using var connection = await OpenConnectionAsync(); + command.Connection = connection; + await using var tx = await connection.BeginTransactionAsync(); + await command.ExecuteScalarAsync(); + await tx.CommitAsync(); + } + } + + [Test] + public void ConnectionStateIsClosedWhenDisposed() + { + var c = new SpannerConnection(); + c.Dispose(); + Assert.That(c.State, Is.EqualTo(ConnectionState.Closed)); + } + + [Test] + public async Task ConcurrentReadersAllowed() + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand("SELECT 1", conn); + await using (await cmd.ExecuteReaderAsync()) + { + Assert.That(await conn.ExecuteScalarAsync("SELECT 1"), Is.EqualTo(1)); + } + } + [Test] + public async Task ManyOpenClose() + { + await using var dataSource = CreateDataSource(); + for (var i = 0; i < 256; i++) + { + await using var conn = await dataSource.OpenConnectionAsync(); + } + await using (var conn = dataSource.CreateConnection()) + { + await conn.OpenAsync(); + } + await using (var conn = dataSource.CreateConnection() as SpannerConnection) + { + await conn!.OpenAsync(); + Assert.That(await conn.ExecuteScalarAsync("SELECT 1"), Is.EqualTo(1)); + } + } + + [Test] + public async Task ManyOpenCloseWithTransaction() + { + await using var dataSource = CreateDataSource(); + for (var i = 0; i < 256; i++) + { + await using var conn = await dataSource.OpenConnectionAsync(); + await conn.BeginTransactionAsync(); + } + + await using (var conn = await dataSource.OpenConnectionAsync() as SpannerConnection) + { + Assert.That(await conn!.ExecuteScalarAsync("SELECT 1"), Is.EqualTo(1)); + } + } + + [Test] + public async Task RollbackOnClose() + { + await using var dataSource = CreateDataSource(); + await using (var conn = await dataSource.OpenConnectionAsync() as SpannerConnection) + { + await conn!.BeginTransactionAsync(); + await conn.ExecuteNonQueryAsync("SELECT 1"); + Assert.That(conn.HasTransaction); + } + await using (var conn = await dataSource.OpenConnectionAsync() as SpannerConnection) + { + Assert.False(conn!.HasTransaction); + } + } + + [Test] + // TODO: Enable once https://github.com/googleapis/go-sql-spanner/pull/571 has been merged + [Ignore("https://github.com/googleapis/go-sql-spanner/pull/571")] + public async Task ReadLargeString() + { + const string sql = "select large_value from my_table"; + var value = TestUtils.GenerateRandomString(10_000_000); + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet( + new V1.Type{Code = TypeCode.String}, "large_value", value)); + + await using var dataSource = CreateDataSource(); + await using var conn = await dataSource.OpenConnectionAsync() as SpannerConnection; + var got = await conn!.ExecuteScalarAsync(sql); + Assert.That(got, Is.EqualTo(value)); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task CloseDuringRead() + { + await using var dataSource = CreateDataSource(); + await using var conn = (await dataSource.OpenConnectionAsync() as SpannerConnection)!; + await using (var cmd = new SpannerCommand("SELECT 1", conn)) + await using (var reader = await cmd.ExecuteReaderAsync()) + { + reader.Read(); + conn.Close(); + Assert.That(conn.State, Is.EqualTo(ConnectionState.Closed)); + // Closing a SpannerConnection does not close the related readers. + Assert.False(reader.IsClosed); + } + + conn.Open(); + Assert.That(conn.State, Is.EqualTo(ConnectionState.Open)); + Assert.That(await conn.ExecuteScalarAsync("SELECT 1"), Is.EqualTo(1)); } } \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/TestUtils.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/TestUtils.cs new file mode 100644 index 00000000..24f9b859 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/TestUtils.cs @@ -0,0 +1,19 @@ +namespace Google.Cloud.Spanner.DataProvider.Tests; + +public static class TestUtils +{ + private const string Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + public static string GenerateRandomString(int length) + { + return new string(Enumerable.Repeat(Chars, length) + .Select(s => s[Random.Shared.Next(s.Length)]).ToArray()); + } + +} + +public enum PrepareOrNot +{ + Prepared, + NotPrepared +} diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs index 4b9ad3d6..f361658c 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs @@ -59,6 +59,10 @@ public SpannerCommand(string commandText, SpannerConnection connection) Connection = GaxPreconditions.CheckNotNull(connection, nameof(connection)); _commandText = GaxPreconditions.CheckNotNull(commandText, nameof(commandText)); } + + public SpannerCommand(string cmdText, SpannerConnection connection, SpannerTransaction? transaction) + : this(cmdText, connection) + => Transaction = transaction; internal SpannerCommand(SpannerConnection connection, Mutation mutation) { @@ -180,13 +184,18 @@ private Rows Execute(ExecuteSqlRequest.Types.QueryMode mode = ExecuteSqlRequest. } private Task ExecuteAsync(CancellationToken cancellationToken) + { + return ExecuteAsync(ExecuteSqlRequest.Types.QueryMode.Normal, cancellationToken); + } + + private Task ExecuteAsync(ExecuteSqlRequest.Types.QueryMode mode, CancellationToken cancellationToken) { CheckCommandStateForExecution(); if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } - return TranslateException(() => SpannerConnection.LibConnection!.ExecuteAsync(BuildStatement())); + return TranslateException(() => SpannerConnection.LibConnection!.ExecuteAsync(BuildStatement(mode))); } private void CheckCommandStateForExecution() @@ -237,6 +246,11 @@ public override void Prepare() Execute(ExecuteSqlRequest.Types.QueryMode.Plan); } + public override Task PrepareAsync(CancellationToken cancellationToken = default) + { + return ExecuteAsync(ExecuteSqlRequest.Types.QueryMode.Plan, cancellationToken); + } + protected override DbParameter CreateDbParameter() { return new SpannerParameter(); diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs index 3fe2c811..bf4964ff 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs @@ -36,7 +36,7 @@ public class SpannerConnection : DbConnection private SpannerConnectionStringBuilder? _connectionStringBuilder; [AllowNull] - public override string ConnectionString { + public sealed override string ConnectionString { get => _connectionString; set { @@ -122,6 +122,23 @@ internal Connection? LibConnection private SpannerTransaction? _transaction; + public SpannerConnection() + { + } + + public SpannerConnection(string? connectionString) + { + ConnectionString = connectionString; + } + + public SpannerConnection(SpannerConnectionStringBuilder connectionStringBuilder) + { + GaxPreconditions.CheckNotNull(connectionStringBuilder, nameof(connectionStringBuilder)); + connectionStringBuilder.CheckValid(); + _connectionStringBuilder = connectionStringBuilder; + _connectionString = connectionStringBuilder.ConnectionString; + } + protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) { return BeginTransaction(new TransactionOptions @@ -138,13 +155,16 @@ public DbTransaction BeginReadOnlyTransaction() }); } + /// + /// Start a new transaction using the given TransactionOptions. + /// + /// The options to use for the new transaction + /// The new transaction + /// If the connection has an active transaction public DbTransaction BeginTransaction(TransactionOptions transactionOptions) { EnsureOpen(); - if (_transaction != null) - { - throw new InvalidOperationException("This connection has a transaction."); - } + GaxPreconditions.CheckState(!HasTransaction, "This connection has a transaction."); _transaction = new SpannerTransaction(this, transactionOptions); return _transaction; } @@ -153,10 +173,42 @@ internal void ClearTransaction() { _transaction = null; } + + internal bool HasTransaction => _transaction != null; public override void ChangeDatabase(string databaseName) { - throw new NotImplementedException(); + GaxPreconditions.CheckNotNullOrEmpty(databaseName, nameof(databaseName)); + GaxPreconditions.CheckState(!HasTransaction, "Cannot change database when a transaction is open"); + if (_connectionStringBuilder == null) + { + ConnectionString = $"Data Source={databaseName}"; + return; + } + if (DatabaseName.TryParse(databaseName, allowUnparsed: false, out _)) + { + _connectionStringBuilder.DataSource = databaseName; + } + else + { + if (DatabaseName.TryParse(_connectionStringBuilder.DataSource, out var currentDatabase)) + { + _connectionStringBuilder.DataSource = $"projects/{currentDatabase.ProjectId}/instances/{currentDatabase.InstanceId}/databases/{databaseName}"; + } + else if (!string.IsNullOrEmpty(_connectionStringBuilder.Project) && !string.IsNullOrEmpty(_connectionStringBuilder.Instance)) + { + _connectionStringBuilder.Database = databaseName; + } + else + { + throw new ArgumentException($"Invalid database name: {databaseName}"); + } + } + if (_state == ConnectionState.Open) + { + Close(); + Open(); + } } public override void Close() diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnectionStringBuilder.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnectionStringBuilder.cs index 34c1b869..b00bca5c 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnectionStringBuilder.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnectionStringBuilder.cs @@ -115,9 +115,52 @@ public bool UsePlainText get => SpannerConnectionStringOption.UsePlainText.GetValue(this); set => SpannerConnectionStringOption.UsePlainText.SetValue(this, value); } + + /// + /// The time in milliseconds to wait for a connection before terminating the attempt and generating an error. + /// The default value is 15000 (15 seconds). + /// + [Category("Timeout")] + [Description("The time in milliseconds to wait for a connection before terminating the attempt and generating an error.")] + [DefaultValue(15000u)] + [DisplayName("Connection Timeout")] + public uint ConnectionTimeout + { + get => SpannerConnectionStringOption.ConnectionTimeout.GetValue(this); + set => SpannerConnectionStringOption.ConnectionTimeout.SetValue(this, value); + } + + /// + /// The time in milliseconds to wait for a command before terminating the attempt and generating an error. + /// The default value is 0, which means that the command should use the default timeout set by Spanner. + /// + [Category("Timeout")] + [Description("The time in milliseconds to wait for a command before terminating the attempt and generating an error.")] + [DefaultValue(0u)] + [DisplayName("Command Timeout")] + public uint CommandTimeout + { + get => SpannerConnectionStringOption.CommandTimeout.GetValue(this); + set => SpannerConnectionStringOption.CommandTimeout.SetValue(this, value); + } + + /// + /// The maximum time in milliseconds that a read/write transaction may take to execute. + /// The default value is 0, which means that there is no transaction timeout. + /// + [Category("Timeout")] + [Description("The maximum time in milliseconds that a read/write transaction may take to execute.")] + [DefaultValue(0u)] + [DisplayName("Transaction Timeout")] + public uint TransactionTimeout + { + get => SpannerConnectionStringOption.TransactionTimeout.GetValue(this); + set => SpannerConnectionStringOption.TransactionTimeout.SetValue(this, value); + } /// - /// The hostname or IP address of the Spanner server to connect to. + /// The default isolation level that should be used for transactions on connections created from this connection + /// string. /// [Category("Transaction")] [Description("The default isolation level to use for transactions on this connection.")] @@ -128,19 +171,31 @@ public IsolationLevel DefaultIsolationLevel get => SpannerConnectionStringOption.DefaultIsolationLevel.GetValue(this); set => SpannerConnectionStringOption.DefaultIsolationLevel.SetValue(this, value); } + + /// + /// The search_path that should be used by the connection. + /// + [Category("Options")] + [Description("The search path for this connection.")] + [DefaultValue("")] + [DisplayName("SearchPath")] + public string SearchPath + { + get => SpannerConnectionStringOption.SearchPath.GetValue(this); + set => SpannerConnectionStringOption.SearchPath.SetValue(this, value); + } /// - /// The time in seconds to wait for a connection before terminating the attempt and generating an error. - /// The default value is 15. + /// Any other options that should be set for the connection in the format key1=value1;key2=value2;... /// - [Category("Connection")] - [Description("The time in seconds to wait for a connection before terminating the attempt and generating an error.")] - [DefaultValue(15u)] - [DisplayName("Connection Timeout")] - public uint ConnectionTimeout + [Category("Options")] + [Description("Any additional options to set for the connection.")] + [DefaultValue("")] + [DisplayName("Options")] + public string Options { - get => SpannerConnectionStringOption.ConnectionTimeout.GetValue(this); - set => SpannerConnectionStringOption.ConnectionTimeout.SetValue(this, value); + get => SpannerConnectionStringOption.Options.GetValue(this); + set => SpannerConnectionStringOption.Options.SetValue(this, value); } /// @@ -264,6 +319,10 @@ internal string SpannerLibConnectionString { builder.Append(';').Append(option.SpannerLibKey).Append('=').Append(this[key]); } + else if (key == "Options") + { + builder.Append(';').Append(this[key]); + } } else { @@ -287,13 +346,21 @@ internal abstract class SpannerConnectionStringOption public static readonly SpannerConnectionStringReferenceOption Project; public static readonly SpannerConnectionStringReferenceOption Instance; public static readonly SpannerConnectionStringReferenceOption Database; + + // Timeout Options public static readonly SpannerConnectionStringValueOption ConnectionTimeout; + public static readonly SpannerConnectionStringValueOption CommandTimeout; + public static readonly SpannerConnectionStringValueOption TransactionTimeout; // SSL/TLS Options public static readonly SpannerConnectionStringValueOption UsePlainText; // Transaction Options public static readonly SpannerConnectionStringValueOption DefaultIsolationLevel; + + // Other options + public static readonly SpannerConnectionStringReferenceOption SearchPath; + public static readonly SpannerConnectionStringReferenceOption Options; public static SpannerConnectionStringOption? TryGetOptionForKey(string key) => SOptions.GetValueOrDefault(key); @@ -362,9 +429,21 @@ static SpannerConnectionStringOption() spannerLibKey: "", defaultValue: "")); + // Timeout Options AddOption(options, ConnectionTimeout = new( keys: ["Connection Timeout", "ConnectionTimeout", "Connect Timeout", "connect_timeout"], - defaultValue: 15u)); + spannerLibKey: "connect_timeout", + defaultValue: 15000u)); + + AddOption(options, CommandTimeout = new( + keys: ["Command Timeout", "CommandTimeout", "command_timeout", "statement_timeout"], + spannerLibKey: "statement_timeout", + defaultValue: 0u)); + + AddOption(options, TransactionTimeout = new( + keys: ["Transaction Timeout", "TransactionTimeout", "transaction_timeout"], + spannerLibKey: "transaction_timeout", + defaultValue: 0u)); // SSL/TLS Options AddOption(options, UsePlainText = new( @@ -375,6 +454,18 @@ static SpannerConnectionStringOption() AddOption(options, DefaultIsolationLevel = new( keys: ["DefaultIsolationLevel", "default_isolation_level"], defaultValue: IsolationLevel.Unspecified)); + + // Other options + AddOption(options, SearchPath = new( + keys: ["SearchPath", "search_path"], + spannerLibKey: "search_path", + defaultValue: "")); + + // Other options + AddOption(options, Options = new( + keys: ["Options"], + spannerLibKey: "", + defaultValue: "")); SOptions = options.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); } diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataSource.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataSource.cs index 162b7d0c..d85942a0 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataSource.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataSource.cs @@ -12,22 +12,40 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using System.Data.Common; +using System.Diagnostics.CodeAnalysis; +using Google.Api.Gax; namespace Google.Cloud.Spanner.DataProvider; public class SpannerDataSource : DbDataSource { - public override string ConnectionString { get; } + private readonly SpannerConnectionStringBuilder _connectionStringBuilder; + + [AllowNull] + public sealed override string ConnectionString => _connectionStringBuilder.ConnectionString; + public static SpannerDataSource Create(string connectionString) { - throw new NotImplementedException(); + GaxPreconditions.CheckNotNull(connectionString, nameof(connectionString)); + return Create(new SpannerConnectionStringBuilder(connectionString)); + } + + public static SpannerDataSource Create(SpannerConnectionStringBuilder connectionStringBuilder) + { + return new SpannerDataSource(connectionStringBuilder); + } + + private SpannerDataSource(SpannerConnectionStringBuilder connectionStringBuilder) + { + GaxPreconditions.CheckNotNull(connectionStringBuilder, nameof(connectionStringBuilder)); + connectionStringBuilder.CheckValid(); + _connectionStringBuilder = connectionStringBuilder; } protected override DbConnection CreateDbConnection() { - throw new NotImplementedException(); + return new SpannerConnection(_connectionStringBuilder); } } \ No newline at end of file From 6645b293f2fc8b63ac88252297434ae9ee59a6f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Wed, 29 Oct 2025 09:38:56 +0100 Subject: [PATCH 07/29] chore: use project references instead of package references Use project references where possible for now to simplify development. --- ...spanner-ado-net-specification-tests.csproj | 2 +- .../AbstractMockServerTests.cs | 5 ++ .../spanner-ado-net-tests/CommandTests.cs | 66 +++++++++++++++++++ .../spanner-ado-net-tests/ConnectionTests.cs | 10 +-- .../spanner-ado-net-tests.csproj | 2 +- drivers/spanner-ado-net/spanner-ado-net.sln | 30 +++++++++ .../spanner-ado-net/SpannerCommand.cs | 11 +++- .../spanner-ado-net/SpannerConnection.cs | 5 +- .../spanner-ado-net/spanner-ado-net.csproj | 7 +- .../MockSpannerServer.cs | 19 ++++++ 10 files changed, 143 insertions(+), 14 deletions(-) diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/spanner-ado-net-specification-tests.csproj b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/spanner-ado-net-specification-tests.csproj index 8f0930d8..905694fb 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/spanner-ado-net-specification-tests.csproj +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/spanner-ado-net-specification-tests.csproj @@ -14,7 +14,6 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -28,6 +27,7 @@ + diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs index 049297d1..fce31ec2 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs @@ -65,6 +65,11 @@ protected SpannerDataSource CreateDataSource() { return CreateDataSource(_ => { }); } + + protected SpannerDataSource CreateDataSource(string connectionString) + { + return CreateDataSource(csb => { csb.ConnectionString = connectionString; }); + } protected SpannerDataSource CreateDataSource(Action connectionStringBuilderAction) { diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs index 647f0724..b6df08cc 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs @@ -22,6 +22,8 @@ using Google.Cloud.Spanner.V1; using Google.Cloud.SpannerLib.MockServer; using Google.Protobuf.WellKnownTypes; +using Google.Rpc; +using Grpc.Core; namespace Google.Cloud.Spanner.DataProvider.Tests; @@ -269,6 +271,70 @@ public async Task Timeout() Assert.That(conn!.State, Is.EqualTo(ConnectionState.Open)); } + [Test] + [Ignore("Requires support for statement_timeout in the shared library")] + public async Task TimeoutAsync() + { + Fixture.SpannerMock.AddOrUpdateExecutionTime(nameof(Fixture.SpannerMock.ExecuteStreamingSql), ExecutionTime.FromMillis(10, 0)); + + await using var dataSource = CreateDataSource(csb => csb.CommandTimeout = 1); + await using var conn = await dataSource.OpenConnectionAsync() as SpannerConnection; + await using var cmd = new SpannerCommand("SELECT 1", conn!); + Assert.That(async () => await cmd.ExecuteScalarAsync(), + Throws.Exception + .TypeOf() + .With.InnerException.TypeOf()); + Assert.That(conn!.State, Is.EqualTo(ConnectionState.Open)); + } + + [Test] + public async Task TimeoutSwitchConnection() + { + var csb = new SpannerConnectionStringBuilder(ConnectionString); + Assert.That(csb.CommandTimeout, Is.EqualTo(0)); + + await using var dataSource1 = CreateDataSource(ConnectionString + ";CommandTimeout=100"); + await using var c1 = dataSource1.CreateConnection(); + await using var cmd = c1.CreateCommand(); + Assert.That(cmd.CommandTimeout, Is.EqualTo(100)); + await using var dataSource2 = CreateDataSource(ConnectionString + ";CommandTimeout=101"); + await using (var c2 = dataSource2.CreateConnection()) + { + cmd.Connection = c2; + Assert.That(cmd.CommandTimeout, Is.EqualTo(101)); + } + cmd.CommandTimeout = 102; + await using (var c2 = dataSource2.CreateConnection()) + { + cmd.Connection = c2; + Assert.That(cmd.CommandTimeout, Is.EqualTo(102)); + } + } + + [Test] + [Ignore("Requires support for cancel in the shared library")] + public async Task Cancel() + { + var sql = "insert into my_table (id, value) values (1, 'one')"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(1L)); + Fixture.SpannerMock.AddOrUpdateExecutionTime(nameof(Fixture.SpannerMock.ExecuteStreamingSql), ExecutionTime.FromMillis(50, 0)); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + + // ReSharper disable once AccessToDisposedClosure + var queryTask = Task.Run(() => cmd.ExecuteNonQuery()); + // Wait until the request is on the mock server. + Fixture.SpannerMock.WaitForRequestsToContain(message => message is ExecuteSqlRequest request && request.Sql == sql); + cmd.Cancel(); + Assert.That(async () => await queryTask, Throws + .TypeOf() + .With.InnerException.TypeOf() + .With.InnerException.Property(nameof(SpannerDbException.ErrorCode)).EqualTo(StatusCode.Cancelled) + ); + } + private void AddParameter(DbCommand command, string name, object? value) { var parameter = command.CreateParameter(); diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionTests.cs index 66b5ff2e..3f47c0f0 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionTests.cs @@ -14,7 +14,6 @@ using System.Data; using System.Diagnostics.CodeAnalysis; -using System.Transactions; using Google.Cloud.Spanner.V1; using Google.Cloud.SpannerLib; using Google.Cloud.SpannerLib.MockServer; @@ -302,14 +301,16 @@ public void FailedConnectThenSucceed() [Ignore("Needs connect_timeout property")] public void OpenTimeout() { - // TODO: Add connect_timeout property. + // Close all current pools to ensure that we get a fresh pool. + SpannerPool.CloseSpannerLib(); + Fixture.SpannerMock.AddOrUpdateExecutionTime(nameof(Fixture.SpannerMock.CreateSession), ExecutionTime.FromMillis(20, 0)); var builder = new SpannerConnectionStringBuilder { Host = Fixture.Host, Port = (uint) Fixture.Port, UsePlainText = true, DataSource = "projects/project1/instances/instance1/databases/database1", - //ConnectTimeout = TimeSpan.FromMicroseconds(1), + ConnectionTimeout = 1, }; using var connection = new SpannerConnection(); connection.ConnectionString = builder.ConnectionString; @@ -489,6 +490,7 @@ public async Task ConcurrentReadersAllowed() Assert.That(await conn.ExecuteScalarAsync("SELECT 1"), Is.EqualTo(1)); } } + [Test] public async Task ManyOpenClose() { @@ -541,8 +543,6 @@ public async Task RollbackOnClose() } [Test] - // TODO: Enable once https://github.com/googleapis/go-sql-spanner/pull/571 has been merged - [Ignore("https://github.com/googleapis/go-sql-spanner/pull/571")] public async Task ReadLargeString() { const string sql = "select large_value from my_table"; diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/spanner-ado-net-tests.csproj b/drivers/spanner-ado-net/spanner-ado-net-tests/spanner-ado-net-tests.csproj index f1e0a28c..31870103 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/spanner-ado-net-tests.csproj +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/spanner-ado-net-tests.csproj @@ -13,7 +13,6 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -29,6 +28,7 @@ + diff --git a/drivers/spanner-ado-net/spanner-ado-net.sln b/drivers/spanner-ado-net/spanner-ado-net.sln index bfaca2ca..7a24c1a4 100644 --- a/drivers/spanner-ado-net/spanner-ado-net.sln +++ b/drivers/spanner-ado-net/spanner-ado-net.sln @@ -10,6 +10,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spanner-ado-net-samples", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spanner-ado-net-benchmarks", "spanner-ado-net-benchmarks\spanner-ado-net-benchmarks.csproj", "{2C70D969-A8AA-440B-81D8-532C327F237E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spannerlib-dotnet-mockserver", "..\..\spannerlib\wrappers\spannerlib-dotnet\spannerlib-dotnet-mockserver\spannerlib-dotnet-mockserver.csproj", "{E690FD52-65CD-4F11-A56E-A7D3B8D7A190}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spannerlib-dotnet", "..\..\spannerlib\wrappers\spannerlib-dotnet\spannerlib-dotnet\spannerlib-dotnet.csproj", "{90663BC7-07FD-4089-9594-94D61D9F63A2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spannerlib-dotnet-grpc-impl", "..\..\spannerlib\wrappers\spannerlib-dotnet\spannerlib-dotnet-grpc-impl\spannerlib-dotnet-grpc-impl.csproj", "{8759AB44-DEC6-4E78-B64D-2EE4A403DFE1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spannerlib-dotnet-native-impl", "..\..\spannerlib\wrappers\spannerlib-dotnet\spannerlib-dotnet-native-impl\spannerlib-dotnet-native-impl.csproj", "{85711FA3-547A-4B8E-AA23-95A6108F0DF8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spannerlib-dotnet-grpc-v1", "..\..\spannerlib\wrappers\spannerlib-dotnet\spannerlib-dotnet-grpc-v1\spannerlib-dotnet-grpc-v1.csproj", "{DF3C6D80-EB58-4189-A15A-9D3FEA233AF0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,5 +46,25 @@ Global {2C70D969-A8AA-440B-81D8-532C327F237E}.Debug|Any CPU.Build.0 = Debug|Any CPU {2C70D969-A8AA-440B-81D8-532C327F237E}.Release|Any CPU.ActiveCfg = Release|Any CPU {2C70D969-A8AA-440B-81D8-532C327F237E}.Release|Any CPU.Build.0 = Release|Any CPU + {E690FD52-65CD-4F11-A56E-A7D3B8D7A190}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E690FD52-65CD-4F11-A56E-A7D3B8D7A190}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E690FD52-65CD-4F11-A56E-A7D3B8D7A190}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E690FD52-65CD-4F11-A56E-A7D3B8D7A190}.Release|Any CPU.Build.0 = Release|Any CPU + {90663BC7-07FD-4089-9594-94D61D9F63A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90663BC7-07FD-4089-9594-94D61D9F63A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90663BC7-07FD-4089-9594-94D61D9F63A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90663BC7-07FD-4089-9594-94D61D9F63A2}.Release|Any CPU.Build.0 = Release|Any CPU + {8759AB44-DEC6-4E78-B64D-2EE4A403DFE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8759AB44-DEC6-4E78-B64D-2EE4A403DFE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8759AB44-DEC6-4E78-B64D-2EE4A403DFE1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8759AB44-DEC6-4E78-B64D-2EE4A403DFE1}.Release|Any CPU.Build.0 = Release|Any CPU + {85711FA3-547A-4B8E-AA23-95A6108F0DF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85711FA3-547A-4B8E-AA23-95A6108F0DF8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85711FA3-547A-4B8E-AA23-95A6108F0DF8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85711FA3-547A-4B8E-AA23-95A6108F0DF8}.Release|Any CPU.Build.0 = Release|Any CPU + {DF3C6D80-EB58-4189-A15A-9D3FEA233AF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF3C6D80-EB58-4189-A15A-9D3FEA233AF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF3C6D80-EB58-4189-A15A-9D3FEA233AF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF3C6D80-EB58-4189-A15A-9D3FEA233AF0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs index f361658c..65b8de8a 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs @@ -32,8 +32,15 @@ public class SpannerCommand : DbCommand private string _commandText = ""; [AllowNull] public override string CommandText { get => _commandText; set => _commandText = value ?? ""; } - - public override int CommandTimeout { get; set; } + + private int? _timeout; + + public override int CommandTimeout + { + get => _timeout ?? (int) SpannerConnection.DefaultCommandTimeout; + set => _timeout = value; + } + public override CommandType CommandType { get; set; } = CommandType.Text; public override UpdateRowSource UpdatedRowSource { get; set; } protected override DbConnection? DbConnection { get; set; } diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs index bf4964ff..ba4ae0a4 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs @@ -119,6 +119,8 @@ internal Connection? LibConnection return _libConnection; } } + + internal uint DefaultCommandTimeout => _connectionStringBuilder?.CommandTimeout ?? 0; private SpannerTransaction? _transaction; @@ -297,7 +299,8 @@ private void AssertClosed() protected override DbCommand CreateDbCommand() { - return new SpannerCommand(this); + var cmd = new SpannerCommand(this); + return cmd; } protected override DbBatch CreateDbBatch() diff --git a/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj b/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj index d27cbaf3..8d70844c 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj +++ b/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj @@ -19,10 +19,9 @@ Alpha version: Not for production use - - - - + + + diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/MockSpannerServer.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/MockSpannerServer.cs index 129449af..dec7afbe 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/MockSpannerServer.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/MockSpannerServer.cs @@ -14,6 +14,7 @@ using System.Collections; using System.Collections.Concurrent; +using System.Diagnostics; using Google.Cloud.Spanner.Admin.Database.V1; using Google.Cloud.Spanner.Common.V1; using Google.Cloud.Spanner.V1; @@ -385,6 +386,24 @@ internal void AbortNextStatement() public IEnumerable Requests => new List(_requests).AsReadOnly(); + public bool WaitForRequestsToContain(Func predicate) + { + return WaitForRequestsToContain(predicate, new TimeSpan(5 * TimeSpan.TicksPerSecond)); + } + + public bool WaitForRequestsToContain(Func predicate, TimeSpan timeout) + { + var stopwatch = new Stopwatch(); + while (stopwatch.Elapsed < timeout) + { + if (Requests.Any(predicate)) + { + return true; + } + } + return false; + } + public IEnumerable Contexts => new List(_contexts).AsReadOnly(); public IEnumerable Headers => new List(_headers).AsReadOnly(); From 6c2d8d0cb19970eb4a54649870901590265b80a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Wed, 29 Oct 2025 15:28:00 +0100 Subject: [PATCH 08/29] test: add more tests --- .github/workflows/ado-net-tests.yml | 38 +- .../AbstractMockServerTests.cs | 29 + .../CommandParameterTests.cs | 218 +++++++ .../spanner-ado-net-tests/CommandTests.cs | 553 +++++++++++++++++- .../spanner-ado-net-tests/ConnectionTests.cs | 21 - .../spanner-ado-net-tests/DataSourceTests.cs | 296 ++++++++++ .../SpannerParameterCollectionTests.cs | 303 ++++++++++ .../spanner-ado-net/SpannerCommand.cs | 111 +++- .../spanner-ado-net/SpannerCommandBuilder.cs | 6 +- .../spanner-ado-net/SpannerDataAdapter.cs | 4 + .../spanner-ado-net/SpannerDataReader.cs | 16 +- .../spanner-ado-net/SpannerDataSource.cs | 25 +- .../spanner-ado-net/SpannerDbException.cs | 16 +- .../spanner-ado-net/SpannerParameter.cs | 36 +- .../SpannerParameterCollection.cs | 61 +- .../spanner-ado-net/SpannerTransaction.cs | 10 +- parser/statement_parser.go | 17 + spannerlib/api/batch_test.go | 38 ++ spannerlib/api/connection.go | 2 +- spannerlib/grpc-server/server.go | 19 +- spannerlib/grpc-server/server_test.go | 79 ++- .../wrappers/spannerlib-dotnet/build.sh | 2 + .../MockSpannerServer.cs | 28 +- .../SpannerMockServerFixture.cs | 9 + testutil/mocked_inmem_server.go | 6 +- 25 files changed, 1864 insertions(+), 79 deletions(-) create mode 100644 drivers/spanner-ado-net/spanner-ado-net-tests/CommandParameterTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-tests/DataSourceTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-tests/SpannerParameterCollectionTests.cs diff --git a/.github/workflows/ado-net-tests.yml b/.github/workflows/ado-net-tests.yml index 9092004f..11749d17 100644 --- a/.github/workflows/ado-net-tests.yml +++ b/.github/workflows/ado-net-tests.yml @@ -14,12 +14,46 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: + - name: Checkout code + uses: actions/checkout@v4 - name: Install dotnet uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ matrix.dotnet-version }} - - name: Checkout code - uses: actions/checkout@v4 + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + # Install compilers for cross-compiling between operating systems. + - name: Install compilers + run: | + echo "$RUNNER_OS" + if [ "$RUNNER_OS" == "Windows" ]; then + echo "Windows does not yet support cross compiling" + elif [ "$RUNNER_OS" == "macOS" ]; then + brew tap SergioBenitez/osxct + brew install x86_64-unknown-linux-gnu + brew install mingw-w64 + else + sudo apt-get update + sudo apt install -y g++-mingw-w64-x86-64 gcc-mingw-w64-x86-64 + sudo apt-get install -y gcc-arm-linux-gnueabihf + fi + shell: bash + - name: Build the .NET wrapper + working-directory: spannerlib/wrappers/spannerlib-dotnet + run: | + echo "$RUNNER_OS" + ./build.sh + shell: bash + - name: Restore .NET wrapper dependencies + run: dotnet restore + working-directory: spannerlib/wrappers/spannerlib-dotnet + shell: bash + - name: Build .NET wrapper + run: dotnet build --no-restore -c Release + working-directory: spannerlib/wrappers/spannerlib-dotnet + shell: bash - name: spanner-ado-net-tests working-directory: drivers/spanner-ado-net/spanner-ado-net-tests run: dotnet test --verbosity normal diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs index fce31ec2..82b66979 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Data.Common; using Google.Cloud.SpannerLib.MockServer; namespace Google.Cloud.Spanner.DataProvider.Tests; @@ -28,17 +29,22 @@ static AbstractMockServerTests() protected SpannerMockServerFixture Fixture; + protected SpannerDataSource DataSource { get; private set; } + + protected string ConnectionString => $"Host={Fixture.Host};Port={Fixture.Port};Data Source=projects/p1/instances/i1/databases/d1;UsePlainText=true"; [OneTimeSetUp] public void Setup() { Fixture = new SpannerMockServerFixture(); + DataSource = SpannerDataSource.Create(ConnectionString); } [OneTimeTearDown] public void Teardown() { + DataSource.Dispose(); Fixture.Dispose(); } @@ -108,3 +114,26 @@ public static async Task ExecuteNonQueryAsync( return await command.ExecuteScalarAsync(cancellationToken); } } + +public static class SpannerCommandExtensions +{ + internal static void AddParameter(this SpannerCommand command, string name, object? value) + { + var parameter = command.CreateParameter(); + parameter.ParameterName = name; + parameter.Value = value; + command.Parameters.Add(parameter); + } +} + +public static class BatchExtensions +{ + internal static void AddSpannerBatchCommand(this DbBatch batch, string sql) + { + var command = new SpannerBatchCommand + { + CommandText = sql + }; + batch.BatchCommands.Add(command); + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/CommandParameterTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/CommandParameterTests.cs new file mode 100644 index 00000000..d1ec8775 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/CommandParameterTests.cs @@ -0,0 +1,218 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Data; +using Google.Cloud.Spanner.Admin.Database.V1; +using Google.Cloud.Spanner.V1; +using Google.Cloud.SpannerLib.MockServer; +using TypeCode = Google.Cloud.Spanner.V1.TypeCode; + +namespace Google.Cloud.Spanner.DataProvider.Tests; + +public class CommandParameterTests : AbstractMockServerTests +{ + [Test] + [TestCase(CommandBehavior.Default)] + [TestCase(CommandBehavior.SequentialAccess)] + public async Task InputAndOutputParameters(CommandBehavior behavior) + { + const string sql = "SELECT @c-1 AS c, @a+2 AS b"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + new List>([ + Tuple.Create(TypeCode.Int64, "c"), + Tuple.Create(TypeCode.Int64, "b"), + ]), + new List([[3, 5]]))); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(sql, conn); + cmd.AddParameter("a", 3); + var b = new SpannerParameter { ParameterName = "b", Direction = ParameterDirection.Output }; + cmd.Parameters.Add(b); + var c = new SpannerParameter { ParameterName = "c", Direction = ParameterDirection.InputOutput, Value = 4 }; + cmd.Parameters.Add(c); + await using (await cmd.ExecuteReaderAsync(behavior)) + { + // TODO: Enable if we decide to support output parameters in the same way as npgsql. + // Assert.That(b.Value, Is.EqualTo(5)); + // Assert.That(c.Value, Is.EqualTo(3)); + } + var request = Fixture.SpannerMock.Requests.Single(r => r is ExecuteSqlRequest { Sql: sql }) as ExecuteSqlRequest; + Assert.That(request, Is.Not.Null); + Assert.That(request.Params.Fields.Count, Is.EqualTo(3)); + Assert.That(request.Params.Fields["a"].StringValue, Is.EqualTo("3")); + Assert.That(request.Params.Fields["b"].HasNullValue); + Assert.That(request.Params.Fields["c"].StringValue, Is.EqualTo("4")); + } + + [Test] + public async Task SendWithoutType([Values(PrepareOrNot.NotPrepared, PrepareOrNot.Prepared)] PrepareOrNot prepare) + { + const string sql = "select cast(@p as timestamp)"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet( + new V1.Type{Code = TypeCode.Timestamp}, "p", "2025-10-30T10:00:00.000000000Z")); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(sql, conn); + cmd.AddParameter("p", "2025-10-30T10:00:00Z"); + if (prepare == PrepareOrNot.Prepared) + { + await cmd.PrepareAsync(); + } + + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + Assert.That(reader.GetValue(0), Is.EqualTo(new DateTime(2025, 10, 30, 10, 0, 0, DateTimeKind.Utc))); + + var request = Fixture.SpannerMock.Requests.First(r => r is ExecuteSqlRequest { Sql: sql }) as ExecuteSqlRequest; + Assert.That(request, Is.Not.Null); + Assert.That(request.Params.Fields.Count, Is.EqualTo(1)); + Assert.That(request.Params.Fields["p"].StringValue, Is.EqualTo("2025-10-30T10:00:00Z")); + Assert.That(request.ParamTypes.Count, Is.EqualTo(0)); + + var expectedCount = prepare == PrepareOrNot.Prepared ? 2 : 1; + Assert.That(Fixture.SpannerMock.Requests.Count(r => r is ExecuteSqlRequest { Sql: sql }), Is.EqualTo(expectedCount)); + } + + [Test] + public async Task PositionalParameter() + { + // Set the database dialect to PostgreSQL to enable the use of PostgreSQL-style positional parameters. + Fixture.SpannerMock.AddDialectResult(DatabaseDialect.Postgresql); + const string sql = "SELECT $1"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet( + new V1.Type{Code = TypeCode.Int64}, "c", 8L)); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(sql, conn); + cmd.Parameters.Add(new SpannerParameter { Value = 8 }); + Assert.That(await cmd.ExecuteScalarAsync(), Is.EqualTo(8)); + + var request = Fixture.SpannerMock.Requests.Single(r => r is ExecuteSqlRequest { Sql: sql }) as ExecuteSqlRequest; + Assert.That(request, Is.Not.Null); + Assert.That(request.Params.Fields.Count, Is.EqualTo(1)); + Assert.That(request.Params.Fields["p1"].StringValue, Is.EqualTo("8")); + Assert.That(request.ParamTypes.Count, Is.EqualTo(0)); + } + + [Test] + public async Task UnreferencedNamedParameterIsIgnored() + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand("SELECT 1", conn); + cmd.AddParameter("not_used", 8); + Assert.That(await cmd.ExecuteScalarAsync(), Is.EqualTo(1)); + + var request = Fixture.SpannerMock.Requests.Single(r => r is ExecuteSqlRequest { Sql: "SELECT 1" }) as ExecuteSqlRequest; + Assert.That(request, Is.Not.Null); + Assert.That(request.Params.Fields.Count, Is.EqualTo(1)); + Assert.That(request.Params.Fields["not_used"].StringValue, Is.EqualTo("8")); + Assert.That(request.ParamTypes.Count, Is.EqualTo(0)); + } + + [Test] + public async Task UnreferencedPositionalParameterIsIgnored() + { + // Set the database dialect to PostgreSQL to enable the use of PostgreSQL-style positional parameters. + Fixture.SpannerMock.AddDialectResult(DatabaseDialect.Postgresql); + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand("SELECT 1", conn); + cmd.Parameters.Add(new SpannerParameter { Value = 8 }); + Assert.That(await cmd.ExecuteScalarAsync(), Is.EqualTo(1)); + + var request = Fixture.SpannerMock.Requests.Single(r => r is ExecuteSqlRequest { Sql: "SELECT 1" }) as ExecuteSqlRequest; + Assert.That(request, Is.Not.Null); + Assert.That(request.Params.Fields.Count, Is.EqualTo(1)); + Assert.That(request.Params.Fields["p1"].StringValue, Is.EqualTo("8")); + Assert.That(request.ParamTypes.Count, Is.EqualTo(0)); + } + + [Test] + public void ParameterName() + { + var command = new SpannerCommand(); + + // Add parameters. + command.Parameters.Add(new SpannerParameter{ ParameterName = "@Parameter1", DbType = DbType.Boolean, Value = true }); + command.Parameters.Add(new SpannerParameter{ ParameterName = "@Parameter2", DbType = DbType.Int32, Value = 1 }); + command.Parameters.Add(new SpannerParameter{ ParameterName = "Parameter3", DbType = DbType.DateTime, Value = DBNull.Value }); + command.Parameters.Add(new SpannerParameter{ ParameterName = "Parameter4", DbType = DbType.Binary, Value = DBNull.Value }); + + var parameter = command.Parameters["@Parameter1"]; + Assert.That(parameter, Is.Not.Null); + command.Parameters[0].Value = 1; + + Assert.That(command.Parameters["@Parameter1"].ParameterName, Is.EqualTo("@Parameter1")); + Assert.That(command.Parameters["@Parameter2"].ParameterName, Is.EqualTo("@Parameter2")); + Assert.That(command.Parameters["Parameter3"].ParameterName, Is.EqualTo("Parameter3")); + Assert.That(command.Parameters["Parameter4"].ParameterName, Is.EqualTo("Parameter4")); + + Assert.That(command.Parameters[0].ParameterName, Is.EqualTo("@Parameter1")); + Assert.That(command.Parameters[1].ParameterName, Is.EqualTo("@Parameter2")); + Assert.That(command.Parameters[2].ParameterName, Is.EqualTo("Parameter3")); + Assert.That(command.Parameters[3].ParameterName, Is.EqualTo("Parameter4")); + + // Verify that the '@' is stripped before being sent to Spanner. + var statement = command.BuildStatement(); + Assert.That(statement, Is.Not.Null); + Assert.That(statement.Params.Fields.Count, Is.EqualTo(4)); + Assert.That(statement.Params.Fields["Parameter1"].StringValue, Is.EqualTo("1")); + Assert.That(statement.Params.Fields["Parameter2"].StringValue, Is.EqualTo("1")); + Assert.That(statement.Params.Fields["Parameter3"].HasNullValue); + Assert.That(statement.Params.Fields["Parameter4"].HasNullValue); + + Assert.That(statement.ParamTypes.Count, Is.EqualTo(4)); + Assert.That(statement.ParamTypes["Parameter1"].Code, Is.EqualTo(TypeCode.Bool)); + Assert.That(statement.ParamTypes["Parameter2"].Code, Is.EqualTo(TypeCode.Int64)); + Assert.That(statement.ParamTypes["Parameter3"].Code, Is.EqualTo(TypeCode.Timestamp)); + Assert.That(statement.ParamTypes["Parameter4"].Code, Is.EqualTo(TypeCode.Bytes)); + } + + [Test] + public async Task SameParamMultipleTimes() + { + const string sql = "SELECT @p1, @p1"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + new List>([Tuple.Create(TypeCode.Int64, "p1"), Tuple.Create(TypeCode.Int64, "p1")]), + new List([[8, 8]]))); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(sql, conn); + cmd.AddParameter("@p1", 8); + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + Assert.That(reader[0], Is.EqualTo(8)); + Assert.That(reader[1], Is.EqualTo(8)); + + var request = Fixture.SpannerMock.Requests.Single(r => r is ExecuteSqlRequest { Sql: sql }) as ExecuteSqlRequest; + Assert.That(request, Is.Not.Null); + Assert.That(request.Params.Fields.Count, Is.EqualTo(1)); + Assert.That(request.Params.Fields["p1"].StringValue, Is.EqualTo("8")); + Assert.That(request.ParamTypes.Count, Is.EqualTo(0)); + } + + [Test] + public async Task ParameterMustBeSet() + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand("SELECT @p1::TEXT", conn); + cmd.Parameters.Add(new SpannerParameter{ ParameterName = "@p1" }); + + Assert.That(async () => await cmd.ExecuteReaderAsync(), + Throws.Exception + .TypeOf() + .With.Message.EqualTo("Parameter @p1 has no value")); + } + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs index b6df08cc..7bee948b 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs @@ -12,18 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using System.Data; using System.Data.Common; -using System.Linq; +using System.Diagnostics.CodeAnalysis; using System.Text; using System.Text.Json; -using System.Threading.Tasks; using Google.Cloud.Spanner.V1; using Google.Cloud.SpannerLib.MockServer; using Google.Protobuf.WellKnownTypes; -using Google.Rpc; using Grpc.Core; +using Status = Grpc.Core.Status; +using TypeCode = Google.Cloud.Spanner.V1.TypeCode; namespace Google.Cloud.Spanner.DataProvider.Tests; @@ -171,6 +170,31 @@ public async Task TestAllParameterTypes() Assert.That(fields["p23"].ListValue.Values[0].NumberValue, Is.EqualTo(3.14f)); Assert.That(fields["p23"].ListValue.Values[1].HasNullValue, Is.True); } + + [Test] + public async Task TestExecuteNonQueryWithSelect() + { + const string sql = "select * from my_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSelect1ResultSet()); + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(conn); + cmd.CommandText = sql; + + var result = await cmd.ExecuteNonQueryAsync(); + Assert.That(result, Is.EqualTo(-1)); + } + + [Test] + public async Task TestExecuteNonQueryWithError() + { + const string sql = "select * from my_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateException(new RpcException(new Status(StatusCode.NotFound, "Table not found")))); + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(conn); + cmd.CommandText = sql; + + Assert.ThrowsAsync(async () => await cmd.ExecuteNonQueryAsync()); + } [Test] [TestCase(new[] { true }, TestName = "SingleQuery")] @@ -334,7 +358,528 @@ public async Task Cancel() .With.InnerException.Property(nameof(SpannerDbException.ErrorCode)).EqualTo(StatusCode.Cancelled) ); } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task CloseConnection() + { + await using var conn = await OpenConnectionAsync(); + await using (var cmd = new SpannerCommand("SELECT 1", conn)) + await using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection)) + { + while (reader.Read()) + { + } + } + Assert.That(conn.State, Is.EqualTo(ConnectionState.Closed)); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task CloseDuringRead() + { + await using var dataSource = CreateDataSource(); + await using var conn = (await dataSource.OpenConnectionAsync() as SpannerConnection)!; + await using (var cmd = new SpannerCommand("SELECT 1", conn)) + await using (var reader = await cmd.ExecuteReaderAsync()) + { + reader.Read(); + conn.Close(); + Assert.That(conn.State, Is.EqualTo(ConnectionState.Closed)); + // Closing a SpannerConnection does not close the related readers. + Assert.False(reader.IsClosed); + } + + conn.Open(); + Assert.That(conn.State, Is.EqualTo(ConnectionState.Open)); + Assert.That(await conn.ExecuteScalarAsync("SELECT 1"), Is.EqualTo(1)); + } + + [Test] + public async Task CloseConnectionWithException() + { + const string sql = "select * from non_existing_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateException(new RpcException(new Status(StatusCode.NotFound, "Table not found")))); + + await using var conn = await OpenConnectionAsync(); + await using (var cmd = new SpannerCommand(sql, conn)) + { + Assert.That(() => cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection), + Throws.Exception.TypeOf()); + } + Assert.That(conn.State, Is.EqualTo(ConnectionState.Closed)); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task SingleRow([Values(PrepareOrNot.NotPrepared, PrepareOrNot.Prepared)] PrepareOrNot prepare) + { + const string sql = "SELECT 1, 2 UNION SELECT 3, 4"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + new List>([Tuple.Create(TypeCode.Int64, "c1"), Tuple.Create(TypeCode.Int64, "c2")]), + new List([[1L, 2L], [3L, 4L]]))); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(sql, conn); + if (prepare == PrepareOrNot.Prepared) + { + cmd.Prepare(); + } + + await using var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SingleRow); + Assert.That(() => reader.GetInt32(0), Throws.Exception.TypeOf()); + Assert.That(reader.Read(), Is.True); + Assert.That(reader.GetInt32(0), Is.EqualTo(1)); + Assert.That(reader.Read(), Is.False); + } + + [Test] + public async Task CommandTextNotSet() + { + await using var conn = await OpenConnectionAsync(); + await using (var cmd = new SpannerCommand()) + { + cmd.Connection = conn; + Assert.That(cmd.ExecuteNonQueryAsync, Throws.Exception.TypeOf()); + cmd.CommandText = null; + Assert.That(cmd.ExecuteNonQueryAsync, Throws.Exception.TypeOf()); + cmd.CommandText = ""; + } + + await using (var cmd = conn.CreateCommand()) + { + Assert.That(cmd.ExecuteNonQueryAsync, Throws.Exception.TypeOf()); + } + } + + [Test] + public async Task ExecuteScalar() + { + const string sql = "select name from my_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + new List>([Tuple.Create(TypeCode.String, "name")]), + new List([]))); + + await using var conn = await OpenConnectionAsync(); + await using var command = new SpannerCommand(sql, conn); + Assert.That(command.ExecuteScalarAsync, Is.Null); + + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + new List>([Tuple.Create(TypeCode.String, "name")]), + new List([[DBNull.Value]]))); + Assert.That(command.ExecuteScalarAsync, Is.EqualTo(DBNull.Value)); + + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + new List>([Tuple.Create(TypeCode.String, "name")]), + new List([["X1"], ["X2"]]))); + Assert.That(command.ExecuteScalarAsync, Is.EqualTo("X1")); + } + + [Test] + public async Task ExecuteNonQuery() + { + const string insertOneRow = "insert into my_table (name) values ('Test')"; + Fixture.SpannerMock.AddOrUpdateStatementResult(insertOneRow, StatementResult.CreateUpdateCount(1L)); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = conn.CreateCommand(); + + // Insert one row + cmd.CommandText = insertOneRow; + Assert.That(cmd.ExecuteNonQueryAsync, Is.EqualTo(1)); + + // Insert two rows in one batch using a SQL string that contains two statements. + // TODO: Enable when SpannerLib supports SQL strings with multiple statements. + // cmd.CommandText = $"{insertOneRow}; {insertOneRow}"; + // Assert.That(cmd.ExecuteNonQueryAsync, Is.EqualTo(2)); + + // Execute a large SQL string. + var value = TestUtils.GenerateRandomString(10_000_000); + cmd.CommandText = $"insert into my_table (name) values ('{value}')"; + Fixture.SpannerMock.AddOrUpdateStatementResult(cmd.CommandText, StatementResult.CreateUpdateCount(1L)); + Assert.That(cmd.ExecuteNonQueryAsync, Is.EqualTo(1)); + } + + [Test] + public async Task Dispose() + { + await using var conn = await OpenConnectionAsync(); + var cmd = new SpannerCommand("SELECT 1", conn); + cmd.Dispose(); + Assert.That(() => cmd.ExecuteScalarAsync(), Throws.Exception.TypeOf()); + Assert.That(() => cmd.ExecuteNonQueryAsync(), Throws.Exception.TypeOf()); + Assert.That(() => cmd.ExecuteReaderAsync(), Throws.Exception.TypeOf()); + Assert.That(() => cmd.PrepareAsync(), Throws.Exception.TypeOf()); + } + + [Test] + public async Task DisposeDesNotCloseReader() + { + await using var conn = await OpenConnectionAsync(); + var cmd = new SpannerCommand("SELECT 1", conn); + await using var reader1 = await cmd.ExecuteReaderAsync(); + cmd.Dispose(); + cmd = new SpannerCommand("SELECT 1", conn); + await using var reader2 = await cmd.ExecuteReaderAsync(); + Assert.That(reader2, Is.Not.Null); + Assert.That(reader1.IsClosed, Is.False); + Assert.That(await reader1.ReadAsync(), Is.True); + } + + [Test] + [TestCase(CommandBehavior.Default)] + [TestCase(CommandBehavior.SequentialAccess)] + public async Task StatementMappedOutputParameters(CommandBehavior behavior) + { + const string sql = "select 3, 4 as param1, 5 as param2, 6"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + new List>([ + Tuple.Create(TypeCode.Int64, "c1"), + Tuple.Create(TypeCode.Int64, "param1"), + Tuple.Create(TypeCode.Int64, "param2"), + Tuple.Create(TypeCode.Int64, "c2")]), + new List([[3, 4, 5, 6]]))); + + await using var conn = await OpenConnectionAsync(); + var command = new SpannerCommand(sql, conn); + + var p = new SpannerParameter + { + ParameterName = "param2", + Direction = ParameterDirection.Output, + Value = -1, + DbType = DbType.Int64, + }; + command.Parameters.Add(p); + + p = new SpannerParameter + { + ParameterName = "param1", + Direction = ParameterDirection.Output, + Value = -1, + DbType = DbType.Int64, + }; + command.Parameters.Add(p); + + p = new SpannerParameter + { + ParameterName = "p", + Direction = ParameterDirection.Output, + Value = -1, + DbType = DbType.Int64, + }; + command.Parameters.Add(p); + + await using var reader = await command.ExecuteReaderAsync(behavior); + + // TODO: Enable if we decide to support output parameters in the same way as npgsql. + // Assert.That(command.Parameters["param1"].Value, Is.EqualTo(4)); + // Assert.That(command.Parameters["param2"].Value, Is.EqualTo(5)); + + await reader.ReadAsync(); + + Assert.That(reader.GetInt32(0), Is.EqualTo(3)); + Assert.That(reader.GetInt32(1), Is.EqualTo(4)); + Assert.That(reader.GetInt32(2), Is.EqualTo(5)); + Assert.That(reader.GetInt32(3), Is.EqualTo(6)); + } + + [Test] + public async Task TableDirect() + { + const string sql = "select * from my_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + new List>([Tuple.Create(TypeCode.String, "name")]), + new List([["foo"]]))); + await using var conn = await OpenConnectionAsync(); + + await using var cmd = new SpannerCommand("my_table", conn) { CommandType = CommandType.TableDirect }; + await using var reader = await cmd.ExecuteReaderAsync(); + Assert.That(await reader.ReadAsync(), Is.True); + Assert.That(reader["name"], Is.EqualTo("foo")); + } + + [Test] + public async Task InvalidUtf8() + { + const string sql = "SELECT 'abc\uD801\uD802d'"; + Fixture.SpannerMock.AddOrUpdateStatementResult("SELECT 'abc��d'", StatementResult.CreateResultSet( + new List>([Tuple.Create(TypeCode.String, "c")]), + new List([["abc��d"]]))); + + await using var dataSource = CreateDataSource(); + await using var conn = await dataSource.OpenConnectionAsync() as SpannerConnection; + var value = await conn!.ExecuteScalarAsync(sql); + Assert.That(value, Is.EqualTo("abc��d")); + } + + [Test] + public async Task UseAcrossConnectionChange([Values(PrepareOrNot.Prepared, PrepareOrNot.NotPrepared)] PrepareOrNot prepare) + { + await using var conn1 = await OpenConnectionAsync(); + await using var conn2 = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand("SELECT 1", conn1); + if (prepare == PrepareOrNot.Prepared) + { + await cmd.PrepareAsync(); + } + cmd.Connection = conn2; + if (prepare == PrepareOrNot.Prepared) + { + await cmd.PrepareAsync(); + } + Assert.That(await cmd.ExecuteScalarAsync(), Is.EqualTo(1)); + } + + [Test] + public async Task CreateCommandBeforeConnectionOpen() + { + await using var conn = new SpannerConnection(ConnectionString); + var cmd = new SpannerCommand("SELECT 1", conn); + conn.Open(); + Assert.That(await cmd.ExecuteScalarAsync(), Is.EqualTo(1)); + } + + [Test] + public void ConnectionNotSetThrows() + { + var cmd = new SpannerCommand { CommandText = "SELECT 1" }; + Assert.That(() => cmd.ExecuteScalarAsync(), Throws.Exception.TypeOf()); + } + + [Test] + public void ConnectionNotOpenTrows() + { + using var conn = new SpannerConnection(ConnectionString); + var cmd = new SpannerCommand("SELECT 1", conn); + Assert.That(() => cmd.ExecuteScalarAsync(), Throws.Exception.TypeOf()); + } + + [Test] + public async Task ExecuteNonQueryThrowsSpannerDbException([Values] bool async) + { + const string sql = "insert into my_table (ref) values (1) returning ref"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateException(new RpcException(new Status(StatusCode.FailedPrecondition, "Foreign key constraint violation")))); + await using var conn = await OpenConnectionAsync(); + + var ex = async + ? Assert.ThrowsAsync(async () => await conn.ExecuteNonQueryAsync(sql)) + : Assert.Throws(() => conn.ExecuteNonQuery(sql)); + Assert.That(ex!.Status.Code, Is.EqualTo((int) StatusCode.FailedPrecondition)); + } + + [Test] + public async Task ExecuteScalarThrowsSpannerDbException([Values] bool async) + { + const string sql = "insert into my_table (ref) values (1) returning ref"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateException(new RpcException(new Status(StatusCode.FailedPrecondition, "Foreign key constraint violation")))); + await using var conn = await OpenConnectionAsync(); + + var ex = async + ? Assert.ThrowsAsync(async () => await conn.ExecuteScalarAsync(sql)) + : Assert.Throws(() => conn.ExecuteScalar(sql)); + Assert.That(ex!.Status.Code, Is.EqualTo((int) StatusCode.FailedPrecondition)); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task ExecuteReaderThrowsSpannerDbException([Values] bool async) + { + const string sql = "insert into my_table (ref) values (1) returning ref"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateException(new RpcException(new Status(StatusCode.FailedPrecondition, "Foreign key constraint violation")))); + await using var conn = await OpenConnectionAsync(); + await using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + + var ex = async + ? Assert.ThrowsAsync(async () => await cmd.ExecuteReaderAsync()) + : Assert.Throws(() => cmd.ExecuteReader()); + Assert.That(ex!.Status.Code, Is.EqualTo((int) StatusCode.FailedPrecondition)); + } + [Test] + public void CommandIsNotRecycled() + { + const string sql = "select @p1"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "p1", 8L)); + + using var conn = new SpannerConnection(ConnectionString); + var cmd1 = conn.CreateCommand(); + cmd1.CommandText = sql; + var tx = conn.BeginTransaction(); + cmd1.Transaction = tx; + AddParameter(cmd1, "p1", 8); + _ = cmd1.ExecuteScalar(); + cmd1.Dispose(); + + var cmd2 = conn.CreateCommand(); + Assert.That(cmd2, Is.Not.SameAs(cmd1)); + Assert.That(cmd2.CommandText, Is.Empty); + Assert.That(cmd2.CommandType, Is.EqualTo(CommandType.Text)); + Assert.That(cmd2.Transaction, Is.Null); + Assert.That(cmd2.Parameters, Is.Empty); + } + + [Test] + public async Task ManyParameters([Values(PrepareOrNot.NotPrepared, PrepareOrNot.Prepared)] PrepareOrNot prepare) + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(conn); + var sb = new StringBuilder($"INSERT INTO my_table (some_column) VALUES "); + var numParams = ushort.MaxValue; + for (var i = 0; i < numParams; i++) + { + var paramName = "p" + i; + AddParameter(cmd, paramName, i); + if (i > 0) + sb.Append(", "); + sb.Append($"(@{paramName})"); + } + cmd.CommandText = sb.ToString(); + Fixture.SpannerMock.AddOrUpdateStatementResult(cmd.CommandText, StatementResult.CreateUpdateCount(numParams)); + + if (prepare == PrepareOrNot.Prepared) + { + await cmd.PrepareAsync(); + } + await cmd.ExecuteNonQueryAsync(); + } + + [Test] + [Ignore("Requires multi-statement support in SpannerLib")] + public async Task ManyParametersAcrossStatements() + { + var result = StatementResult.CreateSelect1ResultSet(); + // Create a command with 1000 statements which have 70 params each + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(conn); + var paramIndex = 0; + var sb = new StringBuilder(); + for (var statementIndex = 0; statementIndex < 1000; statementIndex++) + { + if (statementIndex > 0) + sb.Append("; "); + var statement = new StringBuilder(); + statement.Append("SELECT "); + var startIndex = paramIndex; + var endIndex = paramIndex + 70; + for (; paramIndex < endIndex; paramIndex++) + { + var paramName = "p" + paramIndex; + AddParameter(cmd, paramName, paramIndex); + if (paramIndex > startIndex) + statement.Append(", "); + statement.Append('@'); + statement.Append(paramName); + } + sb.Append(statement); + Fixture.SpannerMock.AddOrUpdateStatementResult(statement.ToString(), result); + } + cmd.CommandText = sb.ToString(); + await cmd.ExecuteNonQueryAsync(); + } + + [Test] + public async Task SameCommandDifferentParamValues() + { + const string sql = "select @p"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "p", 8L)); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(sql, conn); + AddParameter(cmd, "p", 8); + await cmd.ExecuteNonQueryAsync(); + var request = Fixture.SpannerMock.Requests.First(r => r is ExecuteSqlRequest { Sql: sql }) as ExecuteSqlRequest; + Assert.That(request, Is.Not.Null); + Assert.That(request.Params.Fields["p"].StringValue, Is.EqualTo("8")); + + Fixture.SpannerMock.ClearRequests(); + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "p", 9L)); + + cmd.Parameters[0].Value = 9; + Assert.That(await cmd.ExecuteScalarAsync(), Is.EqualTo(9)); + request = Fixture.SpannerMock.Requests.First(r => r is ExecuteSqlRequest { Sql: sql }) as ExecuteSqlRequest; + Assert.That(request, Is.Not.Null); + Assert.That(request.Params.Fields["p"].StringValue, Is.EqualTo("9")); + } + + [Test] + public async Task SameCommandDifferentParamInstances() + { + const string sql = "select @p"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "p", 8L)); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(sql, conn); + AddParameter(cmd, "p", 8); + await cmd.ExecuteNonQueryAsync(); + var request = Fixture.SpannerMock.Requests.First(r => r is ExecuteSqlRequest { Sql: sql }) as ExecuteSqlRequest; + Assert.That(request, Is.Not.Null); + Assert.That(request.Params.Fields["p"].StringValue, Is.EqualTo("8")); + + Fixture.SpannerMock.ClearRequests(); + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "p", 9L)); + + cmd.Parameters.RemoveAt(0); + AddParameter(cmd, "p", 9); + Assert.That(await cmd.ExecuteScalarAsync(), Is.EqualTo(9)); + request = Fixture.SpannerMock.Requests.First(r => r is ExecuteSqlRequest { Sql: sql }) as ExecuteSqlRequest; + Assert.That(request, Is.Not.Null); + Assert.That(request.Params.Fields["p"].StringValue, Is.EqualTo("9")); + } + + [Test] + public async Task CancelWhileReadingFromLongRunningQuery() + { + const string sql = "select id from my_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet( + new V1.Type{Code = TypeCode.Int64}, + "id", + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20])); + await using var conn = await OpenConnectionAsync(); + + await using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + + using (var cts = new CancellationTokenSource()) + await using (var reader = await cmd.ExecuteReaderAsync(cts.Token)) + { + Assert.ThrowsAsync(async () => + { + var i = 0; + while (await reader.ReadAsync(cts.Token)) + { + i++; + if (i == 10) + { + await cts.CancelAsync(); + } + } + }); + } + + cmd.CommandText = "SELECT 1"; + Assert.That(await cmd.ExecuteScalarAsync(CancellationToken.None), Is.EqualTo(1)); + } + + [Test] + public async Task CompletedTransactionThrows([Values] bool commit) + { + await using var conn = await OpenConnectionAsync(); + await using var tx = await conn.BeginTransactionAsync(); + await using var cmd = conn.CreateCommand(); + + if (commit) + { + await tx.CommitAsync(); + } + else + { + await tx.RollbackAsync(); + } + Assert.Throws(() => cmd.Transaction = tx); + } + private void AddParameter(DbCommand command, string name, object? value) { var parameter = command.CreateParameter(); diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionTests.cs index 3f47c0f0..dddbc070 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/ConnectionTests.cs @@ -556,25 +556,4 @@ public async Task ReadLargeString() Assert.That(got, Is.EqualTo(value)); } - [Test] - [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] - public async Task CloseDuringRead() - { - await using var dataSource = CreateDataSource(); - await using var conn = (await dataSource.OpenConnectionAsync() as SpannerConnection)!; - await using (var cmd = new SpannerCommand("SELECT 1", conn)) - await using (var reader = await cmd.ExecuteReaderAsync()) - { - reader.Read(); - conn.Close(); - Assert.That(conn.State, Is.EqualTo(ConnectionState.Closed)); - // Closing a SpannerConnection does not close the related readers. - Assert.False(reader.IsClosed); - } - - conn.Open(); - Assert.That(conn.State, Is.EqualTo(ConnectionState.Open)); - Assert.That(await conn.ExecuteScalarAsync("SELECT 1"), Is.EqualTo(1)); - } - } \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/DataSourceTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/DataSourceTests.cs new file mode 100644 index 00000000..02fc56f5 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/DataSourceTests.cs @@ -0,0 +1,296 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Data; +using System.Data.Common; +using System.Diagnostics.CodeAnalysis; +using Google.Cloud.SpannerLib.MockServer; + +namespace Google.Cloud.Spanner.DataProvider.Tests; + +public class DataSourceTests : AbstractMockServerTests +{ + [Test] + public async Task CreateConnection() + { + await using var dataSource = SpannerDataSource.Create(ConnectionString); + await using var connection = dataSource.CreateConnection(); + Assert.That(connection.State, Is.EqualTo(ConnectionState.Closed)); + + await connection.OpenAsync(); + Assert.That(await connection.ExecuteScalarAsync("SELECT 1"), Is.EqualTo(1)); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task OpenConnection([Values] bool async) + { + await using var dataSource = SpannerDataSource.Create(ConnectionString); + await using var connection = async + ? await dataSource.OpenConnectionAsync() + : dataSource.OpenConnection(); + + Assert.That(connection.State, Is.EqualTo(ConnectionState.Open)); + Assert.That(await connection.ExecuteScalarAsync("SELECT 1"), Is.EqualTo(1)); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task ExecuteScalarOnConnectionlessCommand([Values] bool async) + { + await using var dataSource = SpannerDataSource.Create(ConnectionString); + await using var command = dataSource.CreateCommand(); + command.CommandText = "SELECT 1"; + + if (async) + { + Assert.That(await command.ExecuteScalarAsync(), Is.EqualTo(1)); + } + else + { + Assert.That(command.ExecuteScalar(), Is.EqualTo(1)); + } + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task ExecuteNonQueryOnConnectionlessCommand([Values] bool async) + { + await using var dataSource = SpannerDataSource.Create(ConnectionString); + await using var command = dataSource.CreateCommand(); + command.CommandText = "SELECT 1"; + + if (async) + { + Assert.That(await command.ExecuteNonQueryAsync(), Is.EqualTo(-1)); + } + else + { + Assert.That(command.ExecuteNonQuery(), Is.EqualTo(-1)); + } + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task ExecuteReaderOnConnectionlessCommand([Values] bool async) + { + await using var dataSource = SpannerDataSource.Create(ConnectionString); + await using var command = dataSource.CreateCommand(); + command.CommandText = "SELECT 1"; + + await using var reader = async ? await command.ExecuteReaderAsync() : command.ExecuteReader(); + Assert.That(reader.Read()); + Assert.That(reader.GetInt32(0), Is.EqualTo(1)); + } + + [Ignore("Requires support for batching queries")] + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task ExecuteScalarOnConnectionlessBatch([Values] bool async) + { + await using var dataSource = SpannerDataSource.Create(ConnectionString); + await using var batch = dataSource.CreateBatch(); + batch.AddSpannerBatchCommand("SELECT 1"); + batch.AddSpannerBatchCommand("SELECT 2"); + + if (async) + { + Assert.That(await batch.ExecuteScalarAsync(), Is.EqualTo(1)); + } + else + { + Assert.That(batch.ExecuteScalar(), Is.EqualTo(1)); + } + } + + [Ignore("Requires support for batching queries")] + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task ExecuteNonQueryOnConnectionlessBatch([Values] bool async) + { + await using var dataSource = SpannerDataSource.Create(ConnectionString); + await using var batch = dataSource.CreateBatch(); + batch.AddSpannerBatchCommand("SELECT 1"); + batch.AddSpannerBatchCommand("SELECT 2"); + + if (async) + { + Assert.That(await batch.ExecuteNonQueryAsync(), Is.EqualTo(-1)); + } + else + { + Assert.That(batch.ExecuteNonQuery(), Is.EqualTo(-1)); + } + } + + [Ignore("Requires support for batching queries")] + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task ExecuteReaderOnConnectionlessBatch([Values] bool async) + { + await using var dataSource = SpannerDataSource.Create(ConnectionString); + await using var batch = dataSource.CreateBatch(); + batch.AddSpannerBatchCommand("SELECT 1"); + batch.AddSpannerBatchCommand("SELECT 2"); + + await using var reader = async ? await batch.ExecuteReaderAsync() : batch.ExecuteReader(); + Assert.That(reader.Read()); + Assert.That(reader.GetInt32(0), Is.EqualTo(1)); + Assert.That(reader.NextResult()); + Assert.That(reader.Read()); + Assert.That(reader.GetInt32(0), Is.EqualTo(2)); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task ExecuteScalarOnConnectionlessDmlBatch([Values] bool async) + { + const string sql = "insert into my_table (id) values (default)"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(1)); + + await using var dataSource = SpannerDataSource.Create(ConnectionString); + await using var batch = dataSource.CreateBatch(); + batch.AddSpannerBatchCommand(sql); + batch.AddSpannerBatchCommand(sql); + + if (async) + { + Assert.ThrowsAsync(async () => await batch.ExecuteScalarAsync()); + } + else + { + Assert.Throws(() => batch.ExecuteScalar()); + } + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task ExecuteNonQueryOnConnectionlessDmlBatch([Values] bool async) + { + const string sql = "insert into my_table (id) values (default)"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(1)); + + await using var dataSource = SpannerDataSource.Create(ConnectionString); + await using var batch = dataSource.CreateBatch(); + batch.AddSpannerBatchCommand(sql); + batch.AddSpannerBatchCommand(sql); + + if (async) + { + Assert.That(await batch.ExecuteNonQueryAsync(), Is.EqualTo(2)); + } + else + { + Assert.That(batch.ExecuteNonQuery(), Is.EqualTo(2)); + } + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task ExecuteReaderOnConnectionlessDmlBatch([Values] bool async) + { + const string sql = "insert into my_table (id) values (default)"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(1)); + + await using var dataSource = SpannerDataSource.Create(ConnectionString); + await using var batch = dataSource.CreateBatch(); + batch.AddSpannerBatchCommand(sql); + batch.AddSpannerBatchCommand(sql); + + if (async) + { + Assert.ThrowsAsync(async () => await batch.ExecuteReaderAsync()); + } + else + { + Assert.Throws(() => batch.ExecuteReader()); + } + } + + [Test] + public void Dispose() + { + var dataSource = SpannerDataSource.Create(ConnectionString); + var connection1 = dataSource.OpenConnection(); + var connection2 = dataSource.OpenConnection(); + connection1.Close(); + + // SpannerDataSource does not contain any state, so disposing it is a no-op. + dataSource.Dispose(); + using var connection3 = dataSource.OpenConnection(); + + connection2.Close(); + } + + [Test] + public async Task DisposeAsync() + { + var dataSource = SpannerDataSource.Create(ConnectionString); + var connection1 = await dataSource.OpenConnectionAsync(); + var connection2 = await dataSource.OpenConnectionAsync(); + await connection1.CloseAsync(); + + // SpannerDataSource does not contain any state, so disposing it is a no-op. + await dataSource.DisposeAsync(); + await using var connection3 = await dataSource.OpenConnectionAsync(); + + await connection2.CloseAsync(); + } + + [Test] + public async Task CannotAccessConnectionTransactionOnDataSourceCommand() + { + await using var command = DataSource.CreateCommand(); + + Assert.That(() => command.Connection, Throws.Exception.TypeOf()); + Assert.That(() => command.Connection = null, Throws.Exception.TypeOf()); + Assert.That(() => command.Transaction, Throws.Exception.TypeOf()); + Assert.That(() => command.Transaction = null, Throws.Exception.TypeOf()); + + Assert.That(() => command.Prepare(), Throws.Exception.TypeOf()); + Assert.That(() => command.PrepareAsync(), Throws.Exception.TypeOf()); + } + + [Test] + public async Task CannotAccessConnectionTransactionOnDataSourceBatch() + { + await using var batch = DataSource.CreateBatch(); + + Assert.That(() => batch.Connection, Throws.Exception.TypeOf()); + Assert.That(() => batch.Connection = null, Throws.Exception.TypeOf()); + Assert.That(() => batch.Transaction, Throws.Exception.TypeOf()); + Assert.That(() => batch.Transaction = null, Throws.Exception.TypeOf()); + + Assert.That(() => batch.Prepare(), Throws.Exception.TypeOf()); + Assert.That(() => batch.PrepareAsync(), Throws.Exception.TypeOf()); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task AsDbDataSource([Values] bool async) + { + await using DbDataSource dataSource = SpannerDataSource.Create(ConnectionString); + await using var connection = async + ? await dataSource.OpenConnectionAsync() + : dataSource.OpenConnection(); + Assert.That(connection.State, Is.EqualTo(ConnectionState.Open)); + + await using var command = dataSource.CreateCommand("SELECT 1"); + + Assert.That(async + ? await command.ExecuteScalarAsync() + : command.ExecuteScalar(), Is.EqualTo(1)); + } + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/SpannerParameterCollectionTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/SpannerParameterCollectionTests.cs new file mode 100644 index 00000000..156bb151 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/SpannerParameterCollectionTests.cs @@ -0,0 +1,303 @@ +using System.Data; +using System.Data.Common; +using System.Diagnostics.CodeAnalysis; + +namespace Google.Cloud.Spanner.DataProvider.Tests; + +public class SpannerParameterCollectionTests : AbstractMockServerTests +{ + [Test] + public void CanOnlyAddSpannerParameterOrValidValue() + { + using var command = new SpannerCommand(); + + Assert.DoesNotThrow(() => command.Parameters.Add("hello")); + + Assert.That(() => command.Parameters.Add(new SomeOtherDbParameter()), Throws.Exception.TypeOf()); + Assert.That(() => command.Parameters.Add(null!), Throws.Exception.TypeOf()); + } + + [Test] + public void Clear() + { + var p = new SpannerParameter(); + var c1 = new SpannerCommand(); + var c2 = new SpannerCommand(); + c1.Parameters.Add(p); + Assert.That(c1.Parameters.Count, Is.EqualTo(1)); + Assert.That(c2.Parameters.Count, Is.EqualTo(0)); + c1.Parameters.Clear(); + Assert.That(c1.Parameters.Count, Is.EqualTo(0)); + c2.Parameters.Add(p); + Assert.That(c1.Parameters.Count, Is.EqualTo(0)); + Assert.That(c2.Parameters.Count, Is.EqualTo(1)); + } + + [Test] + public void ParameterRename() + { + using var command = new SpannerCommand(); + for (var i = 0; i < 10; i++) + { + command.AddParameter($"p{i + 1:00}", $"String parameter value {i + 1}"); + } + Assert.That(command.Parameters["p03"].ParameterName, Is.EqualTo("p03")); + + // Rename a parameter. + command.Parameters["p03"].ParameterName = "a_new_name"; + Assert.That(command.Parameters.IndexOf("a_new_name"), Is.GreaterThanOrEqualTo(0)); + } + + [Test] + public void UnnamedParameterRename() + { + using var command = new SpannerCommand(); + + for (var i = 0; i < 3; i++) + { + for (var j = 0; j < 10; j++) + { + // Create and add an unnamed parameter before renaming it + var parameter = command.CreateParameter(); + command.Parameters.Add(parameter); + parameter.ParameterName = $"{j}"; + } + Assert.That(command.Parameters["3"].ParameterName, Is.EqualTo("3")); + command.Parameters.Clear(); + } + } + + [Test] + public void RemoveDuplicateParameter() + { + using var command = new SpannerCommand(); + var count = 10; + for (var i = 0; i < count; i++) + { + command.AddParameter($"p{i + 1:00}", $"String parameter value {i + 1}"); + } + + Assert.That(command.Parameters["p02"].ParameterName, Is.EqualTo("p02")); + // Add uppercased version of the same parameter. + command.AddParameter("P02", "String parameter value 2"); + // Remove the original parameter by its name. + command.Parameters.Remove(command.Parameters["p02"]); + + // Test whether we can still find the last added parameter, and if its index is correctly shifted in the lookup. + Assert.That(command.Parameters.IndexOf("p02"), Is.EqualTo(count - 1)); + Assert.That(command.Parameters.IndexOf("P02"), Is.EqualTo(count - 1)); + // And finally test whether other parameters were also correctly shifted. + Assert.That(command.Parameters.IndexOf("p03"), Is.EqualTo(1)); + Assert.That(command.Parameters.IndexOf("p03") == 1); + } + + [Test] + public void RemoveParameter() + { + using var command = new SpannerCommand(); + var count = 10; + for (var i = 0; i < count; i++) + { + command.AddParameter($"p{i + 1:00}", $"String parameter value {i + 1}"); + } + + // Remove the parameter by its name + command.Parameters.Remove(command.Parameters["p02"]); + + // Make sure we cannot find it, also not case insensitively. + Assert.That(command.Parameters.IndexOf("p02"), Is.EqualTo(-1)); + Assert.That(command.Parameters.IndexOf("P02"), Is.EqualTo(-1)); + } + + [Test] + public void RemoveCaseDifferingParameter() + { + var count = 10; + // Add two parameters that only differ in casing. + using var command = new SpannerCommand(); + command.AddParameter("PP0", 1); + command.AddParameter("Pp0", 1); + for (var i = 0; i < count - 2; i++) + { + command.AddParameter($"pp{i}", i); + } + + // Removing Pp0. + command.Parameters.RemoveAt(1); + + // Matching on parameter name always first prefers case-sensitive matching, so we match entry 1 ('pp0'). + Assert.That(command.Parameters.IndexOf("pp0"), Is.EqualTo(1)); + // Exact match to PP0. + Assert.That(command.Parameters.IndexOf("PP0"), Is.EqualTo(0)); + // Case-insensitive match to PP0. + Assert.That(command.Parameters.IndexOf("Pp0"), Is.EqualTo(0)); + } + + [Test] + public void CorrectIndexReturnedForDuplicateParameterName() + { + const int count = 10; + using var command = new SpannerCommand(); + for (var i = 0; i < count; i++) + { + command.AddParameter($"parameter{i + 1:00}", $"String parameter value {i + 1}"); + } + Assert.That(command.Parameters["parameter02"].ParameterName, Is.EqualTo("parameter02")); + + // Add an upper-case version of one of the parameters. + command.AddParameter("Parameter02", "String parameter value 2"); + + // Insert another case-insensitive before the original. + command.Parameters.Insert(0, new SpannerParameter { ParameterName = "ParameteR02", Value = "String parameter value 2" }); + + // Try to find the exact index. + Assert.That(command.Parameters.IndexOf("parameter02"), Is.EqualTo(2)); + Assert.That(command.Parameters.IndexOf("Parameter02"), Is.EqualTo(command.Parameters.Count - 1)); + Assert.That(command.Parameters.IndexOf("ParameteR02"), Is.EqualTo(0)); + // This name does not exist so we expect the first case-insensitive match to be returned. + Assert.That(command.Parameters.IndexOf("ParaMeteR02"), Is.EqualTo(0)); + + // And finally test whether other parameters were also correctly shifted. + Assert.That(command.Parameters.IndexOf("parameter03"), Is.EqualTo(3)); + } + + [Test] + public void FindsCaseInsensitiveLookups() + { + const int count = 10; + using var command = new SpannerCommand(); + var parameters = command.Parameters; + for (var i = 0; i < count; i++) + { + parameters.Add(new SpannerParameter{ ParameterName = $"p{i}", Value = i }); + } + Assert.That(command.Parameters.IndexOf("P1"), Is.EqualTo(1)); + } + + [Test] + public void FindsCaseSensitiveLookups() + { + const int count = 10; + using var command = new SpannerCommand(); + var parameters = command.Parameters; + for (var i = 0; i < count; i++) + { + parameters.Add(new SpannerParameter{ ParameterName = $"p{i}", Value = i}); + } + Assert.That(command.Parameters.IndexOf("p1"), Is.EqualTo(1)); + } + + [Test] + public void ThrowsOnIndexerMismatch() + { + const int count = 10; + using var command = new SpannerCommand(); + var parameters = command.Parameters; + for (var i = 0; i < count; i++) + { + parameters.Add(new SpannerParameter{ ParameterName = $"p{i}", Value = i}); + } + + Assert.DoesNotThrow(() => + { + command.Parameters["p1"] = new SpannerParameter("p1", 1); + command.Parameters["p1"] = new SpannerParameter("P1", 1); + }); + + Assert.Throws(() => + { + command.Parameters["p1"] = new SpannerParameter("p2", 1); + }); + } + + [Test] + public void PositionalParameterLookupReturnsFirstMatch() + { + const int count = 10; + using var command = new SpannerCommand(); + var parameters = command.Parameters; + for (var i = 0; i < count; i++) + { + parameters.Add(new SpannerParameter("", i)); + } + Assert.That(command.Parameters.IndexOf(""), Is.EqualTo(0)); + } + + [Test] + public void MultiplePositionsSameInstanceIsAllowed() + { + using var cmd = new SpannerCommand(); + cmd.CommandText = "SELECT $1, $2"; + var p = new SpannerParameter("", "Hello world"); + cmd.Parameters.Add(p); + Assert.DoesNotThrow(() => cmd.Parameters.Add(p)); + } + + [Test] + public void IndexOfFallsBackToFirstInsensitiveMatch([Values] bool manyParams) + { + const int count = 10; + using var command = new SpannerCommand(); + var parameters = command.Parameters; + + parameters.Add(new SpannerParameter("foo", 8)); + parameters.Add(new SpannerParameter("bar", 8)); + parameters.Add(new SpannerParameter("BAR", 8)); + + if (manyParams) + { + for (var i = 0; i < count; i++) + { + parameters.Add(new SpannerParameter($"p{i}", i)); + } + } + Assert.That(parameters.IndexOf("Bar"), Is.EqualTo(1)); + } + + [Test] + public void IndexOfPrefersCaseSensitiveMatch([Values] bool manyParams) + { + const int count = 10; + using var command = new SpannerCommand(); + var parameters = command.Parameters; + + parameters.Add(new SpannerParameter("FOO", 8)); + parameters.Add(new SpannerParameter("foo", 8)); + + if (manyParams) + { + for (var i = 0; i < count; i++) + { + parameters.Add(new SpannerParameter($"p{i}", i)); + } + } + Assert.That(parameters.IndexOf("foo"), Is.EqualTo(1)); + } + + [Test] + public void CloningSucceeds() + { + const int count = 10; + var command = new SpannerCommand(); + for (var i = 0; i < count; i++) + { + command.Parameters.Add(new SpannerParameter()); + } + Assert.DoesNotThrow(() => command.Clone()); + } + + class SomeOtherDbParameter : DbParameter + { + public override void ResetDbType() {} + + public override DbType DbType { get; set; } + public override ParameterDirection Direction { get; set; } + public override bool IsNullable { get; set; } + [AllowNull] public override string ParameterName { get; set; } = ""; + [AllowNull] public override string SourceColumn { get; set; } = ""; + public override object? Value { get; set; } + public override bool SourceColumnNullMapping { get; set; } + public override int Size { get; set; } + } + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs index 65b8de8a..85e5b1a5 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs @@ -26,7 +26,7 @@ namespace Google.Cloud.Spanner.DataProvider; -public class SpannerCommand : DbCommand +public class SpannerCommand : DbCommand, ICloneable { private SpannerConnection SpannerConnection => (SpannerConnection)Connection!; @@ -37,15 +37,30 @@ public class SpannerCommand : DbCommand public override int CommandTimeout { - get => _timeout ?? (int) SpannerConnection.DefaultCommandTimeout; + get => _timeout ?? (int?) SpannerConnection?.DefaultCommandTimeout ?? 0; set => _timeout = value; } public override CommandType CommandType { get; set; } = CommandType.Text; - public override UpdateRowSource UpdatedRowSource { get; set; } + + public override UpdateRowSource UpdatedRowSource { get; set; } = UpdateRowSource.Both; protected override DbConnection? DbConnection { get; set; } protected override DbParameterCollection DbParameterCollection { get; } = new SpannerParameterCollection(); - protected override DbTransaction? DbTransaction { get; set; } + + SpannerTransaction? _transaction; + protected override DbTransaction? DbTransaction + { + get => _transaction; + set + { + var tx = (SpannerTransaction?)value; + + if (tx is { IsCompleted: true }) + throw new InvalidOperationException("Transaction is already completed"); + _transaction = tx; + } + } + public override bool DesignTimeVisible { get; set; } private bool HasTransaction => DbTransaction is SpannerTransaction; @@ -53,6 +68,8 @@ public override int CommandTimeout public TransactionOptions.Types.ReadOnly? SingleUseReadOnlyTransactionOptions { get; set; } public RequestOptions? RequestOptions { get; set; } + + private bool _disposed; public SpannerCommand() {} @@ -77,6 +94,20 @@ internal SpannerCommand(SpannerConnection connection, Mutation mutation) _mutation = mutation; } + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _disposed = true; + } + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(SpannerCommand)); + } + } + public override void Cancel() { // TODO: Implement in Spanner lib @@ -89,9 +120,15 @@ internal ExecuteSqlRequest BuildStatement(ExecuteSqlRequest.Types.QueryMode mode var spannerParams = ((SpannerParameterCollection)DbParameterCollection).CreateSpannerParams(); var queryParams = spannerParams.Item1; var paramTypes = spannerParams.Item2; + var sql = CommandText; + if (CommandType == CommandType.TableDirect) + { + // TODO: Quote the table name + sql = $"select * from {sql}"; + } var statement = new ExecuteSqlRequest { - Sql = CommandText, + Sql = sql, Params = queryParams, RequestOptions = RequestOptions, QueryMode = mode, @@ -156,7 +193,7 @@ private Mutation BuildMutation() write.Columns.Add(name); } - values.Values.Add(spannerParameter.ConvertToProto()); + values.Values.Add(spannerParameter.ConvertToProto(spannerParameter)); } else { @@ -208,13 +245,14 @@ private Task ExecuteAsync(ExecuteSqlRequest.Types.QueryMode mode, Cancella private void CheckCommandStateForExecution() { GaxPreconditions.CheckState(!string.IsNullOrEmpty(_commandText), "Cannot execute empty command"); - GaxPreconditions.CheckNotNull(SpannerConnection, nameof(SpannerConnection)); + GaxPreconditions.CheckState(Connection != null, "No connection has been set for the command"); GaxPreconditions.CheckState(Transaction == null || Transaction.Connection == SpannerConnection, "The transaction that has been set for this command is from a different connection"); } public override int ExecuteNonQuery() { + CheckDisposed(); if (_mutation != null) { ExecuteMutation(); @@ -234,6 +272,7 @@ public override int ExecuteNonQuery() public override object? ExecuteScalar() { + CheckDisposed(); GaxPreconditions.CheckState(_mutation == null, "Cannot execute mutations with ExecuteScalar()"); var rows = Execute(); using var reader = new SpannerDataReader(SpannerConnection, rows, CommandBehavior.Default); @@ -250,29 +289,53 @@ public override int ExecuteNonQuery() public override void Prepare() { + CheckDisposed(); Execute(ExecuteSqlRequest.Types.QueryMode.Plan); } public override Task PrepareAsync(CancellationToken cancellationToken = default) { + CheckDisposed(); return ExecuteAsync(ExecuteSqlRequest.Types.QueryMode.Plan, cancellationToken); } protected override DbParameter CreateDbParameter() { + CheckDisposed(); return new SpannerParameter(); } protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) { + CheckDisposed(); GaxPreconditions.CheckState(_mutation == null, "Cannot execute mutations with ExecuteDbDataReader()"); - var rows = Execute(); - return new SpannerDataReader(SpannerConnection, rows, behavior); + try + { + var rows = Execute(); + return new SpannerDataReader(SpannerConnection, rows, behavior); + } + catch (SpannerException exception) + { + if (behavior.HasFlag(CommandBehavior.CloseConnection)) + { + SpannerConnection.Close(); + } + throw new SpannerDbException(exception); + } + catch (Exception) + { + if (behavior.HasFlag(CommandBehavior.CloseConnection)) + { + SpannerConnection.Close(); + } + throw; + } } protected override async Task ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) { + CheckDisposed(); GaxPreconditions.CheckState(_mutation == null, "Cannot execute mutations with ExecuteDbDataReader()"); try { @@ -281,7 +344,37 @@ protected override async Task ExecuteDbDataReaderAsync(CommandBeha } catch (SpannerException exception) { + if (behavior.HasFlag(CommandBehavior.CloseConnection)) + { + SpannerConnection.Close(); + } throw new SpannerDbException(exception); } + catch (Exception) + { + if (behavior.HasFlag(CommandBehavior.CloseConnection)) + { + await SpannerConnection.CloseAsync(); + } + throw; + } } + + object ICloneable.Clone() => Clone(); + + public virtual SpannerCommand Clone() + { + var clone = new SpannerCommand() + { + Connection = Connection, + _commandText = _commandText, + _transaction = _transaction, + CommandTimeout = CommandTimeout, + CommandType = CommandType, + DesignTimeVisible = DesignTimeVisible, + }; + (DbParameterCollection as SpannerParameterCollection)?.CloneTo((clone.Parameters as SpannerParameterCollection)!); + return clone; + } + } \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommandBuilder.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommandBuilder.cs index c1727c0d..67a0bd9b 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommandBuilder.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommandBuilder.cs @@ -17,6 +17,10 @@ namespace Google.Cloud.Spanner.DataProvider; +/// +/// This class is currently not supported. +/// All methods in this class throw a NotImplementedException. +/// public class SpannerCommandBuilder : DbCommandBuilder { protected override void ApplyParameterInfo(DbParameter parameter, DataRow row, StatementType statementType, bool whereClause) @@ -43,4 +47,4 @@ protected override void SetRowUpdatingHandler(DbDataAdapter adapter) { throw new System.NotImplementedException(); } -} \ No newline at end of file +} diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataAdapter.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataAdapter.cs index 74dfceab..f69e85fc 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataAdapter.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataAdapter.cs @@ -16,6 +16,10 @@ namespace Google.Cloud.Spanner.DataProvider; +// SpannerDataAdapter does not do anything, as Spanner does not return base table and key column information for simple +// select statements. +// +// One possible way to implement it could be to only support it in combination with a TableDirect command. public class SpannerDataAdapter : DbDataAdapter { diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs index 465ebc8d..96f86600 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs @@ -122,12 +122,20 @@ public override bool Read() public override async Task ReadAsync(CancellationToken cancellationToken) { - if (!InternalRead()) + try { - _hasReadData = true; - _currentRow = await LibRows.NextAsync(); + if (!InternalRead()) + { + _hasReadData = true; + _currentRow = await LibRows.NextAsync(cancellationToken); + } + + return _currentRow != null; + } + catch (SpannerException exception) + { + throw SpannerDbException.TranslateException(exception); } - return _currentRow != null; } private bool InternalRead() diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataSource.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataSource.cs index d85942a0..34bc693e 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataSource.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataSource.cs @@ -12,8 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; using System.Data.Common; using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; using Google.Api.Gax; namespace Google.Cloud.Spanner.DataProvider; @@ -25,7 +28,6 @@ public class SpannerDataSource : DbDataSource [AllowNull] public sealed override string ConnectionString => _connectionStringBuilder.ConnectionString; - public static SpannerDataSource Create(string connectionString) { GaxPreconditions.CheckNotNull(connectionString, nameof(connectionString)); @@ -44,8 +46,27 @@ private SpannerDataSource(SpannerConnectionStringBuilder connectionStringBuilder _connectionStringBuilder = connectionStringBuilder; } + public new SpannerConnection CreateConnection() => (base.CreateConnection() as SpannerConnection)!; + + public new SpannerConnection OpenConnection() => (base.OpenDbConnection() as SpannerConnection)!; + + public new async ValueTask OpenConnectionAsync(CancellationToken cancellationToken = default) + { + var connection = CreateConnection(); + try + { + await connection.OpenAsync(cancellationToken).ConfigureAwait(false); + return connection; + } + catch + { + await connection.DisposeAsync().ConfigureAwait(false); + throw; + } + } + protected override DbConnection CreateDbConnection() { return new SpannerConnection(_connectionStringBuilder); } -} \ No newline at end of file +} diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerDbException.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerDbException.cs index eb03b77d..619015b8 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerDbException.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerDbException.cs @@ -15,6 +15,7 @@ using System; using System.Data.Common; using Google.Cloud.SpannerLib; +using Google.Rpc; namespace Google.Cloud.Spanner.DataProvider; @@ -28,13 +29,24 @@ internal static T TranslateException(Func func) } catch (SpannerException exception) { - throw new SpannerDbException(exception); + throw TranslateException(exception); } } + + internal static Exception TranslateException(SpannerException exception) + { + if (exception.Code == Code.Cancelled) + { + return new OperationCanceledException(exception.Message, exception); + } + return new SpannerDbException(exception); + } private SpannerException SpannerException { get; } + + public Status Status => SpannerException.Status; - internal SpannerDbException(SpannerException spannerException) : base(spannerException.Message, spannerException.Status.Code) + internal SpannerDbException(SpannerException spannerException) : base(spannerException.Message, spannerException) { SpannerException = spannerException; } diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerParameter.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerParameter.cs index f1e7e2d0..0b1bd774 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerParameter.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerParameter.cs @@ -29,7 +29,7 @@ namespace Google.Cloud.Spanner.DataProvider; -public class SpannerParameter : DbParameter +public class SpannerParameter : DbParameter, ICloneable { private DbType? _dbType; @@ -64,7 +64,7 @@ public override string SourceColumn get => _sourceColumn; set => _sourceColumn = value ?? ""; } - public override object? Value { get; set; } + public sealed override object? Value { get; set; } public override bool SourceColumnNullMapping { get; set; } public override int Size { get; set; } public override DataRowVersion SourceVersion @@ -72,16 +72,25 @@ public override DataRowVersion SourceVersion get => DataRowVersion.Current; set { } } + + public SpannerParameter() { } + + public SpannerParameter(string name, object? value) + { + GaxPreconditions.CheckNotNull(name, nameof(name)); + _name = name; + Value = value; + } public override void ResetDbType() { _dbType = null; } - internal Value ConvertToProto() + internal Value ConvertToProto(DbParameter dbParameter) { - GaxPreconditions.CheckState(Value != null, $"Parameter {ParameterName} has no value"); - return ConvertToProto(Value!); + GaxPreconditions.CheckState(dbParameter.Direction != ParameterDirection.Input || Value != null, $"Parameter {ParameterName} has no value"); + return ConvertToProto(Value); } internal Google.Cloud.Spanner.V1.Type? GetSpannerType() @@ -89,7 +98,7 @@ internal Value ConvertToProto() return SpannerParameterType ?? TypeConversion.GetSpannerType(_dbType); } - private Value ConvertToProto(object value) + private Value ConvertToProto(object? value) { var type = GetSpannerType(); return ConvertToProto(value, type); @@ -182,5 +191,20 @@ private static Value ConvertToProto(object? value, Google.Cloud.Spanner.V1.Type? } return proto; } + + object ICloneable.Clone() => Clone(); + + public SpannerParameter Clone() + { + var clone = new SpannerParameter(_name, Value) + { + Direction = Direction, + IsNullable = IsNullable, + SourceColumn = SourceColumn, + SourceVersion = SourceVersion, + SourceColumnNullMapping = SourceColumnNullMapping, + }; + return clone; + } } \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerParameterCollection.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerParameterCollection.cs index 9fc6734d..caf2277f 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerParameterCollection.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerParameterCollection.cs @@ -36,6 +36,10 @@ public override int Add(object value) { _params.Add(spannerParameter); } + else if (value is DbParameter) + { + throw new ArgumentException("value is not a SpannerParameter"); + } else { _params.Add(new SpannerParameter { ParameterName = "p" + (index + 1), Value = value }); @@ -51,7 +55,7 @@ public override void Clear() public override bool Contains(object value) { - return _params.Find(p => Equals(p.Value, value)) != null; + return IndexOf(value) > -1; } public override int IndexOf(object value) @@ -70,6 +74,10 @@ public override void Insert(int index, object value) { _params.Insert(index, spannerParameter); } + else if (value is DbParameter) + { + throw new ArgumentException("value is not a SpannerParameter"); + } else { _params.Insert(index, new SpannerParameter { ParameterName = "p" + (index + 1), Value = value }); @@ -93,7 +101,7 @@ public override void RemoveAt(int index) public override void RemoveAt(string parameterName) { - var index = _params.FindIndex(p => Equals(p.ParameterName, parameterName)); + var index = IndexOf(parameterName); if (index > -1) { _params.RemoveAt(index); @@ -118,11 +126,24 @@ protected override void SetParameter(string parameterName, DbParameter value) GaxPreconditions.CheckNotNull(value, nameof(value)); if (value is SpannerParameter spannerParameter) { - var index = _params.FindIndex(p => Equals(p.ParameterName, parameterName)); + if (spannerParameter.ParameterName == "") + { + spannerParameter.ParameterName = parameterName; + } + else if (!spannerParameter.ParameterName.Equals(parameterName, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException("Parameter names mismatch"); + } + var index = IndexOf(parameterName); if (index > -1) { _params[index] = spannerParameter; } + else + { + spannerParameter.ParameterName = parameterName; + Add(spannerParameter); + } } else { @@ -132,12 +153,17 @@ protected override void SetParameter(string parameterName, DbParameter value) public override int IndexOf(string parameterName) { - return _params.FindIndex(p => Equals(p.ParameterName, parameterName)); + var result = _params.FindIndex(p => Equals(p.ParameterName, parameterName)); + if (result > -1) + { + return result; + } + return _params.FindIndex(p => p.ParameterName.Equals(parameterName, StringComparison.OrdinalIgnoreCase)); } public override bool Contains(string value) { - return _params.Find(p => Equals(p.Value, value)) != null; + return IndexOf(value) > -1; } public override void CopyTo(Array array, int index) @@ -170,9 +196,12 @@ protected override DbParameter GetParameter(int index) return _params[index]; } - protected override DbParameter GetParameter(string parameterName) +#pragma warning disable CS8764 + protected override DbParameter? GetParameter(string parameterName) +#pragma warning restore CS8764 { - return _params.Find(p => Equals(p.ParameterName, parameterName)); + var index = IndexOf(parameterName); + return index > -1 ? _params[index] : null; } public override void AddRange(Array values) @@ -201,7 +230,11 @@ public override void AddRange(Array values) { name = "p" + name[1..]; } - queryParams.Fields.Add(name, spannerParameter.ConvertToProto()); + else if (string.IsNullOrEmpty(name)) + { + name = "p" + (index + 1); + } + queryParams.Fields.Add(name, spannerParameter.ConvertToProto(spannerParameter)); var paramType = spannerParameter.GetSpannerType(); if (paramType != null) { @@ -215,4 +248,16 @@ public override void AddRange(Array values) } return Tuple.Create(queryParams, paramTypes); } + + internal void CloneTo(SpannerParameterCollection other) + { + GaxPreconditions.CheckNotNull(other, nameof(other)); + other._params.Clear(); + foreach (var param in _params) + { + var newParam = param.Clone(); + other._params.Add(newParam); + } + } + } \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs index 5852fd21..b283b980 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs @@ -29,6 +29,8 @@ public class SpannerTransaction : DbTransaction protected override DbConnection? DbConnection => _spannerConnection; public override IsolationLevel IsolationLevel { get; } private SpannerLib.Connection LibConnection { get; } + + internal bool IsCompleted => _spannerConnection == null; private bool _disposed; @@ -72,7 +74,7 @@ private static IsolationLevel TranslateIsolationLevel(TransactionOptions.Types.I protected override void Dispose(bool disposing) { - if (_spannerConnection != null) + if (!IsCompleted) { // Do a shoot-and-forget rollback. RollbackAsync(CancellationToken.None); @@ -83,7 +85,7 @@ protected override void Dispose(bool disposing) public override ValueTask DisposeAsync() { - if (_spannerConnection != null) + if (!IsCompleted) { // Do a shoot-and-forget rollback. RollbackAsync(CancellationToken.None); @@ -120,7 +122,7 @@ public override Task RollbackAsync(CancellationToken cancellationToken = default private void EndTransaction(Action endTransactionMethod) { CheckDisposed(); - GaxPreconditions.CheckState(_spannerConnection is not null, "This transaction is no longer active"); + GaxPreconditions.CheckState(!IsCompleted, "This transaction is no longer active"); try { endTransactionMethod(); @@ -135,7 +137,7 @@ private void EndTransaction(Action endTransactionMethod) private Task EndTransactionAsync(Func endTransactionMethod) { CheckDisposed(); - GaxPreconditions.CheckState(_spannerConnection is not null, "This transaction is no longer active"); + GaxPreconditions.CheckState(!IsCompleted, "This transaction is no longer active"); try { return endTransactionMethod(); diff --git a/parser/statement_parser.go b/parser/statement_parser.go index d86d8295..ef0875a8 100644 --- a/parser/statement_parser.go +++ b/parser/statement_parser.go @@ -708,6 +708,23 @@ const ( StatementTypeClientSide ) +func (st StatementType) String() string { + switch st { + case StatementTypeQuery: + return "Query" + case StatementTypeDml: + return "DML" + case StatementTypeDdl: + return "DDL" + case StatementTypeClientSide: + return "ClientSide" + case StatementTypeUnknown: + default: + return "Unknown" + } + return "Unknown" +} + // DmlType designates the type of modification that a DML statement will execute. type DmlType int diff --git a/spannerlib/api/batch_test.go b/spannerlib/api/batch_test.go index 1ae9af14..957d7faa 100644 --- a/spannerlib/api/batch_test.go +++ b/spannerlib/api/batch_test.go @@ -236,3 +236,41 @@ func TestExecuteDdlBatchInTransaction(t *testing.T) { t.Fatalf("ClosePool returned unexpected error: %v", err) } } + +func TestExecuteQueryInBatch(t *testing.T) { + t.Parallel() + + ctx := context.Background() + server, teardown := setupMockServer(t) + defer teardown() + dsn := fmt.Sprintf("%s/projects/p/instances/i/databases/d?useplaintext=true", server.Address) + + poolId, err := CreatePool(ctx, dsn) + if err != nil { + t.Fatalf("CreatePool returned unexpected error: %v", err) + } + connId, err := CreateConnection(ctx, poolId) + if err != nil { + t.Fatalf("CreateConnection returned unexpected error: %v", err) + } + + // Try to execute a batch with queries. This should fail. + request := &spannerpb.ExecuteBatchDmlRequest{Statements: []*spannerpb.ExecuteBatchDmlRequest_Statement{ + {Sql: "select 1"}, + {Sql: "select 2"}, + }} + _, err = ExecuteBatch(ctx, poolId, connId, request) + if g, w := spanner.ErrCode(err), codes.InvalidArgument; g != w { + t.Fatalf("error code mismatch\n Got: %v\nWant: %v", g, w) + } + if g, w := err.Error(), "rpc error: code = InvalidArgument desc = unsupported statement for batching: select 1"; g != w { + t.Fatalf("error message mismatch\n Got: %v\nWant: %v", g, w) + } + + if err := CloseConnection(ctx, poolId, connId); err != nil { + t.Fatalf("CloseConnection returned unexpected error: %v", err) + } + if err := ClosePool(ctx, poolId); err != nil { + t.Fatalf("ClosePool returned unexpected error: %v", err) + } +} diff --git a/spannerlib/api/connection.go b/spannerlib/api/connection.go index ab256879..78d270ff 100644 --- a/spannerlib/api/connection.go +++ b/spannerlib/api/connection.go @@ -471,7 +471,7 @@ func determineBatchType(conn *Connection, statements []*spannerpb.ExecuteBatchDm } else if firstStatementType == parser.StatementTypeDdl { batchType = parser.BatchTypeDdl } else { - return status.Errorf(codes.InvalidArgument, "unsupported statement type for batching: %v", firstStatementType) + return status.Errorf(codes.InvalidArgument, "unsupported statement for batching: %v", statements[0].Sql) } for i, statement := range statements { if i > 0 { diff --git a/spannerlib/grpc-server/server.go b/spannerlib/grpc-server/server.go index 24c82786..1a5bff30 100644 --- a/spannerlib/grpc-server/server.go +++ b/spannerlib/grpc-server/server.go @@ -46,6 +46,18 @@ func main() { if err != nil { log.Fatalf("failed to listen: %v\n", err) } + grpcServer, err := createServer() + if err != nil { + log.Fatalf("failed to create server: %v\n", err) + } + log.Printf("Starting gRPC server on %s\n", lis.Addr().String()) + err = grpcServer.Serve(lis) + if err != nil { + log.Printf("failed to serve: %v\n", err) + } +} + +func createServer() (*grpc.Server, error) { var opts []grpc.ServerOption // Set a max message size that is essentially no limit. opts = append(opts, grpc.MaxRecvMsgSize(math.MaxInt32)) @@ -53,11 +65,8 @@ func main() { server := spannerLibServer{} pb.RegisterSpannerLibServer(grpcServer, &server) - log.Printf("Starting gRPC server on %s\n", lis.Addr().String()) - err = grpcServer.Serve(lis) - if err != nil { - log.Printf("failed to serve: %v\n", err) - } + + return grpcServer, nil } var _ pb.SpannerLibServer = &spannerLibServer{} diff --git a/spannerlib/grpc-server/server_test.go b/spannerlib/grpc-server/server_test.go index 35cc3396..f10d40b2 100644 --- a/spannerlib/grpc-server/server_test.go +++ b/spannerlib/grpc-server/server_test.go @@ -2,6 +2,8 @@ package main import ( "context" + cryptorand "crypto/rand" + "encoding/base64" "fmt" "net" "os" @@ -194,6 +196,74 @@ func TestExecuteStreaming(t *testing.T) { } } +func TestLargeMessage(t *testing.T) { + t.Parallel() + ctx := context.Background() + + server, teardown := setupMockSpannerServer(t) + defer teardown() + dsn := fmt.Sprintf("%s/projects/p/instances/i/databases/d?useplaintext=true", server.Address) + + client, cleanup := startTestSpannerLibServer(t) + defer cleanup() + + pool, err := client.CreatePool(ctx, &pb.CreatePoolRequest{ConnectionString: dsn}) + if err != nil { + t.Fatalf("failed to create pool: %v", err) + } + connection, err := client.CreateConnection(ctx, &pb.CreateConnectionRequest{Pool: pool}) + if err != nil { + t.Fatalf("failed to create connection: %v", err) + } + + query := "insert into foo (value) values (@value)" + b := make([]byte, 10_000_000) + n, err := cryptorand.Read(b) + if err != nil { + t.Fatalf("failed to read value: %v", err) + } + if g, w := n, len(b); g != w { + t.Fatalf("length mismatch\n Got: %v\nWant: %v", g, w) + } + value := base64.StdEncoding.EncodeToString(b) + _ = server.TestSpanner.PutStatementResult(query, &testutil.StatementResult{ + Type: testutil.StatementResultUpdateCount, + UpdateCount: 1, + }) + stream, err := client.ExecuteStreaming(ctx, &pb.ExecuteRequest{ + Connection: connection, + ExecuteSqlRequest: &sppb.ExecuteSqlRequest{ + Sql: query, + Params: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "value": {Kind: &structpb.Value_StringValue{StringValue: value}}, + }, + }, + }, + }) + if err != nil { + t.Fatalf("failed to execute: %v", err) + } + numRows := 0 + for { + row, err := stream.Recv() + if err != nil { + t.Fatalf("failed to receive row: %v", err) + } + if len(row.Data) == 0 { + break + } + numRows++ + } + if g, w := numRows, 0; g != w { + t.Fatalf("num rows mismatch\n Got: %v\nWant: %v", g, w) + } + + if _, err := client.ClosePool(ctx, pool); err != nil { + t.Fatalf("failed to close pool: %v", err) + } +} + func TestExecuteStreamingClientSideStatement(t *testing.T) { t.Parallel() ctx := context.Background() @@ -485,11 +555,10 @@ func startTestSpannerLibServer(t *testing.T) (client pb.SpannerLibClient, cleanu t.Fatalf("failed to listen: %v\n", err) } addr := lis.Addr().String() - var opts []grpc.ServerOption - grpcServer := grpc.NewServer(opts...) - - server := spannerLibServer{} - pb.RegisterSpannerLibServer(grpcServer, &server) + grpcServer, err := createServer() + if err != nil { + t.Fatalf("failed to create server: %v\n", err) + } go func() { _ = grpcServer.Serve(lis) }() conn, err := grpc.NewClient( diff --git a/spannerlib/wrappers/spannerlib-dotnet/build.sh b/spannerlib/wrappers/spannerlib-dotnet/build.sh index c763088e..daca91b6 100755 --- a/spannerlib/wrappers/spannerlib-dotnet/build.sh +++ b/spannerlib/wrappers/spannerlib-dotnet/build.sh @@ -38,8 +38,10 @@ echo "Skip windows: $SKIP_WINDOWS" # Remove existing builds rm -r ./spannerlib-dotnet-native/libraries 2> /dev/null +rm -r ./spannerlib-dotnet-grpc-server/binaries 2> /dev/null rm -r ./*/bin 2> /dev/null rm -r ./*/obj 2> /dev/null +dotnet nuget locals global-packages --clear # Build gRPC server echo "Building gRPC server..." diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/MockSpannerServer.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/MockSpannerServer.cs index dec7afbe..8b5a9010 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/MockSpannerServer.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/MockSpannerServer.cs @@ -15,6 +15,7 @@ using System.Collections; using System.Collections.Concurrent; using System.Diagnostics; +using System.Reflection; using Google.Cloud.Spanner.Admin.Database.V1; using Google.Cloud.Spanner.Common.V1; using Google.Cloud.Spanner.V1; @@ -23,6 +24,7 @@ using Google.Protobuf.WellKnownTypes; using Google.Rpc; using Grpc.Core; +using Enum = System.Enum; using Status = Google.Rpc.Status; using GrpcCore = Grpc.Core; @@ -357,7 +359,7 @@ public void AddOrUpdateExecutionTime(string method, ExecutionTime executionTime) ); } - private void AddDialectResult() + public void AddDialectResult(DatabaseDialect dialect = DatabaseDialect.GoogleStandardSql) { AddOrUpdateStatementResult(SDialectQuery, StatementResult.CreateResultSet( @@ -367,10 +369,23 @@ private void AddDialectResult() }, new List { - new object[] { "GOOGLE_STANDARD_SQL" }, + new object[] { GetEnumOriginalName(dialect) }, })); } - + + string GetEnumOriginalName(Enum enumValue) + { + var enumType = enumValue.GetType(); + var enumValueName = enumValue.ToString(); + var enumValueInfo = enumType.GetMember(enumValueName).Single(); + var attribute = enumValueInfo.GetCustomAttribute(); + if (attribute is null) + { + throw new InvalidOperationException($"Attribute '{nameof(Protobuf.Reflection.OriginalNameAttribute)}' not found on enum '{enumType}'."); + } + return attribute.Name; + } + internal void AbortTransaction(string transactionId) { _abortedTransactions.TryAdd(ByteString.FromBase64(transactionId), true); @@ -384,6 +399,11 @@ internal void AbortNextStatement() } } + public void ClearRequests() + { + _requests.Clear(); + } + public IEnumerable Requests => new List(_requests).AsReadOnly(); public bool WaitForRequestsToContain(Func predicate) @@ -732,7 +752,7 @@ public override async Task ExecuteStreamingSql(ExecuteSqlRequest request, IServe } else { - throw new RpcException(new GrpcCore.Status(StatusCode.InvalidArgument, $"No result found for {request.Sql}")); + throw new RpcException(new GrpcCore.Status(StatusCode.InvalidArgument, $"No result found for {request.Sql[0..Math.Min(request.Sql.Length, 5000)]}")); } } diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/SpannerMockServerFixture.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/SpannerMockServerFixture.cs index c9d21e6a..aa25eac4 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/SpannerMockServerFixture.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/SpannerMockServerFixture.cs @@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; namespace Google.Cloud.SpannerLib.MockServer; @@ -53,10 +54,18 @@ public SpannerMockServerFixture() var builder = WebHost.CreateDefaultBuilder(); builder.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddJsonFile("appsettings.json", true)); builder.UseStartup(_ => new MockServerStartup(SpannerMock, DatabaseAdminMock)); + builder.ConfigureServices(services => + { + services.AddGrpc(options => + { + options.MaxReceiveMessageSize = null; + }); + }); builder.ConfigureKestrel(options => { // Setup a HTTP/2 endpoint without TLS. options.Listen(endpoint, o => o.Protocols = HttpProtocols.Http2); + options.Limits.MaxRequestBodySize = 100_000_000; }); _host = builder.Build(); _host.Start(); diff --git a/testutil/mocked_inmem_server.go b/testutil/mocked_inmem_server.go index 5bf012ff..1bb70452 100644 --- a/testutil/mocked_inmem_server.go +++ b/testutil/mocked_inmem_server.go @@ -17,6 +17,7 @@ package testutil import ( "encoding/base64" "fmt" + "math" "net" "strconv" "testing" @@ -111,7 +112,10 @@ func (s *MockedSpannerInMemTestServer) setupMockedServerWithAddr(t *testing.T, a s.setupSelect1Result() s.setupFooResults() s.setupSingersResults() - s.server = grpc.NewServer() + var serverOpts []grpc.ServerOption + // Set a max message size that is essentially no limit. + serverOpts = append(serverOpts, grpc.MaxRecvMsgSize(math.MaxInt32)) + s.server = grpc.NewServer(serverOpts...) spannerpb.RegisterSpannerServer(s.server, s.TestSpanner) instancepb.RegisterInstanceAdminServer(s.server, s.TestInstanceAdmin) databasepb.RegisterDatabaseAdminServer(s.server, s.TestDatabaseAdmin) From ad9b5adea655758c8490187dd1d77ac986be58dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Fri, 31 Oct 2025 18:04:34 +0100 Subject: [PATCH 09/29] test: add more tests --- .../AbstractMockServerTests.cs | 7 + .../spanner-ado-net-tests/ReaderTests.cs | 1469 +++++++++++++++++ .../spanner-ado-net-tests/SchemaTests.cs | 250 +++ .../SpannerParameterCollectionTests.cs | 14 + .../SpannerParameterTests.cs | 214 +++ .../spanner-ado-net-tests/SqlParserTests.cs | 293 ++++ .../spanner-ado-net-tests/TransactionTests.cs | 523 ++++++ .../spanner-ado-net/Preconditions.cs | 25 + .../spanner-ado-net/SpannerCommand.cs | 12 +- .../spanner-ado-net/SpannerConnection.cs | 65 +- .../spanner-ado-net/SpannerDataReader.cs | 122 +- .../spanner-ado-net/SpannerParameter.cs | 13 +- .../SpannerParameterCollection.cs | 11 + .../spanner-ado-net/SpannerSchemaProvider.cs | 359 ++++ .../spanner-ado-net/SpannerTransaction.cs | 19 +- .../spanner-ado-net/TypeConversion.cs | 6 +- parser/simple_parser.go | 8 +- parser/statement_parser.go | 13 +- parser/statement_parser_test.go | 61 + spannerlib/api/connection.go | 3 + .../wrappers/spannerlib-dotnet/publish.sh | 3 + .../MockSpannerServer.cs | 7 +- .../SpannerConverter.cs | 13 + 23 files changed, 3459 insertions(+), 51 deletions(-) create mode 100644 drivers/spanner-ado-net/spanner-ado-net-tests/ReaderTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-tests/SchemaTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-tests/SpannerParameterTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-tests/SqlParserTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net/Preconditions.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net/SpannerSchemaProvider.cs diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs index 82b66979..e5913b4a 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs @@ -60,6 +60,13 @@ public void Reset() Fixture.SpannerMock.Reset(); } + protected SpannerConnection OpenConnection() + { + var connection = new SpannerConnection(ConnectionString); + connection.Open(); + return connection; + } + protected async Task OpenConnectionAsync() { var connection = new SpannerConnection(ConnectionString); diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/ReaderTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/ReaderTests.cs new file mode 100644 index 00000000..b3de813f --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/ReaderTests.cs @@ -0,0 +1,1469 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Buffers.Binary; +using System.Collections; +using System.Data; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Text; +using Google.Cloud.Spanner.V1; +using Google.Cloud.SpannerLib.MockServer; +using Grpc.Core; +using TypeCode = Google.Cloud.Spanner.V1.TypeCode; + +namespace Google.Cloud.Spanner.DataProvider.Tests; + +public class ReaderTests : AbstractMockServerTests +{ + [Test] + public async Task ResumableNonConsumedToNonResumable() + { + var base64Value = Convert.ToBase64String([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + var sql = $"SELECT from_base64('{base64Value}'), 1"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Bytes, "c"), Tuple.Create(TypeCode.Int64, "c")], + [[base64Value, 1L]])); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(sql, conn); + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + await reader.IsDBNullAsync(0); + _ = reader.IsDBNull(0); + await using var stream = reader.GetStream(0); + Assert.That(reader.GetString(0), Is.EqualTo(base64Value)); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task SeekColumns() + { + const string sql = "SELECT 1,2,3"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "c"), Tuple.Create(TypeCode.Int64, "c"), Tuple.Create(TypeCode.Int64, "c")], + [[1,2,3]])); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(sql, conn); + await using var reader = await cmd.ExecuteReaderAsync(); + Assert.That(reader.Read(), Is.True); + Assert.That(reader.GetInt32(0), Is.EqualTo(1)); + Assert.That(reader.GetInt32(0), Is.EqualTo(1)); + Assert.That(reader.GetInt32(1), Is.EqualTo(2)); + Assert.That(reader.GetInt32(0), Is.EqualTo(1)); + } + + [Ignore("Requires multi-statement support")] + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task NoResultSet() + { + const string insert = "INSERT INTO my_table VALUES (8)"; + Fixture.SpannerMock.AddOrUpdateStatementResult(insert, StatementResult.CreateUpdateCount(1L)); + + await using var conn = await OpenConnectionAsync(); + + await using (var cmd = new SpannerCommand(insert, conn)) + await using (var reader = await cmd.ExecuteReaderAsync()) + { + Assert.That(() => reader.GetOrdinal("foo"), Throws.Exception.TypeOf()); + Assert.That(reader.Read(), Is.False); + Assert.That(() => reader.GetOrdinal("foo"), Throws.Exception.TypeOf()); + Assert.That(reader.FieldCount, Is.EqualTo(0)); + Assert.That(reader.NextResult(), Is.False); + Assert.That(() => reader.GetOrdinal("foo"), Throws.Exception.TypeOf()); + } + + await using (var cmd = new SpannerCommand($"SELECT 1; {insert}", conn)) + await using (var reader = await cmd.ExecuteReaderAsync()) + { + await reader.NextResultAsync(); + Assert.That(() => reader.GetOrdinal("foo"), Throws.Exception.TypeOf()); + Assert.That(reader.Read(), Is.False); + Assert.That(() => reader.GetOrdinal("foo"), Throws.Exception.TypeOf()); + Assert.That(reader.FieldCount, Is.EqualTo(0)); + } + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task EmptyResultSet() + { + const string sql = "SELECT 1 AS foo WHERE FALSE"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "foo")], [])); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(sql, conn); + await using var reader = await cmd.ExecuteReaderAsync(); + Assert.That(reader.Read(), Is.False); + Assert.That(reader.FieldCount, Is.EqualTo(1)); + Assert.That(reader.GetOrdinal("foo"), Is.EqualTo(0)); + Assert.That(() => reader[0], Throws.Exception.TypeOf()); + } + + [Ignore("Requires multi-statement support")] + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task FieldCount() + { + await using var conn = await OpenConnectionAsync(); + + await using var cmd = new SpannerCommand("SELECT 1; SELECT 2,3", conn); + await using (var reader = await cmd.ExecuteReaderAsync()) + { + Assert.That(reader.FieldCount, Is.EqualTo(1)); + Assert.That(reader.Read(), Is.True); + Assert.That(reader.FieldCount, Is.EqualTo(1)); + Assert.That(reader.Read(), Is.False); + Assert.That(reader.FieldCount, Is.EqualTo(1)); + Assert.That(reader.NextResult(), Is.True); + Assert.That(reader.FieldCount, Is.EqualTo(2)); + Assert.That(reader.NextResult(), Is.False); + Assert.That(reader.FieldCount, Is.EqualTo(0)); + } + + cmd.CommandText = $"INSERT INTO my_table (int) VALUES (1)"; + await using (var reader = await cmd.ExecuteReaderAsync()) + { + Assert.That(() => reader.FieldCount, Is.EqualTo(0)); + } + } + + [Ignore("Requires multi-statement support")] + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task RecordsAffected() + { + const int insertCount = 15; + for (var i = 0; i < insertCount; i++) + { + Fixture.SpannerMock.AddOrUpdateStatementResult($"INSERT INTO my_table (int) VALUES ({i});", StatementResult.CreateUpdateCount(1)); + } + + await using var conn = await OpenConnectionAsync(); + + var sb = new StringBuilder(); + for (var i = 0; i < 10; i++) + { + sb.Append($"INSERT INTO my_table (int) VALUES ({i});"); + } + // Testing, that on close reader consumes all rows (as insert doesn't have a result set, but select does) + sb.Append("SELECT 1;"); + for (var i = 10; i < 15; i++) + { + sb.Append($"INSERT INTO my_table (int) VALUES ({i});"); + } + + var cmd = new SpannerCommand(sb.ToString(), conn); + var reader = await cmd.ExecuteReaderAsync(); + reader.Close(); + Assert.That(reader.RecordsAffected, Is.EqualTo(insertCount)); + + const string select = "SELECT * FROM my_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(select, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "int")], + [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12], [13], [14], [15]])); + cmd = new SpannerCommand(select, conn); + reader = await cmd.ExecuteReaderAsync(); + reader.Close(); + Assert.That(reader.RecordsAffected, Is.EqualTo(-1)); + + const string update = "UPDATE my_table SET int=int+1 WHERE int > 10"; + Fixture.SpannerMock.AddOrUpdateStatementResult(update, StatementResult.CreateUpdateCount(5)); + cmd = new SpannerCommand(update, conn); + reader = await cmd.ExecuteReaderAsync(); + reader.Close(); + Assert.That(reader.RecordsAffected, Is.EqualTo(4)); + + const string noUpdate = "UPDATE my_table SET int=8 WHERE int=666"; + Fixture.SpannerMock.AddOrUpdateStatementResult(noUpdate, StatementResult.CreateUpdateCount(0)); + cmd = new SpannerCommand(noUpdate, conn); + reader = await cmd.ExecuteReaderAsync(); + reader.Close(); + Assert.That(reader.RecordsAffected, Is.EqualTo(0)); + + const string delete = "DELETE FROM my_table WHERE int > 10"; + Fixture.SpannerMock.AddOrUpdateStatementResult(delete, StatementResult.CreateUpdateCount(4)); + cmd = new SpannerCommand(delete, conn); + reader = await cmd.ExecuteReaderAsync(); + reader.Close(); + Assert.That(reader.RecordsAffected, Is.EqualTo(4)); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task GetStringWithParameter() + { + await using var conn = await OpenConnectionAsync(); + const string text = "Random text"; + const string sql = "SELECT name FROM my_table WHERE name = @value;"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "name")], [[text]])); + + var command = new SpannerCommand(sql, conn); + var param = new SpannerParameter + { + ParameterName = "value", + DbType = DbType.String, + Size = text.Length, + Value = text + }; + command.Parameters.Add(param); + + await using var dr = await command.ExecuteReaderAsync(); + dr.Read(); + var result = dr.GetString(0); + Assert.That(result, Is.EqualTo(text)); + } + + [Test] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task GetStringWithQuoteWithParameter() + { + const string test = "Text with ' single quote"; + const string sql = "SELECT name FROM my_table WHERE name = @value;"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "name")], [[test]])); + + using var conn = await OpenConnectionAsync(); + var command = new SpannerCommand(sql, conn); + + var param = new SpannerParameter + { + ParameterName = "value", + DbType = DbType.String, + Size = test.Length, + Value = test + }; + command.Parameters.Add(param); + + using var dr = await command.ExecuteReaderAsync(); + dr.Read(); + var result = dr.GetString(0); + Assert.That(result, Is.EqualTo(test)); + } + + [Test] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task GetValueByName() + { + const string sql = "SELECT 'Random text' AS real_column"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "real_column")], [["Random text"]])); + + await using var conn = await OpenConnectionAsync(); + using var command = new SpannerCommand(sql, conn); + using var dr = await command.ExecuteReaderAsync(); + dr.Read(); + Assert.That(dr["real_column"], Is.EqualTo("Random text")); + Assert.That(() => dr["non_existing"], Throws.Exception.TypeOf()); + } + + [Test] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + [SuppressMessage("ReSharper", "ConvertToUsingDeclaration")] + public async Task GetFieldType() + { + const string sql = "SELECT cast(1 as int64) AS some_column"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "some_column")], [[1L]])); + + using var conn = await OpenConnectionAsync(); + using (var cmd = new SpannerCommand(sql, conn)) + using (var reader = await cmd.ExecuteReaderAsync()) + { + reader.Read(); + Assert.That(reader.GetFieldType(0), Is.SameAs(typeof(long))); + } + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task GetFieldType_SchemaOnly() + { + const string sql = "SELECT cast(1 as int64) AS some_column"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "some_column")], [[]])); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(sql, conn); + await using var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SchemaOnly); + Assert.False(reader.Read()); + Assert.That(reader.GetFieldType(0), Is.SameAs(typeof(long))); + + var request = Fixture.SpannerMock.Requests.OfType().First(); + Assert.That(request, Is.Not.Null); + Assert.That(request.QueryMode, Is.EqualTo(ExecuteSqlRequest.Types.QueryMode.Plan)); + } + + [SuppressMessage("ReSharper", "UseAwaitUsing")] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + [Test] + public async Task GetDataTypeName([Values] TypeCode typeCode) + { + if (typeCode == TypeCode.Array || typeCode == TypeCode.Unspecified) + { + return; + } + + var sql = $"SELECT cast(NULL as {typeCode.ToString()} AS some_column"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(typeCode, "some_column")], [])); + + using var conn = await OpenConnectionAsync(); + using var cmd = new SpannerCommand(sql, conn); + using var reader = await cmd.ExecuteReaderAsync(); + reader.Read(); + Assert.That(reader.GetDataTypeName(0), Is.EqualTo(typeCode.ToString().ToUpper())); + } + + [Test] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + public async Task GetName() + { + const string sql = "SELECT 1 AS some_column"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "some_column")], [[1L]])); + + using var conn = await OpenConnectionAsync(); + using var command = new SpannerCommand(sql, conn); + using var dr = await command.ExecuteReaderAsync(); + await dr.ReadAsync(); + Assert.That(dr.GetName(0), Is.EqualTo("some_column")); + } + + [Test] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task GetFieldValueAsObject() + { + const string sql = "SELECT 'foo'"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "c")], [["foo"]])); + + using var conn = await OpenConnectionAsync(); + using var cmd = new SpannerCommand(sql, conn); + using var reader = await cmd.ExecuteReaderAsync(); + reader.Read(); + Assert.That(reader.GetFieldValue(0), Is.EqualTo("foo")); + } + + [Test] + public async Task GetValues() + { + const string sql = "SELECT 'hello', 1, cast('2014-01-01' as DATE)"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [ + Tuple.Create(TypeCode.String, "c1"), + Tuple.Create(TypeCode.Int64, "c2"), + Tuple.Create(TypeCode.Date, "c3"), + ], [["hello", 1L, new DateOnly(2014, 1, 1)]])); + + await using var conn = await OpenConnectionAsync(); + await using var command = new SpannerCommand(sql, conn); + await using (var dr = await command.ExecuteReaderAsync()) + { + await dr.ReadAsync(); + var values = new object[4]; + Assert.That(dr.GetValues(values), Is.EqualTo(3)); + Assert.That(values, Is.EqualTo(new object?[] { "hello", 1, new DateOnly(2014, 1, 1), null })); + } + await using (var dr = await command.ExecuteReaderAsync()) + { + await dr.ReadAsync(); + var values = new object[2]; + Assert.That(dr.GetValues(values), Is.EqualTo(2)); + Assert.That(values, Is.EqualTo(new object[] { "hello", 1 })); + } + } + + [Test] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task ExecuteReaderGettingEmptyResultSetWithOutputParameter() + { + const string sql = "SELECT * FROM my_table WHERE name = NULL;"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "c")], [])); + + using var conn = await OpenConnectionAsync(); + var command = new SpannerCommand(sql, conn); + var param = new SpannerParameter("some_param", DbType.String) + { + Direction = ParameterDirection.Output + }; + command.Parameters.Add(param); + using var dr = await command.ExecuteReaderAsync(); + Assert.That(dr.NextResult(), Is.False); + } + + [Test] + public async Task GetValueFromEmptyResultSet() + { + const string sql = "SELECT * FROM my_table WHERE name = :value;"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "name")], [])); + + await using var conn = await OpenConnectionAsync(); + await using var command = new SpannerCommand(sql, conn); + const string test = "Text single quote"; + var param = new SpannerParameter + { + ParameterName = "value", + DbType = DbType.String, + Size = test.Length, + Value = test + }; + command.Parameters.Add(param); + + await using var dr = await command.ExecuteReaderAsync(); + Assert.False(await dr.ReadAsync()); + // This line should throw the invalid operation exception as the data reader will + // have an empty resultset. + Assert.That(() => Console.WriteLine(dr.IsDBNull(0)), + Throws.Exception.TypeOf()); + } + + [Test] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task ReadPastReaderEnd() + { + using var conn = await OpenConnectionAsync(); + var command = new SpannerCommand("SELECT 1", conn); + using var dr = await command.ExecuteReaderAsync(); + while (dr.Read()) {} + Assert.That(() => dr[0], Throws.Exception.TypeOf()); + } + + [Ignore("Require multi-statement support")] + [Test] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task SingleResult() + { + await using var conn = await OpenConnectionAsync(); + await using var command = new SpannerCommand("SELECT 1; SELECT 2", conn); + var reader = await command.ExecuteReaderAsync(CommandBehavior.SingleResult); + Assert.That(reader.Read(), Is.True); + Assert.That(reader.GetInt32(0), Is.EqualTo(1)); + Assert.That(reader.NextResult(), Is.False); + } + + [Test] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task Exception_thrown_from_ExecuteReaderAsync([Values(PrepareOrNot.Prepared, PrepareOrNot.NotPrepared)] PrepareOrNot prepare) + { + const string sql = "SELECT error('test')"; + + using var conn = await OpenConnectionAsync(); + + using var cmd = new SpannerCommand(sql, conn); + if (prepare == PrepareOrNot.Prepared) + { + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "error")], [])); + cmd.Prepare(); + } + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateException(new RpcException(new Status(StatusCode.Internal, "test")))); + Assert.That(async () => await cmd.ExecuteReaderAsync(), Throws.Exception.TypeOf()); + } + + [Ignore("Require multi-statement support")] + [Test] + public async Task Exception_thrown_from_NextResult([Values(PrepareOrNot.Prepared, PrepareOrNot.NotPrepared)] PrepareOrNot prepare) + { + const string select1 = "SELECT 1"; + const string selectError = "SELECT error('test')"; + + await using var conn = await OpenConnectionAsync(); + + await using var cmd = new SpannerCommand($"{select1}; {selectError}", conn); + if (prepare == PrepareOrNot.Prepared) + { + Fixture.SpannerMock.AddOrUpdateStatementResult(selectError, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "error")], [])); + await cmd.PrepareAsync(); + } + + Fixture.SpannerMock.AddOrUpdateStatementResult(selectError, StatementResult.CreateException(new RpcException(new Status(StatusCode.Internal, "test")))); + await using var reader = await cmd.ExecuteReaderAsync(); + Assert.That(() => reader.NextResult(), Throws.Exception.TypeOf()); + } + + [Test] + public async Task SchemaOnlyReturnsNoData() + { + const string sql = "SELECT * FROM my_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "c")], [])); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(sql, conn); + await using var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SchemaOnly); + Assert.That(await reader.ReadAsync(), Is.False); + + var request = Fixture.SpannerMock.Requests.OfType().First(); + Assert.That(request, Is.Not.Null); + Assert.That(request.QueryMode, Is.EqualTo(ExecuteSqlRequest.Types.QueryMode.Plan)); + } + + [Test] + public async Task SchemaOnlyNextResultBeyondEnd() + { + const string sql = "SELECT * FROM my_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "id")], [])); + + await using var conn = await OpenConnectionAsync(); + + await using var cmd = new SpannerCommand(sql, conn); + await using var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SchemaOnly); + Assert.That(await reader.NextResultAsync(), Is.False); + Assert.That(await reader.NextResultAsync(), Is.False); + } + + [Test] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + public async Task GetOrdinal() + { + const string sql = "SELECT 0, 1 AS some_column WHERE 1=0"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "c"), Tuple.Create(TypeCode.Int64, "some_column")], [])); + + using var conn = await OpenConnectionAsync(); + using var command = new SpannerCommand(sql, conn); + using var reader = await command.ExecuteReaderAsync(); + Assert.That(reader.GetOrdinal("some_column"), Is.EqualTo(1)); + Assert.That(() => reader.GetOrdinal("doesn't_exist"), Throws.Exception.TypeOf()); + } + + [Test] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + public async Task GetOrdinalCaseInsensitive() + { + const string sql = "select 123 as FIELD1"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "FIELD1")], [[123L]])); + + using var conn = await OpenConnectionAsync(); + using var command = new SpannerCommand(sql, conn); + using var reader = await command.ExecuteReaderAsync(); + await reader.ReadAsync(); + Assert.That(reader.GetOrdinal("fieLd1"), Is.EqualTo(0)); + } + + [Test] + public async Task FieldIndexDoesNotExist() + { + await using var conn = await OpenConnectionAsync(); + await using var command = new SpannerCommand("SELECT 1", conn); + await using var dr = await command.ExecuteReaderAsync(); + await dr.ReadAsync(); + Assert.That(() => dr[5], Throws.Exception.TypeOf()); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task ReaderStillOpen_CanExecuteMoreCommands() + { + await using var conn = await OpenConnectionAsync(); + await using var cmd1 = new SpannerCommand("SELECT 1", conn); + await using var reader1 = await cmd1.ExecuteReaderAsync(); + Assert.That(conn.ExecuteNonQuery("SELECT 1"), Is.EqualTo(-1)); + Assert.That(await conn.ExecuteNonQueryAsync("SELECT 1"), Is.EqualTo(-1)); + Assert.That(conn.ExecuteScalar("SELECT 1"), Is.EqualTo(1)); + Assert.That(await conn.ExecuteScalarAsync("SELECT 1"), Is.EqualTo(1)); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task CleansUpOkWithDisposeCalls([Values(PrepareOrNot.Prepared, PrepareOrNot.NotPrepared)] PrepareOrNot prepare) + { + await using var conn = await OpenConnectionAsync(); + await using var command = new SpannerCommand("SELECT 1", conn); + await using var dr = await command.ExecuteReaderAsync(); + await dr.ReadAsync(); + dr.Close(); + + await using var upd = conn.CreateCommand(); + upd.CommandText = "SELECT 1"; + if (prepare == PrepareOrNot.Prepared) + { + upd.Prepare(); + } + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + public async Task Null() + { + const string sql = "SELECT @p1, cast(@p2 as string)"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "p1"), Tuple.Create(TypeCode.String, "p2")], [[DBNull.Value, DBNull.Value]])); + + using var conn = await OpenConnectionAsync(); + using var cmd = new SpannerCommand(sql, conn); + cmd.Parameters.Add(new SpannerParameter("p1", DbType.String) { Value = DBNull.Value }); + cmd.Parameters.Add(new SpannerParameter { ParameterName = "p2", Value = DBNull.Value }); + + using var reader = await cmd.ExecuteReaderAsync(); + reader.Read(); + + for (var i = 0; i < cmd.Parameters.Count; i++) + { + Assert.That(reader.IsDBNull(i), Is.True); + Assert.That(reader.IsDBNullAsync(i).Result, Is.True); + Assert.That(reader.GetValue(i), Is.EqualTo(DBNull.Value)); + Assert.That(reader.GetFieldValue(i), Is.EqualTo(DBNull.Value)); + Assert.That(reader.GetProviderSpecificValue(i), Is.EqualTo(DBNull.Value)); + Assert.That(() => reader.GetString(i), Throws.Exception.TypeOf()); + } + } + + [Ignore("Requires multi-statement support")] + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + public async Task HasRows([Values(PrepareOrNot.NotPrepared, PrepareOrNot.Prepared)] PrepareOrNot prepare) + { + using var conn = await OpenConnectionAsync(); + + var command = new SpannerCommand($"SELECT 1; SELECT * FROM my_table WHERE name='does_not_exist'", conn); + if (prepare == PrepareOrNot.Prepared) + { + command.Prepare(); + } + using (var reader = await command.ExecuteReaderAsync()) + { + Assert.That(reader.HasRows, Is.True); + Assert.That(reader.HasRows, Is.True); + Assert.That(reader.Read(), Is.True); + Assert.That(reader.HasRows, Is.True); + Assert.That(reader.Read(), Is.False); + Assert.That(reader.HasRows, Is.True); + await reader.NextResultAsync(); + Assert.That(reader.HasRows, Is.False); + } + + command.CommandText = "SELECT * FROM my_table"; + if (prepare == PrepareOrNot.Prepared) + { + command.Prepare(); + } + using (var reader = await command.ExecuteReaderAsync()) + { + reader.Read(); + Assert.That(reader.HasRows, Is.False); + } + + command.CommandText = "SELECT 1"; + if (prepare == PrepareOrNot.Prepared) + { + command.Prepare(); + } + using (var reader = await command.ExecuteReaderAsync()) + { + reader.Read(); + reader.Close(); + Assert.Throws(() => _ = reader.HasRows); + } + + command.CommandText = $"INSERT INTO my_table (name) VALUES ('foo'); SELECT * FROM my_table"; + if (prepare == PrepareOrNot.Prepared) + { + command.Prepare(); + } + using (var reader = await command.ExecuteReaderAsync()) + { + Assert.That(reader.HasRows, Is.True); + reader.Read(); + Assert.That(reader.GetString(0), Is.EqualTo("foo")); + } + + Assert.That(await conn.ExecuteScalarAsync("SELECT 1"), Is.EqualTo(1)); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + public async Task HasRowsSingleStatement([Values(PrepareOrNot.NotPrepared, PrepareOrNot.Prepared)] PrepareOrNot prepare) + { + const string selectNoRows = "SELECT * FROM my_table WHERE name='does_not_exist'"; + Fixture.SpannerMock.AddOrUpdateStatementResult(selectNoRows, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "name")], [])); + + using var conn = await OpenConnectionAsync(); + + var command = new SpannerCommand("SELECT 1", conn); + if (prepare == PrepareOrNot.Prepared) + { + command.Prepare(); + } + using (var reader = await command.ExecuteReaderAsync()) + { + Assert.That(reader.HasRows, Is.True); + Assert.That(reader.HasRows, Is.True); + Assert.That(reader.Read(), Is.True); + Assert.That(reader.HasRows, Is.True); + Assert.That(reader.Read(), Is.False); + Assert.That(reader.HasRows, Is.True); + Assert.False(await reader.NextResultAsync()); + } + + command.CommandText = selectNoRows; + if (prepare == PrepareOrNot.Prepared) + { + command.Prepare(); + } + using (var reader = await command.ExecuteReaderAsync()) + { + Assert.That(reader.HasRows, Is.False); + Assert.That(reader.HasRows, Is.False); + Assert.That(reader.Read(), Is.False); + Assert.That(reader.HasRows, Is.False); + Assert.That(reader.Read(), Is.False); + Assert.That(reader.HasRows, Is.False); + Assert.False(await reader.NextResultAsync()); + } + + command.CommandText = "SELECT 1"; + if (prepare == PrepareOrNot.Prepared) + { + command.Prepare(); + } + using (var reader = await command.ExecuteReaderAsync()) + { + reader.Read(); + reader.Close(); + Assert.Throws(() => _ = reader.HasRows); + } + + const string insertRow = "INSERT INTO my_table (name) VALUES ('foo')"; + Fixture.SpannerMock.AddOrUpdateStatementResult(insertRow, StatementResult.CreateUpdateCount(1L)); + command.CommandText = insertRow; + if (prepare == PrepareOrNot.Prepared) + { + command.Prepare(); + } + using (var reader = await command.ExecuteReaderAsync()) + { + Assert.That(reader.HasRows, Is.False); + Assert.False(reader.Read()); + } + + const string selectRow = "SELECT * FROM my_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(selectRow, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "name")], [["foo"]])); + command.CommandText = selectRow; + if (prepare == PrepareOrNot.Prepared) + { + command.Prepare(); + } + using (var reader = await command.ExecuteReaderAsync()) + { + Assert.That(reader.HasRows, Is.True); + Assert.True(reader.Read()); + Assert.That(reader.GetString(0), Is.EqualTo("foo")); + } + Assert.That(await conn.ExecuteScalarAsync("SELECT 1"), Is.EqualTo(1)); + } + + [Test] + public async Task HasRowsWithoutResultSet() + { + const string sql = "DELETE FROM my_table WHERE name = 'unknown'"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(0L)); + + await using var conn = await OpenConnectionAsync(); + await using var command = new SpannerCommand(sql, conn); + await using var reader = await command.ExecuteReaderAsync(); + Assert.That(reader.HasRows, Is.False); + } + + [Test] + public async Task IntervalAsTimeSpan() + { + const string sql = "SELECT CAST('1 hour' AS interval) AS dauer"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Interval, "dauer")], [["PT1H"]])); + + await using var conn = await OpenConnectionAsync(); + await using var command = new SpannerCommand(sql, conn); + await using var dr = await command.ExecuteReaderAsync() as SpannerDataReader; + Assert.That(dr!.HasRows); + Assert.That(await dr.ReadAsync()); + Assert.That(dr.HasRows); + var ts = dr.GetTimeSpan(0); + Assert.That(ts, Is.EqualTo(TimeSpan.FromHours(1))); + } + + [Test] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task CloseConnectionInMiddleOfRow() + { + const string sql = "SELECT 1, 2"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "c"), Tuple.Create(TypeCode.Int64, "c")], [[1L, 2L]])); + + using var conn = await OpenConnectionAsync(); + using var cmd = new SpannerCommand(sql, conn); + using var reader = await cmd.ExecuteReaderAsync(); + reader.Read(); + } + + [Test] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task InvalidCast() + { + const string sql = "SELECT 'foo'"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "c")], [["foo"]])); + + using var conn = await OpenConnectionAsync(); + using (var cmd = new SpannerCommand(sql, conn)) + using (var reader = await cmd.ExecuteReaderAsync()) + { + reader.Read(); + Assert.That(() => reader.GetInt32(0), Throws.Exception.TypeOf()); + } + + using (var cmd = new SpannerCommand("SELECT 1", conn)) + using (var reader = await cmd.ExecuteReaderAsync()) + { + reader.Read(); + Assert.That(() => reader.GetDateTime(0), Throws.Exception.TypeOf()); + } + Assert.That(await conn.ExecuteScalarAsync("SELECT 1"), Is.EqualTo(1)); + } + + [Test] + public async Task NullableScalar() + { + const string sql = "SELECT @p1, @p2"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "p1"), Tuple.Create(TypeCode.Int64, "p2")], [[DBNull.Value, 8L]])); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(sql, conn); + var p1 = new SpannerParameter { ParameterName = "p1", Value = DBNull.Value, DbType = DbType.Int16 }; + var p2 = new SpannerParameter { ParameterName = "p2", Value = (short)8 }; + Assert.That(p1.DbType, Is.EqualTo(DbType.Int16)); + Assert.That(p2.DbType, Is.EqualTo(DbType.String)); // This is the ADO.NET default + cmd.Parameters.Add(p1); + cmd.Parameters.Add(p2); + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + for (var i = 0; i < cmd.Parameters.Count; i++) + { + Assert.That(reader.GetFieldType(i), Is.EqualTo(typeof(long))); + Assert.That(reader.GetDataTypeName(i), Is.EqualTo("INT64")); + } + + Assert.That(() => reader.GetFieldValue(0), Is.EqualTo(DBNull.Value)); + Assert.That(() => reader.GetFieldValue(0), Throws.TypeOf()); + Assert.That(() => reader.GetFieldValue(0), Throws.Nothing); + Assert.That(reader.GetFieldValue(0), Is.Null); + + Assert.That(() => reader.GetFieldValue(1), Throws.Nothing); + Assert.That(() => reader.GetFieldValue(1), Throws.Nothing); + Assert.That(() => reader.GetFieldValue(1), Throws.Nothing); + Assert.That(reader.GetFieldValue(1), Is.EqualTo(8)); + Assert.That(reader.GetFieldValue(1), Is.EqualTo(8)); + Assert.That(reader.GetFieldValue(1), Is.EqualTo(8)); + } + + [Test] + public async Task ReaderCloseAndDispose() + { + await using var conn = await OpenConnectionAsync(); + await using var cmd1 = conn.CreateCommand(); + cmd1.CommandText = "SELECT 1"; + + var reader1 = await cmd1.ExecuteReaderAsync(CommandBehavior.CloseConnection); + await reader1.CloseAsync(); + + await conn.OpenAsync(); + cmd1.Connection = conn; + var reader2 = await cmd1.ExecuteReaderAsync(CommandBehavior.CloseConnection); + Assert.That(reader1, Is.Not.SameAs(reader2)); + Assert.Throws(() => _ = reader2.GetInt64(0)); + + await reader1.DisposeAsync(); + + Assert.Throws(() => _ = reader2.GetInt64(0)); + } + + [Test] + public async Task ConnectionCloseAndReaderDispose() + { + await using var conn = await OpenConnectionAsync(); + await using var cmd1 = conn.CreateCommand(); + cmd1.CommandText = "SELECT 1"; + + var reader1 = await cmd1.ExecuteReaderAsync(); + await conn.CloseAsync(); + await conn.OpenAsync(); + + var reader2 = await cmd1.ExecuteReaderAsync(); + Assert.That(reader1, Is.Not.SameAs(reader2)); + Assert.Throws(() => _ = reader2.GetInt64(0)); + + await reader1.DisposeAsync(); + + Assert.Throws(() => _ = reader2.GetInt64(0)); + } + + [Test] + public async Task UnboundReaderReuse() + { + Fixture.SpannerMock.AddOrUpdateStatementResult("SELECT 2", StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "c")], [[2L]])); + Fixture.SpannerMock.AddOrUpdateStatementResult("SELECT 3", StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "c")], [[3L]])); + + await using var dataSource = CreateDataSource(csb => { }); + await using var conn1 = await dataSource.OpenConnectionAsync(); + await using var cmd1 = conn1.CreateCommand(); + cmd1.CommandText = "SELECT 1"; + var reader1 = await cmd1.ExecuteReaderAsync(); + await using (var __ = reader1) + { + Assert.That(async () => await reader1.ReadAsync(), Is.EqualTo(true)); + Assert.That(() => reader1.GetInt32(0), Is.EqualTo(1)); + + await reader1.CloseAsync(); + await conn1.CloseAsync(); + } + + await using var conn2 = await dataSource.OpenConnectionAsync(); + await using var cmd2 = conn2.CreateCommand(); + cmd2.CommandText = "SELECT 2"; + var reader2 = await cmd2.ExecuteReaderAsync(); + await using (var __ = reader2) + { + Assert.That(async () => await reader2.ReadAsync(), Is.EqualTo(true)); + Assert.That(() => reader2.GetInt32(0), Is.EqualTo(2)); + Assert.That(reader1, Is.Not.SameAs(reader2)); + + await reader2.CloseAsync(); + await conn2.CloseAsync(); + } + + await using var conn3 = await dataSource.OpenConnectionAsync(); + await using var cmd3 = conn3.CreateCommand(); + cmd3.CommandText = "SELECT 3"; + var reader3 = await cmd3.ExecuteReaderAsync(); + await using (var __ = reader3) + { + Assert.That(async () => await reader3.ReadAsync(), Is.EqualTo(true)); + Assert.That(() => reader3.GetInt32(0), Is.EqualTo(3)); + Assert.That(reader1, Is.Not.SameAs(reader3)); + + await reader3.CloseAsync(); + await conn3.CloseAsync(); + } + } + + [Test] + public async Task ReadStringAsChar() + { + const string sql = "SELECT 'abcdefgh', 'ijklmnop'"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "c1"), Tuple.Create(TypeCode.String, "c2")], [["abcdefgh", "ijklmnop"]])); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + + await using var reader = await cmd.ExecuteReaderAsync(); + Assert.That(await reader.ReadAsync()); + Assert.That(reader.GetChar(0), Is.EqualTo('a')); + Assert.That(reader.GetChar(0), Is.EqualTo('a')); + Assert.That(reader.GetChar(1), Is.EqualTo('i')); + } + + [Test] + public async Task GetBytes() + { + byte[] expected = [1, 2, 3, 4, 5]; + var base64 = Convert.ToBase64String(expected); + const string query = "SELECT bytes, 'foo', bytes, 'bar', bytes, bytes FROM my_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(query, StatementResult.CreateResultSet( + [ + Tuple.Create(TypeCode.Bytes, "bytes"), + Tuple.Create(TypeCode.String, "foo"), + Tuple.Create(TypeCode.Bytes, "bytes"), + Tuple.Create(TypeCode.String, "bar"), + Tuple.Create(TypeCode.Bytes, "bytes"), + Tuple.Create(TypeCode.Bytes, "bytes"), + ], [[ + base64, + "foo", + base64, + "bar", + base64, + base64, + ]])); + + await using var conn = await OpenConnectionAsync(); + var actual = new byte[expected.Length]; + + await using var cmd = new SpannerCommand(query, conn); + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + Assert.That(reader.GetBytes(0, 0, actual, 0, 2), Is.EqualTo(2)); + Assert.That(actual[0], Is.EqualTo(expected[0])); + Assert.That(actual[1], Is.EqualTo(expected[1])); + Assert.That(reader.GetBytes(0, 0, null, 0, 0), Is.EqualTo(expected.Length), "Bad column length"); + + Assert.That(reader.GetBytes(0, 0, actual, 4, 1), Is.EqualTo(1)); + Assert.That(actual[4], Is.EqualTo(expected[0])); + + Assert.That(reader.GetBytes(0, 2, actual, 2, 3), Is.EqualTo(3)); + Assert.That(actual, Is.EqualTo(expected)); + Assert.That(reader.GetBytes(0, 0, null, 0, 0), Is.EqualTo(expected.Length), "Bad column length"); + + Assert.That(reader.GetString(1), Is.EqualTo("foo")); + reader.GetBytes(2, 0, actual, 0, 2); + + // Jump to another column from the middle of the column + reader.GetBytes(4, 0, actual, 0, 2); + Assert.That(reader.GetBytes(4, expected.Length - 1, actual, 0, 2), Is.EqualTo(1), + "Length greater than data length"); + Assert.That(actual[0], Is.EqualTo(expected[^1]), "Length greater than data length"); + Assert.That(() => reader.GetBytes(4, 0, actual, 0, actual.Length + 1), + Throws.Exception.TypeOf(), "Length great than output buffer length"); + // Close in the middle of a column + reader.GetBytes(5, 0, actual, 0, 2); + + var result = (byte[]) cmd.ExecuteScalar()!; + Assert.That(result.Length, Is.EqualTo(5)); + } + + [Test] + public async Task GetStreamSecondTimeWorks() + { + var expected = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + var base64 = Convert.ToBase64String(expected); + var sql = $"SELECT from_base64({base64})"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Bytes, "from_base64")], [[base64]])); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(sql, conn); + await using var reader = await cmd.ExecuteReaderAsync(); + + await reader.ReadAsync(); + Assert.That(reader.GetStream(0), Is.Not.Null); + Assert.That(reader.GetStream(0), Is.Not.Null); + } + + public static IEnumerable GetStreamCases() + { + var binary = MemoryMarshal + .AsBytes(Enumerable.Range(0, 1024).ToArray()) + .ToArray(); + yield return (binary, binary); + + var bigBinary = MemoryMarshal + .AsBytes(Enumerable.Range(0, 8193).ToArray()) + .ToArray(); + yield return (bigBinary, bigBinary); + + var bigint = 0xDEADBEEFL; + var bigintBinary = BitConverter.GetBytes( + BitConverter.IsLittleEndian + ? BinaryPrimitives.ReverseEndianness(bigint) + : bigint); + yield return (bigint, bigintBinary); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + public async Task GetStream( + [Values] bool isAsync, + [ValueSource(nameof(GetStreamCases))] (T Generic, byte[] Binary) value) + { + const string sql = "SELECT @p, @p"; + var base64 = Convert.ToBase64String(value.Binary); + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Bytes, "p"), Tuple.Create(TypeCode.Bytes, "p")], [[base64, base64]])); + + var expected = value.Binary; + var actual = new byte[expected.Length]; + + using var conn = await OpenConnectionAsync(); + using var cmd = new SpannerCommand(sql, conn); + cmd.Parameters.Add(new SpannerParameter("p", value.Generic)); + using var reader = await cmd.ExecuteReaderAsync(); + + await reader.ReadAsync(); + + using var stream = reader.GetStream(0); + Assert.That(stream.Length, Is.EqualTo(expected.Length)); + + var position = 0; + while (position < actual.Length) + { + if (isAsync) + { + position += await stream.ReadAsync(actual, position, actual.Length - position); + } + else + { + position += stream.Read(actual, position, actual.Length - position); + } + } + Assert.That(actual, Is.EqualTo(expected)); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + public async Task OpenStreamWhenChangingColumns([Values(true, false)] bool isAsync) + { + var data = new byte[] { 1, 2, 3 }; + var base64 = Convert.ToBase64String(data); + const string sql = "SELECT @p, @p"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Bytes, "p"), Tuple.Create(TypeCode.Bytes, "p")], [[base64, base64]])); + + using var conn = await OpenConnectionAsync(); + using var cmd = new SpannerCommand(sql, conn); + cmd.Parameters.Add(new SpannerParameter("p", data)); + using var reader = await cmd.ExecuteReaderAsync(); + reader.Read(); + var stream = reader.GetStream(0); + _ = reader.GetValue(1); + Assert.That(() => stream.ReadByte(), Throws.Nothing); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + public async Task OpenStreamWhenChangingRows([Values(true, false)] bool isAsync) + { + var data = new byte[] { 1, 2, 3 }; + var base64 = Convert.ToBase64String(data); + const string sql = "SELECT @p"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Bytes, "p")], [[base64]])); + + using var conn = await OpenConnectionAsync(); + using var cmd = new SpannerCommand(sql, conn); + cmd.Parameters.Add(new SpannerParameter("p", data)); + using var reader = await cmd.ExecuteReaderAsync(); + reader.Read(); + var s1 = reader.GetStream(0); + reader.Read(); + Assert.That(() => s1.ReadByte(), Throws.Nothing); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + public async Task GetBytesWithNull([Values(true, false)] bool isAsync) + { + const string sql = "SELECT bytes FROM my_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Bytes, "p")], [[DBNull.Value]])); + + using var conn = await OpenConnectionAsync(); + var buf = new byte[8]; + using var cmd = new SpannerCommand(sql, conn); + using var reader = await cmd.ExecuteReaderAsync(); + reader.Read(); + Assert.That(reader.IsDBNull(0), Is.True); + Assert.That(() => reader.GetBytes(0, 0, buf, 0, 1), Throws.Exception.TypeOf(), "GetBytes"); + Assert.That(() => reader.GetStream(0), Throws.Exception.TypeOf(), "GetStream"); + Assert.That(() => reader.GetBytes(0, 0, null, 0, 0), Throws.Exception.TypeOf(), "GetBytes with null buffer"); + } + + [Test] + public async Task GetStreamSeek() + { + const string sql = "SELECT 'abcdefgh'"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "c")], [["abcdefgh"]])); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + var buffer = new byte[4]; + + await using var stream = reader.GetStream(0); + Assert.That(stream.CanSeek); + + var seekPosition = stream.Seek(-1, SeekOrigin.End); + Assert.That(seekPosition, Is.EqualTo(stream.Length - 1)); + var read = stream.Read(buffer); + Assert.That(read, Is.EqualTo(1)); + Assert.That(Encoding.ASCII.GetString(buffer, 0, 1), Is.EqualTo("h")); + read = stream.Read(buffer); + Assert.That(read, Is.EqualTo(0)); + + seekPosition = stream.Seek(2, SeekOrigin.Begin); + Assert.That(seekPosition, Is.EqualTo(2)); + read = stream.Read(buffer); + Assert.That(read, Is.EqualTo(buffer.Length)); + Assert.That(Encoding.ASCII.GetString(buffer), Is.EqualTo("cdef")); + + seekPosition = stream.Seek(-3, SeekOrigin.Current); + Assert.That(seekPosition, Is.EqualTo(3)); + read = stream.Read(buffer); + Assert.That(read, Is.EqualTo(buffer.Length)); + Assert.That(Encoding.ASCII.GetString(buffer), Is.EqualTo("defg")); + + stream.Position = 1; + read = stream.Read(buffer); + Assert.That(read, Is.EqualTo(buffer.Length)); + Assert.That(Encoding.ASCII.GetString(buffer), Is.EqualTo("bcde")); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + public async Task GetChars() + { + const string str = "ABCDE"; + var expected = str.ToCharArray(); + var actual = new char[expected.Length]; + var queryText = $"SELECT '{str}', 3, '{str}', 4, '{str}', '{str}', '{str}'"; + Fixture.SpannerMock.AddOrUpdateStatementResult(queryText, StatementResult.CreateResultSet( + [ + Tuple.Create(TypeCode.String, "str"), + Tuple.Create(TypeCode.Int64, "c"), + Tuple.Create(TypeCode.String, "str"), + Tuple.Create(TypeCode.Int64, "c"), + Tuple.Create(TypeCode.String, "str"), + Tuple.Create(TypeCode.String, "str"), + Tuple.Create(TypeCode.String, "str"), + ], [[ str, 3L, str, 4L, str, str, str, ]])); + + using var conn = await OpenConnectionAsync(); + using var cmd = new SpannerCommand(queryText, conn); + using var reader = await cmd.ExecuteReaderAsync(); + reader.Read(); + + Assert.That(reader.GetChars(0, 0, actual, 0, 2), Is.EqualTo(2)); + Assert.That(actual[0], Is.EqualTo(expected[0])); + Assert.That(actual[1], Is.EqualTo(expected[1])); + Assert.That(reader.GetChars(0, 0, null, 0, 0), Is.EqualTo(expected.Length), "Bad column length"); + + Assert.That(reader.GetChars(2, 0, actual, 0, 2), Is.EqualTo(2)); + Assert.That(reader.GetChars(2, 0, actual, 4, 1), Is.EqualTo(1)); + Assert.That(actual[4], Is.EqualTo(expected[0])); + Assert.That(reader.GetChars(2, 2, actual, 2, 3), Is.EqualTo(3)); + Assert.That(actual, Is.EqualTo(expected)); + + //Assert.That(reader.GetChars(2, 0, null, 0, 0), Is.EqualTo(expected.Length), "Bad column length"); + + Assert.That(() => reader.GetChars(3, 0, null, 0, 0), Throws.Exception.TypeOf(), "GetChars on non-text"); + Assert.That(() => reader.GetChars(3, 0, actual, 0, 1), Throws.Exception.TypeOf(), "GetChars on non-text"); + Assert.That(reader.GetInt32(3), Is.EqualTo(4)); + reader.GetChars(4, 0, actual, 0, 2); + // Jump to another column from the middle of the column + reader.GetChars(5, 0, actual, 0, 2); + Assert.That(reader.GetChars(5, expected.Length - 1, actual, 0, 2), Is.EqualTo(1), "Length greater than data length"); + Assert.That(actual[0], Is.EqualTo(expected[^1]), "Length greater than data length"); + Assert.That(() => reader.GetChars(5, 0, actual, 0, actual.Length + 1), Throws.Exception.TypeOf(), "Length great than output buffer length"); + // Close in the middle of a column + reader.GetChars(6, 0, actual, 0, 2); + } + + [Test] + public async Task GetCharsAdvanceConsumed() + { + const string value = "01234567"; + var sql = $"SELECT '{value}'"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "c")], [[value]])); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(sql, conn); + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + var buffer = new char[2]; + // Don't start at the beginning of the column. + reader.GetChars(0, 2, buffer, 0, 2); + Assert.That(buffer, Is.EqualTo(new []{'2', '3'})); + reader.GetChars(0, 4, buffer, 0, 2); + Assert.That(buffer, Is.EqualTo(new []{'4', '5'})); + reader.GetChars(0, 6, buffer, 0, 2); + Assert.That(buffer, Is.EqualTo(new []{'6', '7'})); + reader.GetChars(0, 7, buffer, 0, 2); + Assert.That(buffer, Is.EqualTo(new []{'7', '7'})); + + reader.GetChars(0, 4, buffer, 0, 2); + Assert.That(buffer, Is.EqualTo(new []{'4', '5'})); + reader.GetChars(0, 6, buffer, 0, 2); + Assert.That(buffer, Is.EqualTo(new []{'6', '7'})); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task GetTextReader([Values(true, false)] bool isAsync) + { + const string str = "ABCDE"; + var queryText = $@"SELECT '{str}', 'foo'"; + Fixture.SpannerMock.AddOrUpdateStatementResult(queryText, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "c"), Tuple.Create(TypeCode.String, "c")], [[str, "foo"]])); + + await using var conn = await OpenConnectionAsync(); + var expected = str.ToCharArray(); + var actual = new char[expected.Length]; + + await using var cmd = new SpannerCommand(queryText, conn); + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + var textReader = reader.GetTextReader(0); + textReader.Read(actual, 0, 2); + Assert.That(actual[0], Is.EqualTo(expected[0])); + Assert.That(actual[1], Is.EqualTo(expected[1])); + Assert.That(() => reader.GetTextReader(0), Throws.Nothing, "Sequential text reader twice on same column"); + textReader.Read(actual, 2, 1); + Assert.That(actual[2], Is.EqualTo(expected[2])); + textReader.Dispose(); + + Assert.That(reader.GetChars(0, 0, actual, 4, 1), Is.EqualTo(1)); + Assert.That(actual[4], Is.EqualTo(expected[0])); + Assert.That(reader.GetString(1), Is.EqualTo("foo")); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task TextReaderZeroLengthColumn() + { + const string sql = "SELECT ''"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "c")], [[""]])); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + + await using var reader = await cmd.ExecuteReaderAsync(); + Assert.That(await reader.ReadAsync()); + + using var textReader = reader.GetTextReader(0); + Assert.That(textReader.Peek(), Is.EqualTo(-1)); + Assert.That(textReader.ReadToEnd(), Is.EqualTo(string.Empty)); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + public async Task OpenTextReaderWhenChangingColumns() + { + const string sql = "SELECT 'some_text', 'some_text'"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "c"), Tuple.Create(TypeCode.String, "c")], [["some_text", "some-text"]])); + + using var conn = await OpenConnectionAsync(); + using var cmd = new SpannerCommand(sql, conn); + using var reader = await cmd.ExecuteReaderAsync(); + reader.Read(); + var textReader = reader.GetTextReader(0); + _ = reader.GetValue(1); + Assert.That(() => textReader.Peek(), Throws.Nothing); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + public async Task OpenTextReaderWhenChangingRows() + { + const string sql = "SELECT 'some_text', 'some_text'"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "c"), Tuple.Create(TypeCode.String, "c")], [["some_text", "some-text"]])); + + using var conn = await OpenConnectionAsync(); + using var cmd = new SpannerCommand(sql, conn); + using var reader = await cmd.ExecuteReaderAsync(); + reader.Read(); + var tr1 = reader.GetTextReader(0); + reader.Read(); + Assert.That(() => tr1.Peek(), Throws.Nothing); + } + + [Test] + public async Task GetCharsWhenNull() + { + const string sql = "SELECT cast(null as string)"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "c")], [[DBNull.Value]])); + + var buf = new char[8]; + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(sql, conn); + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + Assert.That(reader.IsDBNull(0), Is.True); + Assert.That(reader.GetChars(0, 0, buf, 0, 1), Is.EqualTo(0)); + Assert.That(() => reader.GetTextReader(0), Throws.Nothing, "GetTextReader"); + Assert.That(reader.GetChars(0, 0, null, 0, 0), Is.EqualTo(0), "GetChars with null buffer"); + } + + [Test] + public async Task GetTextReaderAfterConsumingColumnWorks() + { + const string sql = "SELECT 'foo'"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "c")], [["foo"]])); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(sql, conn); + await using var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess); + await reader.ReadAsync(); + + _ = reader.GetString(0); + Assert.That(() => reader.GetTextReader(0), Throws.Nothing); + } + + [Test] + public async Task GetTextReaderInMiddleOfColumnWorks() + { + const string sql = "SELECT 'foo'"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "c")], [["foo"]])); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(sql, conn); + await using var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess); + await reader.ReadAsync(); + + _ = reader.GetChars(0, 0, new char[2], 0, 2); + Assert.That(() => reader.GetTextReader(0), Throws.Nothing); + } + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/SchemaTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/SchemaTests.cs new file mode 100644 index 00000000..6aa7303a --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/SchemaTests.cs @@ -0,0 +1,250 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Data; +using System.Data.Common; +using System.Text.RegularExpressions; +using TypeCode = Google.Cloud.Spanner.V1.TypeCode; + +namespace Google.Cloud.Spanner.DataProvider.Tests; + +public class SchemaTests : AbstractMockServerTests +{ + [Test] + public async Task MetaDataCollections() + { + await using var conn = await OpenConnectionAsync(); + + var metaDataCollections = await conn.GetSchemaAsync(DbMetaDataCollectionNames.MetaDataCollections); + Assert.That(metaDataCollections.Rows, Has.Count.GreaterThan(0)); + + foreach (var row in metaDataCollections.Rows.OfType()) + { + var collectionName = (string)row!["CollectionName"]; + Assert.That(await conn.GetSchemaAsync(collectionName), Is.Not.Null, $"Collection {collectionName} advertise in MetaDataCollections but is null"); + } + } + + [Test] + public async Task NoParameter() + { + await using var conn = await OpenConnectionAsync(); + + var dataTable1 = conn.GetSchema(); + var collections1 = dataTable1.Rows + .Cast() + .Select(r => (string)r["CollectionName"]) + .ToList(); + + var dataTable2 = conn.GetSchema(DbMetaDataCollectionNames.MetaDataCollections); + var collections2 = dataTable2.Rows + .Cast() + .Select(r => (string)r["CollectionName"]) + .ToList(); + + Assert.That(collections1, Is.EquivalentTo(collections2)); + } + + [Test] + public async Task CaseInsensitiveCollectionName() + { + await using var conn = await OpenConnectionAsync(); + + var dataTable1 = conn.GetSchema(DbMetaDataCollectionNames.MetaDataCollections); + var collections1 = dataTable1.Rows + .Cast() + .Select(r => (string)r["CollectionName"]) + .ToList(); + + var dataTable2 = conn.GetSchema("METADATACOLLECTIONS"); + var collections2 = dataTable2.Rows + .Cast() + .Select(r => (string)r["CollectionName"]) + .ToList(); + + var dataTable3 = conn.GetSchema("metadatacollections"); + var collections3 = dataTable3.Rows + .Cast() + .Select(r => (string)r["CollectionName"]) + .ToList(); + + var dataTable4 = conn.GetSchema("MetaDataCollections"); + var collections4 = dataTable4.Rows + .Cast() + .Select(r => (string)r["CollectionName"]) + .ToList(); + + var dataTable5 = conn.GetSchema("METADATACOLLECTIONS", null!); + var collections5 = dataTable5.Rows + .Cast() + .Select(r => (string)r["CollectionName"]) + .ToList(); + + var dataTable6 = conn.GetSchema("metadatacollections", null!); + var collections6 = dataTable6.Rows + .Cast() + .Select(r => (string)r["CollectionName"]) + .ToList(); + + var dataTable7 = conn.GetSchema("MetaDataCollections", null!); + var collections7 = dataTable7.Rows + .Cast() + .Select(r => (string)r["CollectionName"]) + .ToList(); + + Assert.That(collections1, Is.EquivalentTo(collections2)); + Assert.That(collections1, Is.EquivalentTo(collections3)); + Assert.That(collections1, Is.EquivalentTo(collections4)); + Assert.That(collections1, Is.EquivalentTo(collections5)); + Assert.That(collections1, Is.EquivalentTo(collections6)); + Assert.That(collections1, Is.EquivalentTo(collections7)); + } + + [Test] + public async Task DataSourceInformation() + { + await using var conn = await OpenConnectionAsync(); + var dataTable = conn.GetSchema(DbMetaDataCollectionNames.MetaDataCollections); + var metadata = dataTable.Rows + .Cast() + .Single(r => r["CollectionName"].Equals("DataSourceInformation")); + Assert.That(metadata["NumberOfRestrictions"], Is.Zero); + Assert.That(metadata["NumberOfIdentifierParts"], Is.Zero); + + var dataSourceInfo = conn.GetSchema(DbMetaDataCollectionNames.DataSourceInformation); + var row = dataSourceInfo.Rows.Cast().Single(); + + Assert.That(row["DataSourceProductName"], Is.EqualTo("Spanner")); + Assert.That(row["DataSourceProductVersion"], Is.EqualTo("1.0.0")); + Assert.That(row["DataSourceProductVersionNormalized"], Is.EqualTo("001.000.0000")); + + Assert.That(Regex.Match("`some_identifier`", (string)row["QuotedIdentifierPattern"]).Groups[1].Value, + Is.EqualTo("some_identifier")); + } + + [Test] + public async Task DataTypes() + { + await using var connection = await OpenConnectionAsync(); + + var dataTable = connection.GetSchema(DbMetaDataCollectionNames.MetaDataCollections); + var metadata = dataTable.Rows + .Cast() + .Single(r => r["CollectionName"].Equals("DataTypes")); + Assert.That(metadata["NumberOfRestrictions"], Is.Zero); + Assert.That(metadata["NumberOfIdentifierParts"], Is.Zero); + + var dataTypes = connection.GetSchema(DbMetaDataCollectionNames.DataTypes); + + var boolRow = dataTypes.Rows.Cast().Single(r => r["TypeName"].Equals("Bool")); + Assert.That(boolRow["DataType"], Is.EqualTo("System.Boolean")); + Assert.That(boolRow["ProviderDbType"], Is.EqualTo((int)TypeCode.Bool)); + Assert.That(boolRow["IsUnsigned"], Is.EqualTo(DBNull.Value)); + + var bytesRow = dataTypes.Rows.Cast().Single(r => r["TypeName"].Equals("Bytes")); + Assert.That(bytesRow["DataType"], Is.EqualTo("System.Byte[]")); + Assert.That(bytesRow["ProviderDbType"], Is.EqualTo((int)TypeCode.Bytes)); + Assert.That(bytesRow["IsUnsigned"], Is.EqualTo(DBNull.Value)); + Assert.That(bytesRow["IsBestMatch"], Is.True); + + var dateRow = dataTypes.Rows.Cast().Single(r => r["TypeName"].Equals("Date")); + Assert.That(dateRow["DataType"], Is.EqualTo("System.DateOnly")); + Assert.That(dateRow["ProviderDbType"], Is.EqualTo((int)TypeCode.Date)); + Assert.That(dateRow["IsUnsigned"], Is.EqualTo(DBNull.Value)); + Assert.That(dateRow["IsBestMatch"], Is.True); + + var enumRow = dataTypes.Rows.Cast().Single(r => r["TypeName"].Equals("Enum")); + Assert.That(enumRow["DataType"], Is.EqualTo("System.Int64")); + Assert.That(enumRow["ProviderDbType"], Is.EqualTo((int)TypeCode.Enum)); + Assert.That(enumRow["IsUnsigned"], Is.EqualTo(DBNull.Value)); + Assert.That(enumRow["IsBestMatch"], Is.False); + + var float32Row = dataTypes.Rows.Cast().Single(r => r["TypeName"].Equals("Float32")); + Assert.That(float32Row["DataType"], Is.EqualTo("System.Single")); + Assert.That(float32Row["ProviderDbType"], Is.EqualTo((int)TypeCode.Float32)); + Assert.That(float32Row["IsUnsigned"], Is.False); + Assert.That(float32Row["IsBestMatch"], Is.True); + + var float64Row = dataTypes.Rows.Cast().Single(r => r["TypeName"].Equals("Float64")); + Assert.That(float64Row["DataType"], Is.EqualTo("System.Double")); + Assert.That(float64Row["ProviderDbType"], Is.EqualTo((int)TypeCode.Float64)); + Assert.That(float64Row["IsUnsigned"], Is.False); + Assert.That(float64Row["IsBestMatch"], Is.True); + + var int64Row = dataTypes.Rows.Cast().Single(r => r["TypeName"].Equals("Int64")); + Assert.That(int64Row["DataType"], Is.EqualTo("System.Int64")); + Assert.That(int64Row["ProviderDbType"], Is.EqualTo((int)TypeCode.Int64)); + Assert.That(int64Row["IsUnsigned"], Is.False); + Assert.That(int64Row["IsBestMatch"], Is.True); + + var intervalRow = dataTypes.Rows.Cast().Single(r => r["TypeName"].Equals("Interval")); + Assert.That(intervalRow["DataType"], Is.EqualTo("System.TimeSpan")); + Assert.That(intervalRow["ProviderDbType"], Is.EqualTo((int)TypeCode.Interval)); + Assert.That(intervalRow["IsUnsigned"], Is.EqualTo(DBNull.Value)); + Assert.That(intervalRow["IsBestMatch"], Is.True); + + var jsonRow = dataTypes.Rows.Cast().Single(r => r["TypeName"].Equals("Json")); + Assert.That(jsonRow["DataType"], Is.EqualTo("System.String")); + Assert.That(jsonRow["ProviderDbType"], Is.EqualTo((int)TypeCode.Json)); + Assert.That(jsonRow["IsUnsigned"], Is.EqualTo(DBNull.Value)); + Assert.That(jsonRow["IsBestMatch"], Is.False); + + var numericRow = dataTypes.Rows.Cast().Single(r => r["TypeName"].Equals("Numeric")); + Assert.That(numericRow["DataType"], Is.EqualTo("System.Decimal")); + Assert.That(numericRow["ProviderDbType"], Is.EqualTo((int)TypeCode.Numeric)); + Assert.That(numericRow["IsUnsigned"], Is.False); + Assert.That(numericRow["IsBestMatch"], Is.True); + + var protoRow = dataTypes.Rows.Cast().Single(r => r["TypeName"].Equals("Proto")); + Assert.That(protoRow["DataType"], Is.EqualTo("System.Byte[]")); + Assert.That(protoRow["ProviderDbType"], Is.EqualTo((int)TypeCode.Proto)); + Assert.That(protoRow["IsUnsigned"], Is.EqualTo(DBNull.Value)); + Assert.That(protoRow["IsBestMatch"], Is.False); + + var stringRow = dataTypes.Rows.Cast().Single(r => r["TypeName"].Equals("String")); + Assert.That(stringRow["DataType"], Is.EqualTo("System.String")); + Assert.That(stringRow["ProviderDbType"], Is.EqualTo((int)TypeCode.String)); + Assert.That(stringRow["IsUnsigned"], Is.EqualTo(DBNull.Value)); + Assert.That(stringRow["IsBestMatch"], Is.True); + + var timestampRow = dataTypes.Rows.Cast().Single(r => r["TypeName"].Equals("Timestamp")); + Assert.That(timestampRow["DataType"], Is.EqualTo("System.DateTime")); + Assert.That(timestampRow["ProviderDbType"], Is.EqualTo((int)TypeCode.Timestamp)); + Assert.That(timestampRow["IsUnsigned"], Is.EqualTo(DBNull.Value)); + Assert.That(timestampRow["IsBestMatch"], Is.True); + + var uuidRow = dataTypes.Rows.Cast().Single(r => r["TypeName"].Equals("Uuid")); + Assert.That(uuidRow["DataType"], Is.EqualTo("System.Guid")); + Assert.That(uuidRow["ProviderDbType"], Is.EqualTo((int)TypeCode.Uuid)); + Assert.That(uuidRow["IsUnsigned"], Is.EqualTo(DBNull.Value)); + Assert.That(uuidRow["IsBestMatch"], Is.True); + } + + [Test] + public async Task Restrictions() + { + await using var conn = await OpenConnectionAsync(); + var restrictions = conn.GetSchema(DbMetaDataCollectionNames.Restrictions); + Assert.That(restrictions.Rows, Has.Count.GreaterThan(0)); + } + + [Test] + public async Task ReservedWords() + { + await using var conn = await OpenConnectionAsync(); + var reservedWords = conn.GetSchema(DbMetaDataCollectionNames.ReservedWords); + Assert.That(reservedWords.Rows, Has.Count.GreaterThan(0)); + } + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/SpannerParameterCollectionTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/SpannerParameterCollectionTests.cs index 156bb151..0ce010a0 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/SpannerParameterCollectionTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/SpannerParameterCollectionTests.cs @@ -286,6 +286,20 @@ public void CloningSucceeds() Assert.DoesNotThrow(() => command.Clone()); } + [Test] + public void CleanName() + { + var param = new SpannerParameter(); + var command = new SpannerCommand(); + command.Parameters.Add(param); + + param.ParameterName = null; + + // These should not throw exceptions + Assert.That(command.Parameters.IndexOf(param.ParameterName), Is.EqualTo(0)); + Assert.That(param.ParameterName, Is.EqualTo("")); + } + class SomeOtherDbParameter : DbParameter { public override void ResetDbType() {} diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/SpannerParameterTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/SpannerParameterTests.cs new file mode 100644 index 00000000..cf702c73 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/SpannerParameterTests.cs @@ -0,0 +1,214 @@ +using System.Data; +using System.Data.Common; +using Google.Cloud.Spanner.V1; +using Google.Cloud.SpannerLib.MockServer; +using TypeCode = Google.Cloud.Spanner.V1.TypeCode; + +namespace Google.Cloud.Spanner.DataProvider.Tests; + +public class SpannerParameterTests : AbstractMockServerTests +{ + [Test] + public void SettingValueDoesNotChangeDbType() + { + // ReSharper disable once UseObjectOrCollectionInitializer + var p = new SpannerParameter { DbType = DbType.String }; + p.Value = 8; + Assert.That(p.DbType, Is.EqualTo(DbType.String)); + } + + [Test] + public void DefaultConstructor() + { + var p = new SpannerParameter(); + Assert.That(p.DbType, Is.EqualTo(DbType.String), "DbType"); + Assert.That(p.Direction, Is.EqualTo(ParameterDirection.Input), "Direction"); + Assert.That(p.IsNullable, Is.False, "IsNullable"); + Assert.That(p.ParameterName, Is.Empty, "ParameterName"); + Assert.That(p.Precision, Is.EqualTo(0), "Precision"); + Assert.That(p.Scale, Is.EqualTo(0), "Scale"); + Assert.That(p.Size, Is.EqualTo(0), "Size"); + Assert.That(p.SourceColumn, Is.Empty, "SourceColumn"); + Assert.That(p.SourceVersion, Is.EqualTo(DataRowVersion.Current), "SourceVersion"); + Assert.That(p.Value, Is.Null, "Value"); + } + + [Test] + public void ConstructorValueDateTime() + { + var value = new DateTime(2004, 8, 24); + + var p = new SpannerParameter("address", value); + // Setting a parameter value does not change the type. + Assert.That(p.DbType, Is.EqualTo(DbType.String), "B:DbType"); + Assert.That(p.Direction, Is.EqualTo(ParameterDirection.Input), "B:Direction"); + Assert.That(p.IsNullable, Is.False, "B:IsNullable"); + Assert.That(p.ParameterName, Is.EqualTo("address"), "B:ParameterName"); + Assert.That(p.Precision, Is.EqualTo(0), "B:Precision"); + Assert.That(p.Scale, Is.EqualTo(0), "B:Scale"); + Assert.That(p.Size, Is.EqualTo(0), "B:Size"); + Assert.That(p.SourceColumn, Is.Empty, "B:SourceColumn"); + Assert.That(p.SourceVersion, Is.EqualTo(DataRowVersion.Current), "B:SourceVersion"); + Assert.That(p.Value, Is.EqualTo(value), "B:Value"); + } + + [Test] + public void ConstructorValueDbNull() + { + var p = new SpannerParameter("address", DBNull.Value); + Assert.That(p.DbType, Is.EqualTo(DbType.String), "B:DbType"); + Assert.That(p.Direction, Is.EqualTo(ParameterDirection.Input), "B:Direction"); + Assert.That(p.IsNullable, Is.False, "B:IsNullable"); + Assert.That(p.ParameterName, Is.EqualTo("address"), "B:ParameterName"); + Assert.That(p.Precision, Is.EqualTo(0), "B:Precision"); + Assert.That(p.Scale, Is.EqualTo(0), "B:Scale"); + Assert.That(p.Size, Is.EqualTo(0), "B:Size"); + Assert.That(p.SourceColumn, Is.Empty, "B:SourceColumn"); + Assert.That(p.SourceVersion, Is.EqualTo(DataRowVersion.Current), "B:SourceVersion"); + Assert.That(p.Value, Is.EqualTo(DBNull.Value), "B:Value"); + } + + [Test] + public void ConstructorValueNull() + { + var p = new SpannerParameter("address", null); + Assert.That(p.DbType, Is.EqualTo(DbType.String), "A:DbType"); + Assert.That(p.Direction, Is.EqualTo(ParameterDirection.Input), "A:Direction"); + Assert.That(p.IsNullable, Is.False, "A:IsNullable"); + Assert.That(p.ParameterName, Is.EqualTo("address"), "A:ParameterName"); + Assert.That(p.Precision, Is.EqualTo(0), "A:Precision"); + Assert.That(p.Scale, Is.EqualTo(0), "A:Scale"); + Assert.That(p.Size, Is.EqualTo(0), "A:Size"); + Assert.That(p.SourceColumn, Is.Empty, "A:SourceColumn"); + Assert.That(p.SourceVersion, Is.EqualTo(DataRowVersion.Current), "A:SourceVersion"); + Assert.That(p.Value, Is.Null, "A:Value"); + } + + [Test] + public void Clone() + { + var expected = new SpannerParameter + { + Value = 42, + ParameterName = "TheAnswer", + + DbType = DbType.Int32, + + Direction = ParameterDirection.InputOutput, + IsNullable = true, + Precision = 1, + Scale = 2, + Size = 4, + + SourceVersion = DataRowVersion.Proposed, + SourceColumn = "source", + SourceColumnNullMapping = true, + }; + var actual = expected.Clone(); + + Assert.That(actual.Value, Is.EqualTo(expected.Value)); + Assert.That(actual.ParameterName, Is.EqualTo(expected.ParameterName)); + + Assert.That(actual.DbType, Is.EqualTo(expected.DbType)); + + Assert.That(actual.Direction, Is.EqualTo(expected.Direction)); + Assert.That(actual.IsNullable, Is.EqualTo(expected.IsNullable)); + Assert.That(actual.Precision, Is.EqualTo(expected.Precision)); + Assert.That(actual.Scale, Is.EqualTo(expected.Scale)); + Assert.That(actual.Size, Is.EqualTo(expected.Size)); + + Assert.That(actual.SourceVersion, Is.EqualTo(expected.SourceVersion)); + Assert.That(actual.SourceColumn, Is.EqualTo(expected.SourceColumn)); + Assert.That(actual.SourceColumnNullMapping, Is.EqualTo(expected.SourceColumnNullMapping)); + } + + [Test] + public void ParameterNull() + { + var param = new SpannerParameter{ParameterName = "param", DbType = DbType.Decimal}; + Assert.That(param.Scale, Is.EqualTo(0), "#A1"); + param.Value = DBNull.Value; + Assert.That(param.Scale, Is.EqualTo(0), "#A2"); + + param = new SpannerParameter{ParameterName = "param", DbType = DbType.Int32}; + Assert.That(param.Scale, Is.EqualTo(0), "#B1"); + param.Value = DBNull.Value; + Assert.That(param.Scale, Is.EqualTo(0), "#B2"); + } + + [Test] + public async Task MatchParamIndexCaseInsensitively() + { + const string sql = "SELECT @p,@P"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + new List>([Tuple.Create(TypeCode.String, "p"), Tuple.Create(TypeCode.String, "p")]), + new List([["Hello World", "Hello World"]]))); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new SpannerCommand(sql, conn); + cmd.AddParameter("p", "Hello World"); + await cmd.ExecuteNonQueryAsync(); + + var request = Fixture.SpannerMock.Requests.OfType().Single(r => r.Sql == sql); + Assert.That(request, Is.Not.Null); + // TODO: Revisit once https://github.com/googleapis/go-sql-spanner/issues/594 has been decided. + Assert.That(request.Params.Fields.Count, Is.EqualTo(2)); + Assert.That(request.Params.Fields["p"].StringValue, Is.EqualTo("Hello World")); + Assert.That(request.Params.Fields["P"].HasNullValue); + } + + [Test] + public void PrecisionViaInterface() + { + var parameter = new SpannerParameter(); + var paramIface = (IDbDataParameter)parameter; + + paramIface.Precision = 42; + + Assert.That(paramIface.Precision, Is.EqualTo((byte)42)); + } + + [Test] + public void PrecisionViaBaseClass() + { + var parameter = new SpannerParameter(); + var paramBase = (DbParameter)parameter; + + paramBase.Precision = 42; + + Assert.That(paramBase.Precision, Is.EqualTo((byte)42)); + } + + [Test] + public void ScaleViaInterface() + { + var parameter = new SpannerParameter(); + var paramIface = (IDbDataParameter)parameter; + + paramIface.Scale = 42; + + Assert.That(paramIface.Scale, Is.EqualTo((byte)42)); + } + + [Test] + public void ScaleViaBaseClass() + { + var parameter = new SpannerParameter(); + var paramBase = (DbParameter)parameter; + + paramBase.Scale = 42; + + Assert.That(paramBase.Scale, Is.EqualTo((byte)42)); + } + + [Test] + public void NullValueThrows() + { + using var connection = OpenConnection(); + using var command = new SpannerCommand("SELECT @p", connection); + command.Parameters.Add(new SpannerParameter("p", null)); + + Assert.That(() => command.ExecuteReader(), Throws.InvalidOperationException); + } + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/SqlParserTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/SqlParserTests.cs new file mode 100644 index 00000000..cdf1d6d1 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/SqlParserTests.cs @@ -0,0 +1,293 @@ +// Copyright 2025 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 +// +// https://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. + +using Google.Cloud.Spanner.Admin.Database.V1; +using Google.Cloud.Spanner.V1; +using Google.Cloud.SpannerLib; +using Google.Cloud.SpannerLib.MockServer; +using TypeCode = Google.Cloud.Spanner.V1.TypeCode; + +namespace Google.Cloud.Spanner.DataProvider.Tests; + +public class SqlParserTests : AbstractMockServerTests +{ + [Test] + public void ParameterSimple() + { + const string sql = "SELECT @p1, @p2"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "p1"), Tuple.Create(TypeCode.String, "p2")], [["foo", "foo"]])); + + using var conn = OpenConnection(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + cmd.AddParameter("p1", "foo"); + cmd.AddParameter("p2", "foo"); + + using var reader = cmd.ExecuteReader(); + + var request = Fixture.SpannerMock.Requests.OfType().First(); + Assert.That(request, Is.Not.Null); + Assert.That(request.Sql, Is.EqualTo("SELECT @p1, @p2")); + Assert.That(request.Params.Fields["p1"].StringValue, Is.EqualTo("foo")); + Assert.That(request.Params.Fields["p2"].StringValue, Is.EqualTo("foo")); + } + + [Test] + public void ParameterNameWithDot() + { + const string sql = "SELECT @a.parameter"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "p1"), Tuple.Create(TypeCode.String, "p2")], [["foo", "foo"]])); + + using var conn = OpenConnection(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + cmd.AddParameter("a", "foo"); + + using var reader = cmd.ExecuteReader(); + + var request = Fixture.SpannerMock.Requests.OfType().First(); + Assert.That(request, Is.Not.Null); + Assert.That(request.Sql, Is.EqualTo("SELECT @a.parameter")); + Assert.That(request.Params.Fields.Count, Is.EqualTo(1)); + Assert.That(request.Params.Fields["a"].StringValue, Is.EqualTo("foo")); + } + + [Test, Description("Checks several scenarios in which the SQL is supposed to pass untouched")] + [TestCase(@"SELECT to_tsvector('fat cats ate rats') @@ to_tsquery('cat & rat')", TestName="AtAt")] + [TestCase(@"SELECT 'cat'::tsquery @> 'cat & rat'::tsquery", TestName = "AtGt")] + [TestCase(@"SELECT 'cat'::tsquery <@ 'cat & rat'::tsquery", TestName = "AtLt")] + [TestCase(@"SELECT 'b''la'", TestName = "DoubleTicks")] + [TestCase(@"SELECT 'type(''m.response'')#''O''%'", TestName = "DoubleTicks2")] + [TestCase(@"SELECT 'abc'':str''a:str'", TestName = "DoubleTicks3")] + [TestCase(@"SELECT 1 FROM "":str""", TestName = "DoubleQuoted")] + [TestCase(@"SELECT 1 FROM 'yo'::str", TestName = "DoubleColons")] + [TestCase("SELECT $\u00ffabc0$literal string :str :int$\u00ffabc0 $\u00ffabc0$", TestName = "DollarQuotes")] + [TestCase("SELECT $$:str$$", TestName = "DollarQuotesNoTag")] + public void UntouchedPostgresql(string sql) + { + Fixture.SpannerMock.AddDialectResult(DatabaseDialect.Postgresql); + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.String, "c")], [[1L]])); + + using var conn = OpenConnection(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + + using var reader = cmd.ExecuteReader(); + + var request = Fixture.SpannerMock.Requests.OfType().First(); + Assert.That(request, Is.Not.Null); + Assert.That(request.Sql, Is.EqualTo(sql)); + } + + [Test] + [TestCase(@"SELECT 1<@param", TestName = "LessThan")] + [TestCase(@"SELECT 1>@param", TestName = "GreaterThan")] + [TestCase(@"SELECT 1<>@param", TestName = "NotEqual")] + [TestCase("SELECT--comment\r@param", TestName="LineComment")] + public void ParameterGetsBound(string sql) + { + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "c")], [[1L]])); + + using var conn = OpenConnection(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + cmd.AddParameter("param", 1L); + + using var reader = cmd.ExecuteReader(); + + var request = Fixture.SpannerMock.Requests.OfType().First(); + Assert.That(request, Is.Not.Null); + Assert.That(request.Sql, Is.EqualTo(sql)); + Assert.That(request.Params.Fields.Count, Is.EqualTo(1)); + Assert.That(request.Params.Fields["param"].StringValue, Is.EqualTo("1")); + } + + [Test] + [TestCase(@"SELECT e'ab\'c @param'", TestName = "Estring")] + [TestCase(@"SELECT/*/* -- nested comment @int /*/* *//*/ **/*/*/*/1")] + [TestCase(@"SELECT 1, +-- Comment, @param and also :param +2", TestName = "LineComment")] + public void ParameterDoesNotGetBoundPostgresql(string sql) + { + Fixture.SpannerMock.AddDialectResult(DatabaseDialect.Postgresql); + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "c")], [[1L]])); + + using var conn = OpenConnection(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + + using var reader = cmd.ExecuteReader(); + + var request = Fixture.SpannerMock.Requests.OfType().First(); + Assert.That(request, Is.Not.Null); + Assert.That(request.Sql, Is.EqualTo(sql)); + Assert.That(request.Params.Fields.Count, Is.EqualTo(0)); + } + + [Test] + public void NonConformingString() + { + const string sql = @"SELECT e'abc\'?''a ?'"; + Fixture.SpannerMock.AddDialectResult(DatabaseDialect.Postgresql); + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "c")], [[1L]])); + + using var conn = OpenConnection(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + + using var reader = cmd.ExecuteReader(); + + var request = Fixture.SpannerMock.Requests.OfType().First(); + Assert.That(request, Is.Not.Null); + Assert.That(request.Sql, Is.EqualTo(sql)); + Assert.That(request.Params.Fields.Count, Is.EqualTo(0)); + } + + [Ignore("Requires multi-statement support")] + [Test] + public void MultiqueryWithParameters() + { + const string sql1 = "select @p3, @p1"; + const string sql2 = "select @p2, @p3"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql1, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "p3"), Tuple.Create(TypeCode.Int64, "p1")], [[3L, 1L]])); + Fixture.SpannerMock.AddOrUpdateStatementResult(sql2, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "p2"), Tuple.Create(TypeCode.Int64, "p3")], [[2L, 3L]])); + + using var conn = OpenConnection(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = $"{sql1}; {sql2}"; + cmd.AddParameter("p1", 1L); + cmd.AddParameter("p2", 2L); + cmd.AddParameter("p3", 3L); + + using var reader = cmd.ExecuteReader(); + + var requests = Fixture.SpannerMock.Requests.OfType().ToList(); + Assert.That(requests.Count, Is.EqualTo(2)); + Assert.That(requests[0].Sql, Is.EqualTo(sql1)); + Assert.That(requests[1].Sql, Is.EqualTo(sql2)); + Assert.That(requests[0].Params.Fields.Count, Is.EqualTo(2)); + Assert.That(requests[1].Params.Fields.Count, Is.EqualTo(2)); + + Assert.That(requests[0].Params.Fields["p1"].StringValue, Is.EqualTo("1")); + Assert.That(requests[0].Params.Fields["p3"].StringValue, Is.EqualTo("3")); + + Assert.That(requests[1].Params.Fields["p2"].StringValue, Is.EqualTo("2")); + Assert.That(requests[1].Params.Fields["p3"].StringValue, Is.EqualTo("3")); + } + + [Test] + public void MissingParameterIsIgnored() + { + const string sql = "SELECT @p"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "c")], [[1L]])); + + using var conn = OpenConnection(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + + using var reader = cmd.ExecuteReader(); + + var request = Fixture.SpannerMock.Requests.OfType().First(); + Assert.That(request, Is.Not.Null); + Assert.That(request.Sql, Is.EqualTo(sql)); + Assert.That(request.Params.Fields.Count, Is.EqualTo(1)); + Assert.That(request.Params.Fields["p"].HasNullValue); + } + + [Ignore("Requires multi-statement support")] + [Test] + public void ConsecutiveSemicolons() + { + const string sql = "SELECT 1;;"; + + using var conn = OpenConnection(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + + using var reader = cmd.ExecuteReader(); + + Assert.That(Fixture.SpannerMock.Requests.OfType().Count(), Is.EqualTo(1)); + } + + [Ignore("Requires multi-statement support")] + [Test] + public void TrailingSemicolon() + { + const string sql = "SELECT 1;"; + + using var conn = OpenConnection(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + + using var reader = cmd.ExecuteReader(); + + Assert.That(Fixture.SpannerMock.Requests.OfType().Count(), Is.EqualTo(1)); + } + + [Ignore("Requires empty command support")] + [Test] + public void Empty() + { + const string sql = ""; + + using var conn = OpenConnection(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + + using var reader = cmd.ExecuteReader(); + + Assert.That(Fixture.SpannerMock.Requests.OfType().Count(), Is.EqualTo(0)); + } + + [Ignore("Requires multi-statement support")] + [Test] + public void SemicolonInParentheses() + { + const string sql = "CREATE OR REPLACE RULE test AS ON UPDATE TO test DO (SELECT 1; SELECT 1)"; + + using var conn = OpenConnection(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + + using var reader = cmd.ExecuteReader(); + + Assert.That(Fixture.SpannerMock.Requests.OfType().Count(), Is.EqualTo(1)); + } + + [Ignore("Requires multi-statement support")] + [Test] + public void SemicolonAfterParentheses() + { + const string sql = "CREATE OR REPLACE RULE test AS ON UPDATE TO test DO (SELECT 1); SELECT 1"; + + using var conn = OpenConnection(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + + using var reader = cmd.ExecuteReader(); + + Assert.That(Fixture.SpannerMock.Requests.OfType().Count(), Is.EqualTo(2)); + } + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/TransactionTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/TransactionTests.cs index 3a0353d1..fe5c9e51 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/TransactionTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/TransactionTests.cs @@ -12,8 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Data; +using System.Diagnostics.CodeAnalysis; using Google.Cloud.Spanner.V1; using Google.Cloud.SpannerLib.MockServer; +using Grpc.Core; namespace Google.Cloud.Spanner.DataProvider.Tests; @@ -179,4 +182,524 @@ public async Task TestTransactionTag() var lastCommitRequest = requests.OfType().Last(); Assert.That(lastCommitRequest.RequestOptions.TransactionTag, Is.Null); } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task Commit([Values(PrepareOrNot.NotPrepared, PrepareOrNot.Prepared)] PrepareOrNot prepare) + { + const string insertSql = "INSERT INTO my_table (name) VALUES ('X')"; + const string selectCountSql = "SELECT COUNT(*) FROM my_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(insertSql, StatementResult.CreateUpdateCount(1L)); + Fixture.SpannerMock.AddOrUpdateStatementResult(selectCountSql, StatementResult.CreateSelect1ResultSet()); + + await using var conn = await OpenConnectionAsync(); + + var tx = await conn.BeginTransactionAsync(); + await using (tx) + { + var cmd = new SpannerCommand(insertSql, conn, tx); + if (prepare == PrepareOrNot.Prepared) + { + cmd.Prepare(); + } + cmd.ExecuteNonQuery(); + Assert.That(conn.ExecuteScalar("SELECT COUNT(*) FROM my_table"), Is.EqualTo(1)); + tx.Commit(); + Assert.That(tx.IsCompleted); + Assert.That(() => tx.Connection, Throws.Nothing); + Assert.That(await conn.ExecuteScalarAsync("SELECT COUNT(*) FROM my_table"), Is.EqualTo(1)); + } + Assert.That(() => tx.Connection, Throws.Exception.TypeOf()); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task CommitAsync([Values(PrepareOrNot.NotPrepared, PrepareOrNot.Prepared)] PrepareOrNot prepare) + { + const string insertSql = "INSERT INTO my_table (name) VALUES ('X')"; + const string selectCountSql = "SELECT COUNT(*) FROM my_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(insertSql, StatementResult.CreateUpdateCount(1L)); + Fixture.SpannerMock.AddOrUpdateStatementResult(selectCountSql, StatementResult.CreateSelect1ResultSet()); + + await using var conn = await OpenConnectionAsync(); + + var tx = await conn.BeginTransactionAsync(); + await using (tx) + { + var cmd = new SpannerCommand(insertSql, conn, tx); + if (prepare == PrepareOrNot.Prepared) + { + cmd.Prepare(); + } + await cmd.ExecuteNonQueryAsync(); + Assert.That(conn.ExecuteScalar(selectCountSql), Is.EqualTo(1)); + await tx.CommitAsync(); + Assert.That(tx.IsCompleted); + Assert.That(() => tx.Connection, Throws.Nothing); + Assert.That(await conn.ExecuteScalarAsync(selectCountSql), Is.EqualTo(1)); + } + Assert.That(() => tx.Connection, Throws.Exception.TypeOf()); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task Rollback([Values(PrepareOrNot.NotPrepared, PrepareOrNot.Prepared)] PrepareOrNot prepare) + { + const string insertSql = "INSERT INTO my_table (name) VALUES ('X')"; + const string selectCountSql = "SELECT COUNT(*) FROM my_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(insertSql, StatementResult.CreateUpdateCount(1L)); + + await using var conn = await OpenConnectionAsync(); + + var tx = await conn.BeginTransactionAsync(); + await using (tx) + { + var cmd = new SpannerCommand(insertSql, conn, tx); + if (prepare == PrepareOrNot.Prepared) + { + cmd.Prepare(); + } + cmd.ExecuteNonQuery(); + Fixture.SpannerMock.AddOrUpdateStatementResult(selectCountSql, StatementResult.CreateSelect1ResultSet()); + + Assert.That(conn.ExecuteScalar(selectCountSql), Is.EqualTo(1)); + tx.Rollback(); + Fixture.SpannerMock.AddOrUpdateStatementResult(selectCountSql, StatementResult.CreateSelectZeroResultSet()); + + Assert.That(tx.IsCompleted); + Assert.That(() => tx.Connection, Throws.Nothing); + Assert.That(await conn.ExecuteScalarAsync(selectCountSql), Is.EqualTo(0)); + } + Assert.That(() => tx.Connection, Throws.Exception.TypeOf()); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task RollbackAsync([Values(PrepareOrNot.NotPrepared, PrepareOrNot.Prepared)] PrepareOrNot prepare) + { + const string insertSql = "INSERT INTO my_table (name) VALUES ('X')"; + const string selectCountSql = "SELECT COUNT(*) FROM my_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(insertSql, StatementResult.CreateUpdateCount(1L)); + + await using var conn = await OpenConnectionAsync(); + + var tx = await conn.BeginTransactionAsync(); + await using (tx) + { + var cmd = new SpannerCommand(insertSql, conn, tx); + if (prepare == PrepareOrNot.Prepared) + { + cmd.Prepare(); + } + await cmd.ExecuteNonQueryAsync(); + Fixture.SpannerMock.AddOrUpdateStatementResult(selectCountSql, StatementResult.CreateSelect1ResultSet()); + + Assert.That(conn.ExecuteScalar(selectCountSql), Is.EqualTo(1)); + await tx.RollbackAsync(); + Fixture.SpannerMock.AddOrUpdateStatementResult(selectCountSql, StatementResult.CreateSelectZeroResultSet()); + + Assert.That(tx.IsCompleted); + Assert.That(() => tx.Connection, Throws.Nothing); + Assert.That(await conn.ExecuteScalarAsync(selectCountSql), Is.EqualTo(0)); + } + Assert.That(() => tx.Connection, Throws.Exception.TypeOf()); + } + + [Test] + public async Task RollbackOnDispose() + { + const string insertSql = "INSERT INTO my_table (name) VALUES ('X')"; + Fixture.SpannerMock.AddOrUpdateStatementResult(insertSql, StatementResult.CreateUpdateCount(1L)); + + await using var conn = await OpenConnectionAsync(); + await using (var tx = await conn.BeginTransactionAsync()) + { + await conn.ExecuteNonQueryAsync(insertSql, tx: tx); + } + + // The rollback that is initiated by disposing the transaction is an async shoot-and-forget rollback request. + // So we need to wait a bit for it to show up on the mock server. + Fixture.SpannerMock.WaitForRequestsToContain(request => request is RollbackRequest, TimeSpan.FromSeconds(1)); + var requests = Fixture.SpannerMock.Requests + .Where(request => request is ExecuteSqlRequest or RollbackRequest or CommitRequest) + .Select(request => request.GetType()); + Assert.That(requests, Is.EqualTo(new[]{typeof(ExecuteSqlRequest), typeof(RollbackRequest)})); + } + + [Test] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + public async Task RollbackOnClose() + { + const string insertSql = "INSERT INTO my_table (name) VALUES ('X')"; + Fixture.SpannerMock.AddOrUpdateStatementResult(insertSql, StatementResult.CreateUpdateCount(1L)); + + using (var conn = await OpenConnectionAsync()) + { + var tx = await conn.BeginTransactionAsync(); + await conn.ExecuteNonQueryAsync(insertSql, tx); + } + + // The rollback that is initiated by closing the connection is an async shoot-and-forget rollback request. + // So we need to wait a bit for it to show up on the mock server. + Fixture.SpannerMock.WaitForRequestsToContain(request => request is RollbackRequest, TimeSpan.FromSeconds(1)); + var requests = Fixture.SpannerMock.Requests + .Where(request => request is ExecuteSqlRequest or RollbackRequest or CommitRequest) + .Select(request => request.GetType()); + Assert.That(requests, Is.EqualTo(new[]{typeof(ExecuteSqlRequest), typeof(RollbackRequest)})); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task RollbackFailedTransaction() + { + const string insertSql = "INSERT INTO my_table (name) VALUES ('X')"; + const string badQuery = "BAD QUERY"; + Fixture.SpannerMock.AddOrUpdateStatementResult(insertSql, StatementResult.CreateUpdateCount(1L)); + Fixture.SpannerMock.AddOrUpdateStatementResult(badQuery, StatementResult.CreateException(new RpcException(new Status(StatusCode.InvalidArgument, "Invalid SQL")))); + + await using var conn = await OpenConnectionAsync(); + + await using var tx = await conn.BeginTransactionAsync(); + await conn.ExecuteNonQueryAsync(insertSql, tx: tx); + Assert.That(async () => await conn.ExecuteNonQueryAsync(badQuery), Throws.Exception.TypeOf()); + tx.Rollback(); + Assert.That(tx.IsCompleted); + + var requests = Fixture.SpannerMock.Requests + .Where(request => request is ExecuteSqlRequest or RollbackRequest or CommitRequest) + .Select(request => request.GetType()); + Assert.That(requests, Is.EqualTo(new[] + { + typeof(ExecuteSqlRequest), + typeof(ExecuteSqlRequest), + typeof(RollbackRequest) + })); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task EmptyCommit() + { + await using var conn = await OpenConnectionAsync(); + await conn.BeginTransaction().CommitAsync(); + + // Empty transactions are a no-op. + var requests = Fixture.SpannerMock.Requests + .Where(request => request is BeginTransactionRequest or RollbackRequest or CommitRequest) + .Select(request => request.GetType()); + Assert.That(requests, Is.Empty); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task EmptyRollback() + { + await using var conn = await OpenConnectionAsync(); + await conn.BeginTransaction().RollbackAsync(); + + // Empty transactions are a no-op. + var requests = Fixture.SpannerMock.Requests + .Where(request => request is BeginTransactionRequest or RollbackRequest or CommitRequest) + .Select(request => request.GetType()); + Assert.That(requests, Is.Empty); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + public async Task EmptyDispose() + { + await using var dataSource = CreateDataSource(); + + using (var conn = await dataSource.OpenConnectionAsync()) + using (conn.BeginTransaction()) + { } + + using (var conn = await dataSource.OpenConnectionAsync()) + { + // Make sure the BeginTransaction from the previous connection did not carry over to the new connection. + Assert.That(async () => await conn.BeginTransactionAsync(), Throws.Nothing); + } + } + + [Test] + [TestCase(IsolationLevel.RepeatableRead, TransactionOptions.Types.IsolationLevel.RepeatableRead)] + [TestCase(IsolationLevel.Serializable, TransactionOptions.Types.IsolationLevel.Serializable)] + [TestCase(IsolationLevel.Snapshot, TransactionOptions.Types.IsolationLevel.RepeatableRead)] + [TestCase(IsolationLevel.Unspecified, TransactionOptions.Types.IsolationLevel.Unspecified)] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task SupportedIsolationLevels(IsolationLevel level, TransactionOptions.Types.IsolationLevel expectedSpannerLevel) + { + const string insertSql = "INSERT INTO my_table (name) VALUES ('X')"; + Fixture.SpannerMock.AddOrUpdateStatementResult(insertSql, StatementResult.CreateUpdateCount(1L)); + + await using var conn = await OpenConnectionAsync(); + var tx = conn.BeginTransaction(level); + await conn.ExecuteNonQueryAsync(insertSql, tx: tx); + + // TODO: Add support for this to the shared lib. + // Assert.That(conn.ExecuteScalar("SHOW TRANSACTION ISOLATION LEVEL"), Is.EqualTo(expectedSpannerLevel.ToString())); + await tx.CommitAsync(); + + var request = Fixture.SpannerMock.Requests.OfType().First(); + Assert.That(request.Transaction.Begin.IsolationLevel, Is.EqualTo(expectedSpannerLevel)); + } + + [Test] + [TestCase(IsolationLevel.Chaos)] + [TestCase(IsolationLevel.ReadUncommitted)] + [TestCase(IsolationLevel.ReadCommitted)] + public async Task UnsupportedIsolationLevels(IsolationLevel level) + { + await using var conn = await OpenConnectionAsync(); + Assert.That(() => conn.BeginTransaction(level), Throws.Exception.TypeOf()); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task RollbackTwice() + { + await using var conn = await OpenConnectionAsync(); + var transaction = conn.BeginTransaction(); + transaction.Rollback(); + Assert.That(() => transaction.Rollback(), Throws.Exception.TypeOf()); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task DefaultIsolationLevel() + { + await using var conn = await OpenConnectionAsync(); + var tx = conn.BeginTransaction(); + Assert.That(tx.IsolationLevel, Is.EqualTo(IsolationLevel.Unspecified)); + tx.Rollback(); + + tx = conn.BeginTransaction(IsolationLevel.Unspecified); + Assert.That(tx.IsolationLevel, Is.EqualTo(IsolationLevel.Unspecified)); + tx.Rollback(); + } + + [Test] + public async Task ViaSql() + { + const string insertSql = "INSERT INTO my_table (name) VALUES ('X')"; + Fixture.SpannerMock.AddOrUpdateStatementResult(insertSql, StatementResult.CreateUpdateCount(1L)); + + await using var conn = await OpenConnectionAsync(); + + await conn.ExecuteNonQueryAsync("BEGIN"); + await conn.ExecuteNonQueryAsync(insertSql); + await conn.ExecuteNonQueryAsync("ROLLBACK"); + + var requests = Fixture.SpannerMock.Requests + .Where(request => request is ExecuteSqlRequest or RollbackRequest or CommitRequest) + .Select(request => request.GetType()); + Assert.That(requests, Is.EqualTo(new[] + { + typeof(ExecuteSqlRequest), + typeof(RollbackRequest) + })); + var executeRequest = Fixture.SpannerMock.Requests.OfType().First(); + Assert.That(executeRequest.Transaction?.Begin?.ReadWrite, Is.Not.Null); + Assert.That(executeRequest.Transaction.Begin.IsolationLevel, Is.EqualTo(TransactionOptions.Types.IsolationLevel.Unspecified)); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task Nested() + { + await using var conn = await OpenConnectionAsync(); + conn.BeginTransaction(); + Assert.That(() => conn.BeginTransaction(), Throws.TypeOf()); + } + + [Test] + public void BeginTransactionOnClosedConnectionThrows() + { + using var conn = new SpannerConnection(); + Assert.That(() => conn.BeginTransaction(), Throws.Exception.TypeOf()); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task IsCompletedCommit() + { + const string insertSql = "INSERT INTO my_table (name) VALUES ('X')"; + Fixture.SpannerMock.AddOrUpdateStatementResult(insertSql, StatementResult.CreateUpdateCount(1L)); + + await using var conn = await OpenConnectionAsync(); + var tx = conn.BeginTransaction(); + Assert.That(!tx.IsCompleted); + await conn.ExecuteNonQueryAsync(insertSql, tx: tx); + Assert.That(!tx.IsCompleted); + await tx.CommitAsync(); + Assert.That(tx.IsCompleted); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task IsCompletedRollback() + { + const string insertSql = "INSERT INTO my_table (name) VALUES ('X')"; + Fixture.SpannerMock.AddOrUpdateStatementResult(insertSql, StatementResult.CreateUpdateCount(1L)); + + await using var conn = await OpenConnectionAsync(); + var tx = conn.BeginTransaction(); + Assert.That(!tx.IsCompleted); + await conn.ExecuteNonQueryAsync(insertSql, tx: tx); + Assert.That(!tx.IsCompleted); + tx.Rollback(); + Assert.That(tx.IsCompleted); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task IsCompletedRollbackFailedTransaction() + { + const string insertSql = "INSERT INTO my_table (name) VALUES ('X')"; + const string badQuery = "BAD QUERY"; + Fixture.SpannerMock.AddOrUpdateStatementResult(insertSql, StatementResult.CreateUpdateCount(1L)); + Fixture.SpannerMock.AddOrUpdateStatementResult(badQuery, StatementResult.CreateException(new RpcException(new Status(StatusCode.InvalidArgument, "Invalid SQL")))); + + await using var conn = await OpenConnectionAsync(); + var tx = conn.BeginTransaction(); + Assert.That(!tx.IsCompleted); + await conn.ExecuteNonQueryAsync(insertSql, tx: tx); + Assert.That(!tx.IsCompleted); + Assert.That(async () => await conn.ExecuteNonQueryAsync(badQuery), Throws.Exception.TypeOf()); + Assert.That(!tx.IsCompleted); + tx.Rollback(); + Assert.That(tx.IsCompleted); + } + + [Test] + [SuppressMessage("ReSharper", "UseAwaitUsing")] + public async Task DisposeTransactionRollbackOnOnlyFailedStatement() + { + const string sql = "SELECT * FROM unknown_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateException(new RpcException(new Status(StatusCode.NotFound, "Invalid table")))); + + using var conn = await OpenConnectionAsync(); + // Execute a read/write transaction with only a failed statement. + // This will only lead to a single ExecuteSqlRequest being sent to Spanner. + // That request fails to return a transaction ID, which again means that no Rollback will be sent to Spanner. + await using (var tx = await conn.BeginTransactionAsync()) + { + Assert.That(async () => await conn.ExecuteScalarAsync(sql, tx: tx), + Throws.Exception.TypeOf()); + } + + var requests = Fixture.SpannerMock.Requests + .Where(request => request is ExecuteSqlRequest or BeginTransactionRequest or RollbackRequest or CommitRequest) + .Select(request => request.GetType()); + Assert.That(requests, Is.EqualTo(new[]{typeof(ExecuteSqlRequest)})); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task DisposeConnectionRollbackOnOnlyFailedStatement() + { + const string sql = "SELECT * FROM unknown_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateException(new RpcException(new Status(StatusCode.NotFound, "Invalid table")))); + + // Execute a read/write transaction with only a failed statement. + // This will only lead to a single ExecuteSqlRequest being sent to Spanner. + // That request fails to return a transaction ID, which again means that no Rollback will be sent to Spanner. + var conn = await OpenConnectionAsync(); + var tx = conn.BeginTransaction(); + Assert.That(async () => await conn.ExecuteScalarAsync(sql, tx: tx), Throws.Exception.TypeOf()); + + await conn.DisposeAsync(); + + var requests = Fixture.SpannerMock.Requests + .Where(request => request is ExecuteSqlRequest or BeginTransactionRequest or RollbackRequest or CommitRequest) + .Select(request => request.GetType()); + Assert.That(requests, Is.EqualTo(new[]{typeof(ExecuteSqlRequest)})); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task DisposeConnectionRollbackFailedAndSucceededStatement() + { + const string insertSql = "INSERT INTO my_table (name) VALUES ('X')"; + const string sql = "SELECT * FROM unknown_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateException(new RpcException(new Status(StatusCode.NotFound, "Invalid table")))); + Fixture.SpannerMock.AddOrUpdateStatementResult(insertSql, StatementResult.CreateUpdateCount(1L)); + + // Execute a read/write transaction with only a failed statement. + // This will only lead to a single ExecuteSqlRequest being sent to Spanner. + // That request fails to return a transaction ID, which again means that no Rollback will be sent to Spanner. + var conn = await OpenConnectionAsync(); + var tx = conn.BeginTransaction(); + Assert.That(async () => await conn.ExecuteScalarAsync(sql, tx: tx), Throws.Exception.TypeOf()); + await conn.ExecuteNonQueryAsync(insertSql, tx: tx); + + await conn.DisposeAsync(); + + // The rollback that is initiated by disposing the transaction is an async shoot-and-forget rollback request. + // So we need to wait a bit for it to show up on the mock server. + Fixture.SpannerMock.WaitForRequestsToContain(request => request is RollbackRequest, TimeSpan.FromSeconds(1)); + var requests = Fixture.SpannerMock.Requests + .Where(request => request is ExecuteSqlRequest or BeginTransactionRequest or RollbackRequest or CommitRequest) + .Select(request => request.GetType()); + Assert.That(requests, Is.EqualTo(new[] + { + typeof(ExecuteSqlRequest), // This statement failed + typeof(BeginTransactionRequest), // This is sent because the first ExecuteSqlRequest failed + typeof(ExecuteSqlRequest), // This is a retry of the first statement in order to include it in the tx + typeof(ExecuteSqlRequest), // This is the successful insert + typeof(RollbackRequest), // This is the shoot-and-forget rollback from closing the connection + })); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task Bug3306(bool inTransactionBlock) + { + var conn = await OpenConnectionAsync(); + var tx = await conn.BeginTransactionAsync(); + await conn.ExecuteNonQueryAsync("SELECT 1", tx); + if (!inTransactionBlock) + { + await tx.RollbackAsync(); + } + await conn.CloseAsync(); + + conn = await OpenConnectionAsync(); + var tx2 = await conn.BeginTransactionAsync(); + + await tx.DisposeAsync(); + + Assert.That(tx.IsDisposed, Is.True); + Assert.That(tx2.IsDisposed, Is.False); + + await conn.DisposeAsync(); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task AccessConnectionOnCompletedTransaction() + { + await using var conn = await OpenConnectionAsync(); + await using var tx = await conn.BeginTransactionAsync(); + tx.Commit(); + Assert.That(tx.Connection, Is.Null); + } + + [Test] + public async Task CanAccessConnectionAfterCommit() + { + await using var dataSource = CreateDataSource(); + await using var conn = await dataSource.OpenConnectionAsync(); + await using var tx = await conn.BeginTransactionAsync(); + await conn.ExecuteNonQueryAsync("SELECT 1", tx); + await tx.CommitAsync(); + await conn.CloseAsync(); + Assert.DoesNotThrow(() => + { + _ = tx.Connection; + }); + } + } \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/Preconditions.cs b/drivers/spanner-ado-net/spanner-ado-net/Preconditions.cs new file mode 100644 index 00000000..c3b4867c --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/Preconditions.cs @@ -0,0 +1,25 @@ +// Copyright 2025 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 +// +// https://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. + +using System; + +namespace Google.Cloud.Spanner.DataProvider; + +internal static class Preconditions +{ + internal static int CheckIndexRange(int argument, string paramName, int minInclusive, int maxInclusive) => + argument < minInclusive || argument > maxInclusive ? + throw new IndexOutOfRangeException($"Value {argument} should be in range [{minInclusive}, {maxInclusive}]") : argument; + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs index 85e5b1a5..bc2f0661 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs @@ -45,7 +45,9 @@ public override int CommandTimeout public override UpdateRowSource UpdatedRowSource { get; set; } = UpdateRowSource.Both; protected override DbConnection? DbConnection { get; set; } + protected override DbParameterCollection DbParameterCollection { get; } = new SpannerParameterCollection(); + public new SpannerParameterCollection Parameters => (SpannerParameterCollection)DbParameterCollection; SpannerTransaction? _transaction; protected override DbTransaction? DbTransaction @@ -311,7 +313,10 @@ protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) GaxPreconditions.CheckState(_mutation == null, "Cannot execute mutations with ExecuteDbDataReader()"); try { - var rows = Execute(); + var mode = behavior.HasFlag(CommandBehavior.SchemaOnly) + ? ExecuteSqlRequest.Types.QueryMode.Plan + : ExecuteSqlRequest.Types.QueryMode.Normal; + var rows = Execute(mode); return new SpannerDataReader(SpannerConnection, rows, behavior); } catch (SpannerException exception) @@ -339,7 +344,10 @@ protected override async Task ExecuteDbDataReaderAsync(CommandBeha GaxPreconditions.CheckState(_mutation == null, "Cannot execute mutations with ExecuteDbDataReader()"); try { - var rows = await ExecuteAsync(cancellationToken); + var mode = behavior.HasFlag(CommandBehavior.SchemaOnly) + ? ExecuteSqlRequest.Types.QueryMode.Plan + : ExecuteSqlRequest.Types.QueryMode.Normal; + var rows = await ExecuteAsync(mode, cancellationToken); return new SpannerDataReader(SpannerConnection, rows, behavior); } catch (SpannerException exception) diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs index ba4ae0a4..7ae73c2f 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs @@ -20,10 +20,13 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; +using System.Transactions; using Google.Api.Gax; using Google.Cloud.Spanner.Common.V1; using Google.Cloud.Spanner.V1; using Google.Cloud.SpannerLib; +using IsolationLevel = System.Data.IsolationLevel; +using TransactionOptions = Google.Cloud.Spanner.V1.TransactionOptions; namespace Google.Cloud.Spanner.DataProvider; @@ -99,10 +102,15 @@ public override string ServerVersion get { AssertOpen(); - return Assembly.GetAssembly(typeof(Connection))?.GetName().Version?.ToString() ?? ""; + // TODO: Return an actual version number + return "1.0.0"; } } + internal Version ServerVersionNormalized => Version.Parse(ServerVersion); + + internal string ServerVersionNormalizedString => FormattableString.Invariant($"{ServerVersionNormalized.Major:000}.{ServerVersionNormalized.Minor:000}.{ServerVersionNormalized.Build:0000}"); + public override bool CanCreateBatch => true; private bool _disposed; @@ -123,7 +131,13 @@ internal Connection? LibConnection internal uint DefaultCommandTimeout => _connectionStringBuilder?.CommandTimeout ?? 0; private SpannerTransaction? _transaction; - + + private System.Transactions.Transaction? EnlistedTransaction { get; set; } + + private SpannerSchemaProvider? _mSchemaProvider; + + private SpannerSchemaProvider GetSchemaProvider() => _mSchemaProvider ??= new SpannerSchemaProvider(this); + public SpannerConnection() { } @@ -140,6 +154,40 @@ public SpannerConnection(SpannerConnectionStringBuilder connectionStringBuilder) _connectionStringBuilder = connectionStringBuilder; _connectionString = connectionStringBuilder.ConnectionString; } + + public new ValueTask BeginTransactionAsync(CancellationToken cancellationToken = default) + => BeginTransactionAsync(IsolationLevel.Unspecified, cancellationToken); + + public new ValueTask BeginTransactionAsync(IsolationLevel isolationLevel, + CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + try + { + return new ValueTask(BeginTransaction(new TransactionOptions + { + IsolationLevel = SpannerTransaction.TranslateIsolationLevel(isolationLevel), + })); + } + catch (Exception e) + { + return ValueTask.FromException(e); + } + } + + public new SpannerTransaction BeginTransaction() => BeginTransaction(IsolationLevel.Unspecified); + + public new SpannerTransaction BeginTransaction(IsolationLevel isolationLevel) + { + return BeginTransaction(new TransactionOptions + { + IsolationLevel = SpannerTransaction.TranslateIsolationLevel(isolationLevel), + }); + } protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) { @@ -149,7 +197,7 @@ protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLeve }); } - public DbTransaction BeginReadOnlyTransaction() + public SpannerTransaction BeginReadOnlyTransaction() { return BeginTransaction(new TransactionOptions { @@ -163,7 +211,7 @@ public DbTransaction BeginReadOnlyTransaction() /// The options to use for the new transaction /// The new transaction /// If the connection has an active transaction - public DbTransaction BeginTransaction(TransactionOptions transactionOptions) + public SpannerTransaction BeginTransaction(TransactionOptions transactionOptions) { EnsureOpen(); GaxPreconditions.CheckState(!HasTransaction, "This connection has a transaction."); @@ -296,6 +344,8 @@ private void AssertClosed() EnsureOpen(); return LibConnection!.WriteMutationsAsync(mutations, cancellationToken); } + + public new SpannerCommand CreateCommand() => (SpannerCommand) base.CreateCommand(); protected override DbCommand CreateDbCommand() { @@ -343,5 +393,12 @@ public DbCommand CreateDeleteCommand(string table) { return new SpannerCommand(this, new Mutation { Delete = new Mutation.Types.Delete { Table = table } }); } + + public override DataTable GetSchema() => GetSchemaProvider().GetSchema(); + + public override DataTable GetSchema(string collectionName) + => GetSchema(collectionName, null); + public override DataTable GetSchema(string collectionName, string?[]? restrictionValues) + => GetSchemaProvider().GetSchema(collectionName, restrictionValues); } \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs index 96f86600..902a9f5a 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs @@ -19,6 +19,7 @@ using System.Data.Common; using System.Globalization; using System.IO; +using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; @@ -76,7 +77,7 @@ public override bool HasRows } return CheckForRows(); } - } + } public override bool IsClosed => _closed; public override int Depth => 0; @@ -117,6 +118,7 @@ public override bool Read() _hasReadData = true; _currentRow = LibRows.Next(); } + _hasData = _hasData || _currentRow != null; return _currentRow != null; } @@ -129,7 +131,7 @@ public override async Task ReadAsync(CancellationToken cancellationToken) _hasReadData = true; _currentRow = await LibRows.NextAsync(cancellationToken); } - + _hasData = _hasData || _currentRow != null; return _currentRow != null; } catch (SpannerException exception) @@ -158,7 +160,7 @@ private bool InternalRead() private bool CheckForRows() { - _tempRow = LibRows.Next(); + _tempRow ??= LibRows.Next(); return _tempRow != null; } @@ -290,35 +292,42 @@ public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int { CheckValidPosition(); CheckValidOrdinal(ordinal); - GaxPreconditions.CheckState(LibRows.Metadata!.RowType.Fields[ordinal].Type.Code == TypeCode.Bytes, - "Spanner only supports conversion to byte arrays for columns of type BYTES."); - GaxPreconditions.CheckArgumentRange(bufferOffset, nameof(bufferOffset), 0, buffer?.Length ?? 0); - GaxPreconditions.CheckArgumentRange(length, nameof(length), 0, buffer?.Length ?? int.MaxValue); + CheckNotNull(ordinal); + var code = LibRows.Metadata!.RowType.Fields[ordinal].Type.Code; + GaxPreconditions.CheckState(Array.Exists([TypeCode.Bytes, TypeCode.Json, TypeCode.String], c => c == code), + "Spanner only supports conversion to byte arrays for columns of type BYTES or STRING."); + Preconditions.CheckIndexRange(bufferOffset, nameof(bufferOffset), 0, buffer?.Length ?? 0); + Preconditions.CheckIndexRange(length, nameof(length), 0, buffer?.Length ?? int.MaxValue); if (buffer != null) { - GaxPreconditions.CheckArgumentRange(bufferOffset + length, nameof(length), 0, buffer.Length); + Preconditions.CheckIndexRange(bufferOffset + length, nameof(length), 0, buffer.Length); } - var bytes = IsDBNull(ordinal) ? null : GetFieldValue(ordinal); + byte[] bytes; + if (code == TypeCode.Bytes) + { + bytes = GetFieldValue(ordinal); + } + else + { + var s = GetFieldValue(ordinal); + bytes = Encoding.UTF8.GetBytes(s); + } if (buffer == null) { // Return the length of the value if `buffer` is null: // https://docs.microsoft.com/en-us/dotnet/api/system.data.idatarecord.getbytes?view=netstandard-2.1#remarks - return bytes?.Length ?? 0; + return bytes.Length; } - var copyLength = Math.Min(length, (bytes?.Length ?? 0) - (int)dataOffset); + var copyLength = Math.Min(length, bytes.Length - (int)dataOffset); if (copyLength < 0) { // Read nothing and just return. return 0; } - - if (bytes != null) - { - Array.Copy(bytes, (int)dataOffset, buffer, bufferOffset, copyLength); - } - + + Array.Copy(bytes, (int)dataOffset, buffer, bufferOffset, copyLength); return copyLength; } @@ -333,10 +342,11 @@ public override char GetChar(int ordinal) } if (value.HasStringValue) { - if (value.StringValue.Length == 1) + if (value.StringValue.Length == 0) { - return value.StringValue[0]; + throw new InvalidCastException("not a valid char value"); } + return value.StringValue[0]; } throw new InvalidCastException("not a valid char value"); } @@ -344,13 +354,14 @@ public override char GetChar(int ordinal) public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length) { var value = GetProtoValue(ordinal); - if (value.HasNullValue) + var code = GetSpannerType(ordinal).Code; + if (!Array.Exists([TypeCode.Bytes, TypeCode.Json, TypeCode.String], c => c == code)) { - return 0; + throw new InvalidCastException("not a valid type for getting as chars"); } - if (!value.HasStringValue) + if (value.HasNullValue) { - throw new DataException("not a valid type for getting as chars"); + return 0; } if (buffer == null) { @@ -358,8 +369,8 @@ public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int // https://docs.microsoft.com/en-us/dotnet/api/system.data.idatarecord.getbytes?view=netstandard-2.1#remarks return value.StringValue.ToCharArray().Length; } - GaxPreconditions.CheckArgumentRange(bufferOffset, nameof(bufferOffset), 0, buffer.Length); - GaxPreconditions.CheckArgumentRange(length, nameof(length), 0, buffer.Length - bufferOffset); + Preconditions.CheckIndexRange(bufferOffset, nameof(bufferOffset), 0, buffer.Length); + Preconditions.CheckIndexRange(length, nameof(length), 0, buffer.Length - bufferOffset); var intDataOffset = (int)dataOffset; var sourceLength = Math.Min(length, value.StringValue.Length - intDataOffset); @@ -375,14 +386,13 @@ public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int return 0; } - // TODO: Optimize - var chars = value.StringValue.ToCharArray(intDataOffset, sourceLength); + var chars = value.StringValue.ToCharArray(); if (intDataOffset >= chars.Length) { return 0; } - Array.Copy(chars, 0, buffer, bufferOffset, destLength); + Array.Copy(chars, dataOffset, buffer, bufferOffset, destLength); return destLength; } @@ -619,6 +629,8 @@ public override long GetInt64(int ordinal) } throw new InvalidCastException("not a valid Int64 value"); } + + public TimeSpan GetTimeSpan(int ordinal) => GetFieldValue(ordinal); public override string GetName(int ordinal) { @@ -629,6 +641,7 @@ public override string GetName(int ordinal) public override int GetOrdinal(string name) { CheckNotClosed(); + // First try with case sensitivity. for (var i = 0; i < LibRows.Metadata?.RowType.Fields.Count; i++) { if (Equals(LibRows.Metadata?.RowType.Fields[i].Name, name)) @@ -636,12 +649,21 @@ public override int GetOrdinal(string name) return i; } } + // Nothing found, try with case-insensitive comparison. + for (var i = 0; i < LibRows.Metadata?.RowType.Fields.Count; i++) + { + if (string.Equals(LibRows.Metadata?.RowType.Fields[i].Name, name, StringComparison.InvariantCultureIgnoreCase)) + { + return i; + } + } throw new IndexOutOfRangeException($"No column with name {name} found"); } public override T GetFieldValue(int ordinal) { CheckNotClosed(); + CheckValidPosition(); CheckValidOrdinal(ordinal); if (typeof(T) == typeof(Stream)) { @@ -653,32 +675,60 @@ public override T GetFieldValue(int ordinal) CheckNotNull(ordinal); return (T)(object)GetTextReader(ordinal); } - if (typeof(T) == typeof(char)) + if (typeof(T) == typeof(char) || typeof(T) == typeof(char?)) { + if (IsDBNull(ordinal) && typeof(T) == typeof(char?)) + { + return (T)(object)null!; + } return (T)(object)GetChar(ordinal); } - if (typeof(T) == typeof(DateTime)) + if (typeof(T) == typeof(DateTime) || typeof(T) == typeof(DateTime?)) { + if (IsDBNull(ordinal) && typeof(T) == typeof(DateTime?)) + { + return (T)(object)null!; + } return (T)(object)GetDateTime(ordinal); } - if (typeof(T) == typeof(double)) + if (typeof(T) == typeof(double) || typeof(T) == typeof(double?)) { + if (IsDBNull(ordinal) && typeof(T) == typeof(double?)) + { + return (T)(object)null!; + } return (T)(object)GetDouble(ordinal); } - if (typeof(T) == typeof(float)) + if (typeof(T) == typeof(float) || typeof(T) == typeof(float?)) { + if (IsDBNull(ordinal) && typeof(T) == typeof(float?)) + { + return (T)(object)null!; + } return (T)(object)GetFloat(ordinal); } - if (typeof(T) == typeof(Int16)) + if (typeof(T) == typeof(Int16) || typeof(T) == typeof(Int16?)) { + if (IsDBNull(ordinal) && typeof(T) == typeof(Int16?)) + { + return (T)(object)null!; + } return (T)(object)GetInt16(ordinal); } - if (typeof(T) == typeof(int)) + if (typeof(T) == typeof(int) || typeof(T) == typeof(int?)) { + if (IsDBNull(ordinal) && typeof(T) == typeof(int?)) + { + return (T)(object)null!; + } return (T)(object)GetInt32(ordinal); } - if (typeof(T) == typeof(long)) + if (typeof(T) == typeof(long) || typeof(T) == typeof(long?)) { + if (IsDBNull(ordinal) && typeof(T) == typeof(long?)) + { + return (T)(object)null!; + } return (T)(object)GetInt64(ordinal); } @@ -726,7 +776,7 @@ private static object GetUnderlyingValue(Google.Cloud.Spanner.V1.Type type, Valu case TypeCode.Int64: return long.Parse(value.StringValue); case TypeCode.Interval: - return TimeSpan.Parse(value.StringValue); + return XmlConvert.ToTimeSpan(value.StringValue); case TypeCode.Json: return value.StringValue; case TypeCode.Numeric: diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerParameter.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerParameter.cs index 0b1bd774..9b6187d8 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerParameter.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerParameter.cs @@ -29,7 +29,7 @@ namespace Google.Cloud.Spanner.DataProvider; -public class SpannerParameter : DbParameter, ICloneable +public class SpannerParameter : DbParameter, IDbDataParameter, ICloneable { private DbType? _dbType; @@ -49,6 +49,10 @@ public override DbType DbType public override ParameterDirection Direction { get; set; } = ParameterDirection.Input; public override bool IsNullable { get; set; } + public new byte Precision { get; set; } + + public new byte Scale { get; set; } + private string _name = ""; [AllowNull] public override string ParameterName { @@ -66,7 +70,10 @@ public override string SourceColumn } public sealed override object? Value { get; set; } public override bool SourceColumnNullMapping { get; set; } + + // TODO: Size should truncate the value to any explicit size that is set. public override int Size { get; set; } + public override DataRowVersion SourceVersion { get => DataRowVersion.Current; @@ -198,8 +205,12 @@ public SpannerParameter Clone() { var clone = new SpannerParameter(_name, Value) { + _dbType = _dbType, Direction = Direction, IsNullable = IsNullable, + Precision = Precision, + Scale = Scale, + Size = Size, SourceColumn = SourceColumn, SourceVersion = SourceVersion, SourceColumnNullMapping = SourceColumnNullMapping, diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerParameterCollection.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerParameterCollection.cs index caf2277f..26691ae5 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerParameterCollection.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerParameterCollection.cs @@ -47,6 +47,17 @@ public override int Add(object value) return index; } + + public SpannerParameter AddWithValue(string parameterName, object? value) + { + var parameter = new SpannerParameter + { + ParameterName = parameterName, + Value = value, + }; + Add(parameter); + return parameter; + } public override void Clear() { diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerSchemaProvider.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerSchemaProvider.cs new file mode 100644 index 00000000..73ecdc32 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerSchemaProvider.cs @@ -0,0 +1,359 @@ +// Copyright 2025 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 +// +// https://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. + +using System; +using System.Data; +using System.Data.Common; +using Google.Api.Gax; +using TypeCode = Google.Cloud.Spanner.V1.TypeCode; + +namespace Google.Cloud.Spanner.DataProvider; + +internal sealed class SpannerSchemaProvider(SpannerConnection connection) +{ + internal DataTable GetSchema() => GetSchema("MetaDataCollections", null); + + internal DataTable GetSchema(string collectionName, string?[]? restrictionValues) + { + GaxPreconditions.CheckNotNull(collectionName, nameof(collectionName)); + var dataTable = new DataTable(); + if (string.Equals(collectionName, DbMetaDataCollectionNames.MetaDataCollections, StringComparison.OrdinalIgnoreCase)) + { + FillMetaDataCollections(dataTable, restrictionValues); + } + else if (string.Equals(collectionName, DbMetaDataCollectionNames.DataSourceInformation, StringComparison.OrdinalIgnoreCase)) + { + FillDataSourceInformation(dataTable, restrictionValues); + } + else if (string.Equals(collectionName, DbMetaDataCollectionNames.DataTypes, StringComparison.OrdinalIgnoreCase)) + { + FillDataTypes(dataTable, restrictionValues); + } + else if (string.Equals(collectionName, DbMetaDataCollectionNames.ReservedWords, StringComparison.OrdinalIgnoreCase)) + { + FillReservedWords(dataTable, restrictionValues); + } + else if (string.Equals(collectionName, DbMetaDataCollectionNames.Restrictions, StringComparison.OrdinalIgnoreCase)) + { + FillRestrictions(dataTable, restrictionValues); + } + else + { + throw new ArgumentException($"Invalid collection name: '{collectionName}'.", nameof(collectionName)); + } + return dataTable; + } + + private void FillMetaDataCollections(DataTable dataTable, string?[]? restrictionValues) + { + GaxPreconditions.CheckArgument(restrictionValues == null || restrictionValues.Length == 0, nameof(restrictionValues), "restrictionValues is not supported for schema 'MetaDataCollections'."); + + dataTable.TableName = DbMetaDataCollectionNames.MetaDataCollections; + dataTable.Columns.AddRange( + [ + new("CollectionName", typeof(string)), + new("NumberOfRestrictions", typeof(int)), + new("NumberOfIdentifierParts", typeof(int)), + ]); + + dataTable.Rows.Add(DbMetaDataCollectionNames.MetaDataCollections, 0, 0); + dataTable.Rows.Add(DbMetaDataCollectionNames.DataSourceInformation, 0, 0); + dataTable.Rows.Add(DbMetaDataCollectionNames.DataTypes, 0, 0); + dataTable.Rows.Add(DbMetaDataCollectionNames.ReservedWords, 0, 0); + dataTable.Rows.Add(DbMetaDataCollectionNames.Restrictions, 0, 0); + } + + private void FillDataSourceInformation(DataTable dataTable, string?[]? restrictionValues) + { + GaxPreconditions.CheckArgument(restrictionValues == null || restrictionValues.Length == 0, nameof(restrictionValues), "restrictionValues is not supported for schema 'DataSourceInformation'."); + + dataTable.TableName = DbMetaDataCollectionNames.DataSourceInformation; + dataTable.Columns.AddRange( + [ + new("CompositeIdentifierSeparatorPattern", typeof(string)), + new("DataSourceProductName", typeof(string)), + new("DataSourceProductVersion", typeof(string)), + new("DataSourceProductVersionNormalized", typeof(string)), + new("GroupByBehavior", typeof(GroupByBehavior)), + new("IdentifierPattern", typeof(string)), + new("IdentifierCase", typeof(IdentifierCase)), + new("OrderByColumnsInSelect", typeof(bool)), + new("ParameterMarkerFormat", typeof(string)), + new("ParameterMarkerPattern", typeof(string)), + new("ParameterNameMaxLength", typeof(int)), + new("QuotedIdentifierPattern", typeof(string)), + new("QuotedIdentifierCase", typeof(IdentifierCase)), + new("ParameterNamePattern", typeof(string)), + new("StatementSeparatorPattern", typeof(string)), + new("StringLiteralPattern", typeof(string)), + new("SupportedJoinOperators", typeof(SupportedJoinOperators)), + ]); + + var row = dataTable.NewRow(); + row["CompositeIdentifierSeparatorPattern"] = @"\."; + row["DataSourceProductName"] = "Spanner"; + row["DataSourceProductVersion"] = connection.ServerVersion; + row["DataSourceProductVersionNormalized"] = connection.ServerVersionNormalizedString; + row["GroupByBehavior"] = GroupByBehavior.Unrelated; + row["IdentifierPattern"] = @"(^\[\p{Lo}\p{Lu}\p{Ll}][\p{Lo}\p{Lu}\p{Ll}\p{Nd}_]*$)"; + row["IdentifierCase"] = IdentifierCase.Insensitive; + row["OrderByColumnsInSelect"] = false; + row["ParameterMarkerFormat"] = "{0}"; + row["ParameterMarkerPattern"] = "(@[A-Za-z0-9_]*)"; + row["ParameterNameMaxLength"] = 128; + row["QuotedIdentifierPattern"] = @"(([^`]|\\`)+)"; + row["QuotedIdentifierCase"] = IdentifierCase.Insensitive; + row["ParameterNamePattern"] = @"[\p{Lo}\p{Lu}\p{Ll}\p{Lm}][\p{Lo}\p{Lu}\p{Ll}\p{Lm}\p{Nd}_]*"; + row["StatementSeparatorPattern"] = ";"; + row["StringLiteralPattern"] = @"'(([^']|'')*)'"; + row["SupportedJoinOperators"] = + SupportedJoinOperators.FullOuter | + SupportedJoinOperators.Inner | + SupportedJoinOperators.LeftOuter | + SupportedJoinOperators.RightOuter; + dataTable.Rows.Add(row); + } + + private void FillDataTypes(DataTable dataTable, string?[]? restrictionValues) + { + GaxPreconditions.CheckArgument(restrictionValues == null || restrictionValues.Length == 0, nameof(restrictionValues), "restrictionValues is not supported for schema 'DataTypes'."); + + dataTable.TableName = DbMetaDataCollectionNames.DataTypes; + dataTable.Columns.AddRange( + [ + new("TypeName", typeof(string)), + new("ProviderDbType", typeof(int)), + new("ColumnSize", typeof(long)), + new("CreateFormat", typeof(string)), + new("CreateParameters", typeof(string)), + new("DataType", typeof(string)), + new("IsAutoIncrementable", typeof(bool)), + new("IsBestMatch", typeof(bool)), + new("IsCaseSensitive", typeof(bool)), + new("IsFixedLength", typeof(bool)), + new("IsFixedPrecisionScale", typeof(bool)), + new("IsLong", typeof(bool)), + new("IsNullable", typeof(bool)), + new("IsSearchable", typeof(bool)), + new("IsSearchableWithLike", typeof(bool)), + new("IsUnsigned", typeof(bool)), + new("MaximumScale", typeof(short)), + new("MinimumScale", typeof(short)), + new("IsConcurrencyType", typeof(bool)), + new("IsLiteralSupported", typeof(bool)), + new("LiteralPrefix", typeof(string)), + new("LiteralSuffix", typeof(string)), + new("NativeDataType", typeof(string)), + ]); + + foreach (var code in Enum.GetValues()) + { + // These are not supported as stored column types. + if (code == TypeCode.Unspecified || code == TypeCode.Struct) + { + continue; + } + // TODO: Add arrays + if (code == TypeCode.Array) + { + continue; + } + + var clrType = TypeConversion.GetSystemType(code); + var clrTypeName = clrType.ToString(); + // Both STRING and JSON are mapped to System.String. STRING is the best match. + // Both ENUM and INT64 are mapped to System.Int64. INT64 is the best match. + // Both PROTO and BYTES are mapped to System.Byte[]. BYTES is the best match. + var isBestMatch = code != TypeCode.Json && code != TypeCode.Enum && code != TypeCode.Proto; + var dataTypeName = code.ToString(); + var isAutoIncrementable = code == TypeCode.Int64; + var isFixedLength = code != TypeCode.String && code != TypeCode.Bytes && code != TypeCode.Json && code != TypeCode.Proto; + var createFormat = isFixedLength + ? dataTypeName + : dataTypeName + "({0})"; + var createParameters = isFixedLength ? "" : "length"; + var isFixedPrecisionScale = isFixedLength; + var isLong = false; + var columnSize = 0; + var isCaseSensitive = code == TypeCode.String || code == TypeCode.Json; + var isSearchableWithLike = code == TypeCode.String; + object isUnsigned = code == TypeCode.Int64 || code == TypeCode.Float32 || code == TypeCode.Float64 || + code == TypeCode.Numeric ? false : DBNull.Value; + var literalPrefix = $" {code.ToString()}"; + + var row = dataTable.NewRow(); + row["TypeName"] = dataTypeName; + row["ProviderDbType"] = (int) code; + row["ColumnSize"] = columnSize; + row["CreateFormat"] = createFormat; + row["CreateParameters"] = createParameters; + row["DataType"] = clrTypeName; + row["IsAutoIncrementable"] = isAutoIncrementable; + row["IsBestMatch"] = isBestMatch; + row["IsCaseSensitive"] = isCaseSensitive; + row["IsFixedLength"] = isFixedLength; + row["IsFixedPrecisionScale"] = isFixedPrecisionScale; + row["IsLong"] = isLong; + row["IsNullable"] = true; + row["IsSearchable"] = true; + row["IsSearchableWithLike"] = isSearchableWithLike; + row["IsUnsigned"] = isUnsigned; + row["MaximumScale"] = DBNull.Value; + row["MinimumScale"] = DBNull.Value; + row["IsConcurrencyType"] = false; + row["IsLiteralSupported"] = true; + row["LiteralPrefix"] = literalPrefix; + row["LiteralSuffix"] = DBNull.Value; + row["NativeDataType"] = DBNull.Value; + + dataTable.Rows.Add(row); + } + } + private static void FillRestrictions(DataTable dataTable, string?[]? restrictionValues) + { + GaxPreconditions.CheckArgument(restrictionValues == null || restrictionValues.Length == 0, nameof(restrictionValues), "restrictionValues is not supported for schema 'Restrictions'."); + + dataTable.TableName = DbMetaDataCollectionNames.Restrictions; + dataTable.Columns.AddRange( + [ + new("CollectionName", typeof(string)), + new("RestrictionName", typeof(string)), + new("RestrictionDefault", typeof(string)), + new("RestrictionNumber", typeof(int)), + ]); + + dataTable.Rows.Add("Columns", "Catalog", "TABLE_CATALOG", 1); + dataTable.Rows.Add("Columns", "Schema", "TABLE_SCHEMA", 2); + dataTable.Rows.Add("Columns", "Table", "TABLE_NAME", 3); + dataTable.Rows.Add("Columns", "Column", "COLUMN_NAME", 4); + dataTable.Rows.Add("Tables", "Catalog", "TABLE_CATALOG", 1); + dataTable.Rows.Add("Tables", "Schema", "TABLE_SCHEMA", 2); + dataTable.Rows.Add("Tables", "Table", "TABLE_NAME", 3); + dataTable.Rows.Add("Tables", "TableType", "TABLE_TYPE", 4); + dataTable.Rows.Add("Foreign Keys", "Catalog", "TABLE_CATALOG", 1); + dataTable.Rows.Add("Foreign Keys", "Schema", "TABLE_SCHEMA", 2); + dataTable.Rows.Add("Foreign Keys", "Table", "TABLE_NAME", 3); + dataTable.Rows.Add("Foreign Keys", "Constraint Name", "CONSTRAINT_NAME", 4); + dataTable.Rows.Add("Indexes", "Catalog", "TABLE_CATALOG", 1); + dataTable.Rows.Add("Indexes", "Schema", "TABLE_SCHEMA", 2); + dataTable.Rows.Add("Indexes", "Table", "TABLE_NAME", 3); + dataTable.Rows.Add("Indexes", "Name", "INDEX_NAME", 4); + dataTable.Rows.Add("IndexColumns", "Catalog", "TABLE_CATALOG", 1); + dataTable.Rows.Add("IndexColumns", "Schema", "TABLE_SCHEMA", 2); + dataTable.Rows.Add("IndexColumns", "Table", "TABLE_NAME", 3); + dataTable.Rows.Add("IndexColumns", "Name", "INDEX_NAME", 4); + dataTable.Rows.Add("IndexColumns", "Column", "COLUMN_NAME", 5); + } + + private static void FillReservedWords(DataTable dataTable, string?[]? restrictionValues) + { + GaxPreconditions.CheckArgument(restrictionValues == null || restrictionValues.Length == 0, nameof(restrictionValues), "restrictionValues is not supported for schema 'ReservedWords'."); + + dataTable.TableName = DbMetaDataCollectionNames.ReservedWords; + dataTable.Columns.AddRange( + [ + new("ReservedWord", typeof(string)), + ]); + + var keywords = new [] + { + "ALL", + "AND", + "ANY", + "ARRAY", + "AS", + "ASC", + "AT", + "BETWEEN", + "BY", + "CASE", + "CAST", + "CHECK", + "COLUMN", + "COMMIT", + "CONSTRAINT", + "CREATE", + "CROSS", + "CUBE", + "CURRENT", + "DEFAULT", + "DELETE", + "DESC", + "DESCENDING", + "DISTINCT", + "DROP", + "ELSE", + "END", + "ESCAPE", + "EXCEPT", + "EXISTS", + "FALSE", + "FETCH", + "FOLLOWING", + "FOR", + "FOREIGN", + "FROM", + "FULL", + "GROUP", + "GROUPING", + "HAVING", + "IN", + "INNER", + "INSERT", + "INTERSECT", + "INTERVAL", + "INTO", + "IS", + "JOIN", + "LEFT", + "LIKE", + "LIMIT", + "NOT", + "NULL", + "ON", + "OR", + "ORDER", + "OUTER", + "PARTITION", + "PRECEDING", + "PRIMARY", + "REFERENCES", + "RIGHT", + "ROLLUP", + "ROW", + "ROWS", + "SELECT", + "SET", + "SOME", + "TABLE", + "THEN", + "TO", + "TRUE", + "UNBOUNDED", + "UNION", + "UNNEST", + "UPDATE", + "USING", + "VALUES", + "WHEN", + "WHERE", + "WINDOW", + "WITH" + }; + foreach (string word in keywords) + { + dataTable.Rows.Add(word); + } + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs index b283b980..b3b2e1f5 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs @@ -25,12 +25,25 @@ namespace Google.Cloud.Spanner.DataProvider; public class SpannerTransaction : DbTransaction { private SpannerConnection? _spannerConnection; - - protected override DbConnection? DbConnection => _spannerConnection; + + protected override DbConnection? DbConnection + { + get + { + CheckDisposed(); + return _spannerConnection; + } + } public override IsolationLevel IsolationLevel { get; } + + // TODO: Implement savepoint support in the shared library. + public override bool SupportsSavepoints => false; + private SpannerLib.Connection LibConnection { get; } internal bool IsCompleted => _spannerConnection == null; + + internal bool IsDisposed => _disposed; private bool _disposed; @@ -142,7 +155,7 @@ private Task EndTransactionAsync(Func endTransactionMethod) { return endTransactionMethod(); } - finally + finally { _spannerConnection?.ClearTransaction(); _spannerConnection = null; diff --git a/drivers/spanner-ado-net/spanner-ado-net/TypeConversion.cs b/drivers/spanner-ado-net/spanner-ado-net/TypeConversion.cs index c31c4fc5..d0bd9a1c 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/TypeConversion.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/TypeConversion.cs @@ -63,9 +63,11 @@ static TypeConversion() return dbType == null ? null : SDbTypeToSpannerTypeMapping.GetValueOrDefault(dbType.Value); } - internal static System.Type GetSystemType(V1.Type type) + internal static System.Type GetSystemType(V1.Type type) => GetSystemType(type.Code); + + internal static System.Type GetSystemType(TypeCode code) { - return type.Code switch + return code switch { TypeCode.Bool => typeof(bool), TypeCode.Bytes => typeof(byte[]), diff --git a/parser/simple_parser.go b/parser/simple_parser.go index e8fef032..aa15af82 100644 --- a/parser/simple_parser.go +++ b/parser/simple_parser.go @@ -138,7 +138,7 @@ func (p *simpleParser) eatDollarTag() (string, bool) { return "", false } } else { - if p.eatToken('$') { + if p.eatTokenWithWhitespaceOption('$' /*eatWhiteSpaces=*/, false) { return string(p.sql[startPos : p.pos-1]), true } if !p.isValidIdentifierChar() { @@ -393,12 +393,18 @@ func (p *simpleParser) skipStatementHint() bool { // isMultibyte returns true if the character at the current position // is a multibyte utf8 character. func (p *simpleParser) isMultibyte() bool { + if p.pos >= len(p.sql) { + return false + } return isMultibyte(p.sql[p.pos]) } // nextChar moves the parser to the next character. This takes into // account that some characters could be multibyte characters. func (p *simpleParser) nextChar() { + if p.pos >= len(p.sql) { + return + } if !p.isMultibyte() { p.pos++ return diff --git a/parser/statement_parser.go b/parser/statement_parser.go index ef0875a8..c41004d8 100644 --- a/parser/statement_parser.go +++ b/parser/statement_parser.go @@ -402,6 +402,17 @@ func (p *StatementParser) skipMultiLineComment(sql []byte, pos int) int { // could not be read. // The quote length is either 1 for normal quoted strings, and 3 for triple-quoted string. func (p *StatementParser) skipQuoted(sql []byte, pos int, quote byte) (int, int, error) { + isEscapeString := false + if !p.supportsBackslashEscape() && pos > 0 { + // TODO: Also implement support for the standard_conforming_strings property in PostgreSQL. + // See https://www.postgresql.org/docs/current/runtime-config-compatible.html#GUC-STANDARD-CONFORMING-STRINGS + // Check if it is an escape-string. This enables the use of a backslash to start an escape sequence, even if + // the dialect normally does not support that. Escape strings start with an e or E, e.g. "e'It\'s valid'". + // The second part of the check is to verify that the e or E is not part of a keyword, e.g. WHERE. + // The following is valid SQL, but does not designate an escape-string: + // SELECT * FROM my_table WHERE'test'=col1; + isEscapeString = (sql[pos-1] == 'e' || sql[pos-1] == 'E') && (pos == 1 || !isLatinLetter(sql[pos-2])) + } isTripleQuoted := p.supportsTripleQuotedLiterals() && len(sql) > pos+2 && sql[pos+1] == quote && sql[pos+2] == quote if isTripleQuoted && (isMultibyte(sql[pos+1]) || isMultibyte(sql[pos+2])) { isTripleQuoted = false @@ -434,7 +445,7 @@ func (p *StatementParser) skipQuoted(sql []byte, pos int, quote byte) (int, int, // This was the end quote. return pos + 1, quoteLength, nil } - } else if p.supportsBackslashEscape() && len(sql) > pos+1 && c == '\\' && sql[pos+1] == quote { + } else if (p.supportsBackslashEscape() || isEscapeString) && len(sql) > pos+1 && c == '\\' && sql[pos+1] == quote { // This is an escaped quote (e.g. 'foo\'bar'). // Note that in raw strings, the \ officially does not start an // escape sequence, but the result is still the same, as in a raw diff --git a/parser/statement_parser_test.go b/parser/statement_parser_test.go index 2ab64399..2e5ba147 100644 --- a/parser/statement_parser_test.go +++ b/parser/statement_parser_test.go @@ -897,6 +897,32 @@ SELECT * FROM PersonsTable WHERE id=@id`, ?it\'?s'?`)), }, }, + "e-string": { + input: `SELECT e'ab\'c?'`, + wantSQL: map[databasepb.DatabaseDialect]string{ + databasepb.DatabaseDialect_GOOGLE_STANDARD_SQL: `SELECT e'ab\'c?'`, + databasepb.DatabaseDialect_POSTGRESQL: `SELECT e'ab\'c?'`, + }, + want: map[databasepb.DatabaseDialect][]string{ + databasepb.DatabaseDialect_GOOGLE_STANDARD_SQL: {}, + databasepb.DatabaseDialect_POSTGRESQL: {}, + }, + }, + "not an e-string": { + input: `SELECT * from my_table WHERE'ab\'c?' = col1`, + wantSQL: map[databasepb.DatabaseDialect]string{ + databasepb.DatabaseDialect_GOOGLE_STANDARD_SQL: `SELECT * from my_table WHERE'ab\'c?' = col1`, + databasepb.DatabaseDialect_POSTGRESQL: ``, + }, + want: map[databasepb.DatabaseDialect][]string{ + databasepb.DatabaseDialect_GOOGLE_STANDARD_SQL: {}, + databasepb.DatabaseDialect_POSTGRESQL: nil, + }, + wantErr: map[databasepb.DatabaseDialect]error{ + databasepb.DatabaseDialect_GOOGLE_STANDARD_SQL: nil, + databasepb.DatabaseDialect_POSTGRESQL: spanner.ToSpannerError(status.Error(codes.InvalidArgument, "SQL statement contains an unclosed literal: SELECT * from my_table WHERE'ab\\'c?' = col1")), + }, + }, } for _, dialect := range []databasepb.DatabaseDialect{databasepb.DatabaseDialect_GOOGLE_STANDARD_SQL, databasepb.DatabaseDialect_POSTGRESQL} { parser, err := NewStatementParser(dialect, 1000) @@ -1140,6 +1166,11 @@ SELECT * FROM PersonsTable WHERE id=$1`, wantSQL: `select foo from bar where id=$tag$this is a string$tag$ and value=$1 order by value`, want: []string{"p1"}, }, + "dollar-quoted string with tag with unicode chars": { + input: `SELECT $ÿabc0$literal string ? ?$ÿabc0 $ÿabc0$`, + wantSQL: `SELECT $ÿabc0$literal string ? ?$ÿabc0 $ÿabc0$`, + want: []string{}, + }, "invalid dollar-quoted string": { input: "select foo from bar where id=$tag$this is an invalid string and value=? order by value", wantErr: spanner.ToSpannerError( @@ -1229,6 +1260,16 @@ func TestFindParamsWithCommentsPostgreSQL(t *testing.T) { wantSQL: `$1$tag$?it$?s$tag$%s$2`, want: []string{"p1", "p2"}, }, + "dollar-quoted string with tag similar tag inside": { + input: `$tag$ $tag $tag$`, + wantSQL: `$tag$ $tag $tag$`, + want: []string{}, + }, + "dollar-quoted string with nested dollar-quoted string": { + input: ` ? $tag$ $tag2$ test ? $tag2$ $tag$ ? `, + wantSQL: ` $1 $tag$ $tag2$ test ? $tag2$ $tag$ $2 `, + want: []string{"p1", "p2"}, + }, "dollar-quoted string with linefeed": { input: `?%s$$?it\'?s ?it\'?s$$?`, @@ -2268,6 +2309,26 @@ func TestEatDollarQuotedString(t *testing.T) { want: "", wantErr: true, }, + { + input: "$outer$ outer string $outer $outer$", + want: " outer string $outer ", + wantErr: false, + }, + { + input: "$tag$value $tag#$tag$", + want: "value $tag#", + wantErr: false, + }, + { + input: "$tag$value $tag--not a comment$tag$", + want: "value $tag--not a comment", + wantErr: false, + }, + { + input: "$tag$value $tag/*not a comment*/$tag$", + want: "value $tag/*not a comment*/", + wantErr: false, + }, } statementParser, err := NewStatementParser(databasepb.DatabaseDialect_POSTGRESQL, 1000) if err != nil { diff --git a/spannerlib/api/connection.go b/spannerlib/api/connection.go index 8020e6dc..2f0b611b 100644 --- a/spannerlib/api/connection.go +++ b/spannerlib/api/connection.go @@ -445,6 +445,9 @@ func extractParams(directExecuteContext context.Context, statement *spannerpb.Ex ReturnResultSetStats: true, DirectExecuteQuery: true, DirectExecuteContext: directExecuteContext, + QueryOptions: spanner.QueryOptions{ + Mode: &statement.QueryMode, + }, }) if statement.Params != nil { if statement.ParamTypes == nil { diff --git a/spannerlib/wrappers/spannerlib-dotnet/publish.sh b/spannerlib/wrappers/spannerlib-dotnet/publish.sh index 4c703e2f..44b73e23 100755 --- a/spannerlib/wrappers/spannerlib-dotnet/publish.sh +++ b/spannerlib/wrappers/spannerlib-dotnet/publish.sh @@ -20,6 +20,9 @@ fi # Build all components ./build.sh +# Remove existing packages. +find ./**/bin/Release -type f -name "Alpha*.nupkg" -exec rm {} \; + # Pack and publish to nuget dotnet pack find ./**/bin/Release -type f -name "Alpha*.nupkg" -exec \ diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/MockSpannerServer.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/MockSpannerServer.cs index 8b5a9010..f9192359 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/MockSpannerServer.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/MockSpannerServer.cs @@ -59,6 +59,11 @@ public static StatementResult CreateException(Exception exception) return new StatementResult(exception); } + public static StatementResult CreateSelectZeroResultSet() + { + return CreateSingleColumnResultSet(new Spanner.V1.Type { Code = Spanner.V1.TypeCode.Int64 }, "COL1", 0); + } + public static StatementResult CreateSelect1ResultSet() { return CreateSingleColumnResultSet(new Spanner.V1.Type { Code = Spanner.V1.TypeCode.Int64 }, "COL1", 1); @@ -413,7 +418,7 @@ public bool WaitForRequestsToContain(Func predicate) public bool WaitForRequestsToContain(Func predicate, TimeSpan timeout) { - var stopwatch = new Stopwatch(); + var stopwatch = Stopwatch.StartNew(); while (stopwatch.Elapsed < timeout) { if (Requests.Any(predicate)) diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/SpannerConverter.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/SpannerConverter.cs index 9b64ffab..9f1a31c7 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/SpannerConverter.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/SpannerConverter.cs @@ -78,11 +78,24 @@ internal static Value ToProtobufValue(Spanner.V1.Type type, object? value) StringValue = XmlConvert.ToString(Convert.ToDateTime(value, InvariantCulture), XmlDateTimeSerializationMode.Utc) }; case TypeCode.Date: + if (value is DateOnly dateOnly) + { + return new Value + { + StringValue = dateOnly.ToString("yyyy-MM-dd"), + }; + } return new Value { StringValue = StripTimePart( XmlConvert.ToString(Convert.ToDateTime(value, InvariantCulture), XmlDateTimeSerializationMode.Utc)) }; + case TypeCode.Interval: + if (value is TimeSpan timeSpan) + { + return Value.ForString(XmlConvert.ToString(timeSpan)); + } + return Value.ForString(value.ToString()); case TypeCode.Json: if (value is string stringValue) { From cd2a8f5e0fc593c21b33784dfada41f2ce496858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Wed, 12 Nov 2025 17:53:15 +0100 Subject: [PATCH 10/29] test: run Entity Framework tests --- driver.go | 8 +++ .../spanner-ado-net/SpannerConnection.cs | 7 +-- .../spanner-ado-net/SpannerDataReader.cs | 57 +++++++++++++------ .../spanner-ado-net/SpannerDbException.cs | 26 +++++++++ .../spanner-ado-net/SpannerTransaction.cs | 2 +- 5 files changed, 78 insertions(+), 22 deletions(-) diff --git a/driver.go b/driver.go index 28310912..efa76b7e 100644 --- a/driver.go +++ b/driver.go @@ -1527,12 +1527,20 @@ func parseIsolationLevel(val string) (sql.IsolationLevel, error) { return sql.LevelDefault, nil case "read_uncommitted": return sql.LevelReadUncommitted, nil + case "readuncommitted": + return sql.LevelReadUncommitted, nil case "read_committed": return sql.LevelReadCommitted, nil + case "readcommitted": + return sql.LevelReadCommitted, nil case "write_committed": return sql.LevelWriteCommitted, nil + case "writecommitted": + return sql.LevelWriteCommitted, nil case "repeatable_read": return sql.LevelRepeatableRead, nil + case "repeatableread": + return sql.LevelRepeatableRead, nil case "snapshot": return sql.LevelSnapshot, nil case "serializable": diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs index 7ae73c2f..db653621 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs @@ -17,16 +17,15 @@ using System.Data; using System.Data.Common; using System.Diagnostics.CodeAnalysis; -using System.Reflection; using System.Threading; using System.Threading.Tasks; -using System.Transactions; using Google.Api.Gax; using Google.Cloud.Spanner.Common.V1; using Google.Cloud.Spanner.V1; using Google.Cloud.SpannerLib; using IsolationLevel = System.Data.IsolationLevel; using TransactionOptions = Google.Cloud.Spanner.V1.TransactionOptions; +using static Google.Cloud.Spanner.DataProvider.SpannerDbException; namespace Google.Cloud.Spanner.DataProvider; @@ -336,7 +335,7 @@ private void AssertClosed() public CommitResponse? WriteMutations(BatchWriteRequest.Types.MutationGroup mutations) { EnsureOpen(); - return LibConnection!.WriteMutations(mutations); + return TranslateException(() => LibConnection!.WriteMutations(mutations)); } public Task WriteMutationsAsync(BatchWriteRequest.Types.MutationGroup mutations, CancellationToken cancellationToken = default) @@ -376,7 +375,7 @@ public long[] ExecuteBatchDml(List commands) statements.Add(batchStatement); } } - return LibConnection!.ExecuteBatch(statements); + return TranslateException(() => LibConnection!.ExecuteBatch(statements)); } public DbCommand CreateInsertCommand(string table) diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs index 902a9f5a..62d7988a 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs @@ -40,34 +40,57 @@ public class SpannerDataReader : DbDataReader private bool _closed; private bool _hasReadData; private bool _hasData; + + private ResultSetMetadata? _metadata; + + private ResultSetMetadata? Metadata + { + get + { + if (_metadata == null) + { + CheckNotClosed(); + _metadata = LibRows.Metadata; + } + return _metadata; + } + } public override int FieldCount { get { CheckNotClosed(); - return LibRows.Metadata?.RowType.Fields.Count ?? 0; + return Metadata?.RowType.Fields.Count ?? 0; } } public override object this[int ordinal] => GetFieldValue(ordinal); public override object this[string name] => this[GetOrdinal(name)]; - public override int RecordsAffected + private long? _stats; + + private long? Stats { get { - CheckNotClosed(); - return (int)LibRows.UpdateCount; + if (_stats == null) + { + CheckNotClosed(); + _stats = LibRows.UpdateCount; + } + return _stats; } } + public override int RecordsAffected => (int) (Stats ?? 0); + public override bool HasRows { get { CheckNotClosed(); - if (LibRows.Metadata?.RowType.Fields.Count == 0) + if (Metadata?.RowType.Fields.Count == 0) { return false; } @@ -167,7 +190,7 @@ private bool CheckForRows() public override DataTable? GetSchemaTable() { CheckNotClosed(); - var metadata = LibRows.Metadata; + var metadata = Metadata; if (metadata?.RowType == null || metadata.RowType.Fields.Count == 0) { return null; @@ -293,7 +316,7 @@ public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int CheckValidPosition(); CheckValidOrdinal(ordinal); CheckNotNull(ordinal); - var code = LibRows.Metadata!.RowType.Fields[ordinal].Type.Code; + var code = Metadata!.RowType.Fields[ordinal].Type.Code; GaxPreconditions.CheckState(Array.Exists([TypeCode.Bytes, TypeCode.Json, TypeCode.String], c => c == code), "Spanner only supports conversion to byte arrays for columns of type BYTES or STRING."); Preconditions.CheckIndexRange(bufferOffset, nameof(bufferOffset), 0, buffer?.Length ?? 0); @@ -400,7 +423,7 @@ public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int public override string GetDataTypeName(int ordinal) { CheckValidOrdinal(ordinal); - return GetTypeName(LibRows.Metadata!.RowType.Fields[ordinal].Type); + return GetTypeName(Metadata!.RowType.Fields[ordinal].Type); } private static string GetTypeName(Google.Cloud.Spanner.V1.Type type) @@ -481,7 +504,7 @@ public override double GetDouble(int ordinal) public override System.Type GetFieldType(int ordinal) { CheckValidOrdinal(ordinal); - return GetClrType(LibRows.Metadata!.RowType.Fields[ordinal].Type); + return GetClrType(Metadata!.RowType.Fields[ordinal].Type); } private static System.Type GetClrType(Google.Cloud.Spanner.V1.Type type) @@ -635,24 +658,24 @@ public override long GetInt64(int ordinal) public override string GetName(int ordinal) { CheckValidOrdinal(ordinal); - return LibRows.Metadata!.RowType.Fields[ordinal].Name; + return Metadata!.RowType.Fields[ordinal].Name; } public override int GetOrdinal(string name) { CheckNotClosed(); // First try with case sensitivity. - for (var i = 0; i < LibRows.Metadata?.RowType.Fields.Count; i++) + for (var i = 0; i < Metadata?.RowType.Fields.Count; i++) { - if (Equals(LibRows.Metadata?.RowType.Fields[i].Name, name)) + if (Equals(Metadata?.RowType.Fields[i].Name, name)) { return i; } } // Nothing found, try with case-insensitive comparison. - for (var i = 0; i < LibRows.Metadata?.RowType.Fields.Count; i++) + for (var i = 0; i < Metadata?.RowType.Fields.Count; i++) { - if (string.Equals(LibRows.Metadata?.RowType.Fields[i].Name, name, StringComparison.InvariantCultureIgnoreCase)) + if (string.Equals(Metadata?.RowType.Fields[i].Name, name, StringComparison.InvariantCultureIgnoreCase)) { return i; } @@ -739,7 +762,7 @@ public override object GetValue(int ordinal) { CheckValidOrdinal(ordinal); CheckValidPosition(); - var type = LibRows.Metadata!.RowType.Fields[ordinal].Type; + var type = Metadata!.RowType.Fields[ordinal].Type; var value = _currentRow!.Values[ordinal]; return GetUnderlyingValue(type, value); } @@ -815,7 +838,7 @@ private Value GetProtoValue(int ordinal) private V1.Type GetSpannerType(int ordinal) { CheckValidOrdinal(ordinal); - return LibRows.Metadata?.RowType.Fields[ordinal].Type ?? throw new DataException("metadata not found"); + return Metadata?.RowType.Fields[ordinal].Type ?? throw new DataException("metadata not found"); } public override int GetValues(object[] values) @@ -862,7 +885,7 @@ private void CheckValidPosition() private void CheckValidOrdinal(int ordinal) { CheckNotClosed(); - var metadata = LibRows.Metadata; + var metadata = Metadata; GaxPreconditions.CheckState(metadata != null && metadata.RowType.Fields.Count > 0, "This reader does not contain any rows"); // Check that the ordinal is within the range of the columns in the query. diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerDbException.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerDbException.cs index 619015b8..a6d0993a 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerDbException.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerDbException.cs @@ -14,6 +14,7 @@ using System; using System.Data.Common; +using System.Threading.Tasks; using Google.Cloud.SpannerLib; using Google.Rpc; @@ -32,6 +33,31 @@ internal static T TranslateException(Func func) throw TranslateException(exception); } } + + internal static Task TranslateException(Task task) + { + return task.ContinueWith( t => + { + if (t.IsFaulted && t.Exception.InnerException is SpannerException spannerException) + { + throw TranslateException(spannerException); + } + }, + TaskContinuationOptions.ExecuteSynchronously); + } + + internal static Task TranslateException(Task task) + { + return task.ContinueWith( t => + { + if (t.IsFaulted && t.Exception.InnerException is SpannerException spannerException) + { + throw TranslateException(spannerException); + } + return t.Result; + }, + TaskContinuationOptions.ExecuteSynchronously); + } internal static Exception TranslateException(SpannerException exception) { diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs index b3b2e1f5..2264ed79 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs @@ -153,7 +153,7 @@ private Task EndTransactionAsync(Func endTransactionMethod) GaxPreconditions.CheckState(!IsCompleted, "This transaction is no longer active"); try { - return endTransactionMethod(); + return SpannerDbException.TranslateException(endTransactionMethod()); } finally { From 182b2f539e45e451b3737a253c84b0b54f2bf631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Fri, 14 Nov 2025 16:28:54 +0100 Subject: [PATCH 11/29] test: run EntityFramework tests --- conn.go | 6 +++++ .../GetValueConversionTests.cs | 25 +++++++++++++++++++ .../AbstractMockServerTests.cs | 1 + .../spanner-ado-net/SpannerDataReader.cs | 3 +-- parser/statement_parser.go | 13 +--------- parser/statements.go | 8 +++--- 6 files changed, 38 insertions(+), 18 deletions(-) diff --git a/conn.go b/conn.go index 5889448f..8194bd6b 100644 --- a/conn.go +++ b/conn.go @@ -682,6 +682,12 @@ func (c *conn) execDDL(ctx context.Context, statements ...spanner.Statement) (dr return nil, err } return driver.ResultNoRows, nil + } else if c.parser.IsDropDatabaseStatement(ddlStatements[0]) { + stmt := &parser.ParsedDropDatabaseStatement{} + if err := stmt.Parse(c.parser, ddlStatements[0]); err != nil { + return nil, err + } + return (&executableDropDatabaseStatement{stmt}).execContext(ctx, c, nil) } op, err := c.adminClient.UpdateDatabaseDdl(ctx, &adminpb.UpdateDatabaseDdlRequest{ diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/GetValueConversionTests.cs b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/GetValueConversionTests.cs index 8e1488a5..70c195a5 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/GetValueConversionTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/GetValueConversionTests.cs @@ -172,4 +172,29 @@ public class GetValueConversionTests(DbFactoryFixture fixture) : GetValueConvers public override void GetByte_throws_for_zero_String() => TestGetValue(DbType.String, ValueKind.Zero, x => x.GetByte(0), (byte)0); + // Spanner allows calling GetFloat for float64 values. + public override void GetFloat_throws_for_maximum_Double() => TestGetValue(DbType.Double, ValueKind.Maximum, x => x.GetFloat(0), float.PositiveInfinity); + + public override void GetFloat_throws_for_maximum_Double_with_GetFieldValue() => TestGetValue(DbType.Double, ValueKind.Maximum, x => x.GetFieldValue(0), float.PositiveInfinity); + + public override async Task GetFloat_throws_for_maximum_Double_with_GetFieldValueAsync() => await TestGetValueAsync(DbType.Double, ValueKind.Maximum, async x => await x.GetFieldValueAsync(0), float.PositiveInfinity); + + public override void GetFloat_throws_for_minimum_Double() => TestGetValue(DbType.Double, ValueKind.Minimum, x => x.GetFloat(0), 0.0f); + + public override void GetFloat_throws_for_minimum_Double_with_GetFieldValue() => TestGetValue(DbType.Double, ValueKind.Minimum, x => x.GetFieldValue(0), 0.0f); + + public override async Task GetFloat_throws_for_minimum_Double_with_GetFieldValueAsync() => await TestGetValueAsync(DbType.Double, ValueKind.Minimum, async x => await x.GetFieldValueAsync(0), 0.0f); + + public override void GetFloat_throws_for_one_Double() => TestGetValue(DbType.Double, ValueKind.One, x => x.GetFloat(0), 1.0f); + + public override void GetFloat_throws_for_one_Double_with_GetFieldValue() => TestGetValue(DbType.Double, ValueKind.One, x => x.GetFieldValue(0), 1.0f); + + public override async Task GetFloat_throws_for_one_Double_with_GetFieldValueAsync() => await TestGetValueAsync(DbType.Double, ValueKind.One, async x => await x.GetFieldValueAsync(0), 1.0f); + + public override void GetFloat_throws_for_zero_Double() => TestGetValue(DbType.Double, ValueKind.Zero, x => x.GetFloat(0), 0.0f); + + public override void GetFloat_throws_for_zero_Double_with_GetFieldValue() => TestGetValue(DbType.Double, ValueKind.Zero, x => x.GetFieldValue(0), 0.0f); + + public override async Task GetFloat_throws_for_zero_Double_with_GetFieldValueAsync() => await TestGetValueAsync(DbType.Double, ValueKind.Zero, async x => await x.GetFieldValueAsync(0), 0.0f); + } diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs index e5913b4a..35e5cab2 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/AbstractMockServerTests.cs @@ -58,6 +58,7 @@ public void SetupResults() public void Reset() { Fixture.SpannerMock.Reset(); + Fixture.DatabaseAdminMock.Reset(); } protected SpannerConnection OpenConnection() diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs index 896cc066..bd69d370 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs @@ -568,8 +568,7 @@ public override float GetFloat(int ordinal) throw new InvalidCastException(exception.Message, exception); } } - var type = GetSpannerType(ordinal); - if (type.Code == TypeCode.Float32) + if (value.HasNumberValue) { return (float)value.NumberValue; } diff --git a/parser/statement_parser.go b/parser/statement_parser.go index 8f550cf4..46f24633 100644 --- a/parser/statement_parser.go +++ b/parser/statement_parser.go @@ -44,11 +44,8 @@ var clientSideKeywords = map[string]bool{ "BEGIN": true, "COMMIT": true, "ROLLBACK": true, - "CREATE": true, // CREATE DATABASE is handled as a client-side statement - "DROP": true, // DROP DATABASE is handled as a client-side statement } -var createStatements = map[string]bool{"CREATE": true} -var dropStatements = map[string]bool{"DROP": true} + var showStatements = map[string]bool{"SHOW": true} var setStatements = map[string]bool{"SET": true} var resetStatements = map[string]bool{"RESET": true} @@ -648,14 +645,6 @@ func (p *StatementParser) isQuery(query string) bool { return info.StatementType == StatementTypeQuery } -func isCreateKeyword(keyword string) bool { - return isStatementKeyword(keyword, createStatements) -} - -func isDropKeyword(keyword string) bool { - return isStatementKeyword(keyword, dropStatements) -} - func isQueryKeyword(keyword string) bool { return isStatementKeyword(keyword, selectStatements) } diff --git a/parser/statements.go b/parser/statements.go index d45b01e3..d253bc37 100644 --- a/parser/statements.go +++ b/parser/statements.go @@ -36,10 +36,6 @@ func parseStatement(parser *StatementParser, keyword, query string) (ParsedState stmt = &ParsedSetStatement{} } else if isResetStatementKeyword(keyword) { stmt = &ParsedResetStatement{} - } else if isCreateKeyword(keyword) && isCreateDatabase(parser, query) { - stmt = &ParsedCreateDatabaseStatement{} - } else if isDropKeyword(keyword) && isDropDatabase(parser, query) { - stmt = &ParsedDropDatabaseStatement{} } else if isStartStatementKeyword(keyword) { if parser.Dialect == databasepb.DatabaseDialect_POSTGRESQL && isStartTransaction(parser, query) { stmt = &ParsedBeginStatement{} @@ -397,6 +393,10 @@ func (s *ParsedDropDatabaseStatement) Query() string { return s.query } +func (s *ParsedDropDatabaseStatement) Parse(parser *StatementParser, query string) error { + return s.parse(parser, query) +} + func (s *ParsedDropDatabaseStatement) parse(parser *StatementParser, query string) error { // Parse a statement of the form // DROP DATABASE From 6ebcc374b914db01a5d07e96fd6c9295ae8923e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Mon, 24 Nov 2025 16:38:10 +0100 Subject: [PATCH 12/29] docs: add samples --- .../spanner-ado-net-samples/EmulatorRunner.cs | 124 +++++++++++ .../spanner-ado-net-samples/Program.cs | 37 ---- .../spanner-ado-net-samples/SampleRunner.cs | 206 ++++++++++++++++++ .../Snippets/HelloWorldSample.cs | 17 ++ .../spanner-ado-net-samples.csproj | 4 + .../SpannerConnectionStringBuilder.cs | 26 +++ 6 files changed, 377 insertions(+), 37 deletions(-) create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/EmulatorRunner.cs delete mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/Program.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/SampleRunner.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/HelloWorldSample.cs diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/EmulatorRunner.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/EmulatorRunner.cs new file mode 100644 index 00000000..88280d7b --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/EmulatorRunner.cs @@ -0,0 +1,124 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Net.Sockets; + +namespace Google.Cloud.Spanner.DataProvider.Samples; + +using Docker.DotNet; +using Docker.DotNet.Models; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +/// +/// This class can be used to programmatically start and stop an instance of the Cloud Spanner emulator. +/// +internal class EmulatorRunner +{ + private static readonly string SEmulatorImageName = "gcr.io/cloud-spanner-emulator/emulator"; + private readonly DockerClient _dockerClient; + private string? _containerId; + + internal EmulatorRunner() + { + _dockerClient = new DockerClientConfiguration(new Uri(GetDockerApiUri())).CreateClient(); + } + + internal bool IsEmulatorRunning() + { + using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + try + { + socket.Connect("localhost", 9010); + } + catch (SocketException ex) + { + if (ex.SocketErrorCode == SocketError.ConnectionRefused) + { + return false; + } + throw; + } + return true; + } + + /// + /// Downloads the latest Spanner emulator docker image and starts the emulator on port 9010. + /// + internal async Task StartEmulator() + { + await PullEmulatorImage(); + var response = await _dockerClient.Containers.CreateContainerAsync(new CreateContainerParameters + { + Image = SEmulatorImageName, + ExposedPorts = new Dictionary + { + { + "9010", default + } + }, + HostConfig = new HostConfig + { + PortBindings = new Dictionary?> + { + {"9010", null} + }, + } + }); + _containerId = response.ID; + await _dockerClient.Containers.StartContainerAsync(_containerId, null); + var inspectResponse = await _dockerClient.Containers.InspectContainerAsync(_containerId); + Thread.Sleep(500); + return inspectResponse.NetworkSettings.Ports["9010/tcp"][0]; + } + + /// + /// Stops the currently running emulator. Fails if no emulator has been started. + /// + internal async Task StopEmulator() + { + if (_containerId != null) + { + await _dockerClient.Containers.KillContainerAsync(_containerId, new ContainerKillParameters()); + await _dockerClient.Containers.RemoveContainerAsync(_containerId, new ContainerRemoveParameters()); + } + } + + private async Task PullEmulatorImage() + { + await _dockerClient.Images.CreateImageAsync(new ImagesCreateParameters + { + FromImage = SEmulatorImageName, + Tag = "latest" + }, + new AuthConfig(), + new Progress()); + } + + private string GetDockerApiUri() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return "npipe://./pipe/docker_engine"; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return "unix:/var/run/docker.sock"; + } + throw new Exception("Was unable to determine what OS this is running on, does not appear to be Windows or Linux!?"); + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Program.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Program.cs deleted file mode 100644 index c5287edf..00000000 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/Program.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2025 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 -// -// https://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. - -using Google.Cloud.Spanner.DataProvider; - -using var connection = new SpannerConnection {ConnectionString = "/projects/appdev-soda-spanner-staging/instances/knut-test-ycsb/databases/knut-test-db"}; -connection.Open(); -using var cmd = connection.CreateCommand(); -cmd.CommandText = "select * from all_types where col_varchar is not null limit 10"; - -using var reader = cmd.ExecuteReader(); -for (int i = 0; i < reader.FieldCount; i++) -{ - Console.Write(reader.GetName(i)); - Console.Write("|"); -} -Console.WriteLine(); -while (reader.Read()) -{ - for (int i = 0; i < reader.FieldCount; i++) - { - Console.Write(reader.GetValue(i)); - Console.Write("|"); - } - Console.WriteLine(); -} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/SampleRunner.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/SampleRunner.cs new file mode 100644 index 00000000..cf32eec5 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/SampleRunner.cs @@ -0,0 +1,206 @@ +using System.Reflection; +using Google.Api.Gax; +using Google.Cloud.Spanner.Common.V1; +using Google.Cloud.Spanner.DataProvider.Samples.Snippets; + +namespace Google.Cloud.Spanner.DataProvider.Samples; + +/// +/// Main class for running a sample from the Snippets directory. +/// Usage: `dotnet run SampleName` +/// Example: `dotnet run HelloWorld` +/// +/// The SampleRunner will automatically start a docker container with a Spanner emulator and execute +/// the sample on that emulator instance. No further setup or configuration is required. +/// +public static class SampleRunner +{ + private static readonly string SnippetsNamespace = typeof(HelloWorldSample).Namespace!; + + static void Main(string[] args) + { + if (args.Length < 1) + { + Console.Error.WriteLine("Not enough arguments.\r\nUsage: dotnet run \r\nExample: dotnet run HelloWorld"); + PrintValidSampleNames(); + return; + } + var sampleName = args[0]; + if (sampleName.Equals("All")) + { + // Run all samples. This is used to test that all samples are runnable. + RunAllSamples(); + } + else + { + RunSample(sampleName, false); + } + } + + + private static void RunAllSamples() + { + var sampleClasses = GetSampleClasses(); + foreach (var sample in sampleClasses) + { + RunSample(sample.Name, true); + } + } + + private static void RunSample(string sampleName, bool failOnException) + { + if (sampleName.EndsWith("Sample")) + { + sampleName = sampleName.Substring(0, sampleName.Length - "Sample".Length); + } + try + { + var sampleMethod = GetSampleMethod(sampleName); + if (sampleMethod != null) + { + Console.WriteLine($"Running sample {sampleName}"); + RunSampleAsync((connectionString) => (Task)sampleMethod.Invoke(null, [connectionString])!).WaitWithUnwrappedExceptions(); + } + } + catch (Exception e) + { + Console.WriteLine($"Running sample failed: {e.Message}\n{e.StackTrace}"); + if (failOnException) + { + throw; + } + } + } + + private static async Task RunSampleAsync(Func sampleMethod) + { + var emulatorRunner = new EmulatorRunner(); + var startedEmulator = false; + try + { + Console.WriteLine(""); + if (emulatorRunner.IsEmulatorRunning()) + { + Console.WriteLine("Emulator is already running. Re-using existing Emulator instance..."); + Console.WriteLine(""); + } + else + { + Console.WriteLine("Starting emulator..."); + var portBinding = await emulatorRunner.StartEmulator(); + Console.WriteLine($"Emulator started on port {portBinding.HostPort}"); + Console.WriteLine(""); + startedEmulator = true; + } + + var projectId = "sample-project"; + var instanceId = "sample-instance"; + var databaseId = "sample-database"; + DatabaseName databaseName = DatabaseName.FromProjectInstanceDatabase(projectId, instanceId, databaseId); + var connectionStringBuilder = new SpannerConnectionStringBuilder() + { + DataSource = databaseName.ToString(), + AutoConfigEmulator = true, + }; + + await sampleMethod.Invoke(connectionStringBuilder.ConnectionString); + } + catch (Exception e) + { + Console.WriteLine($"Running sample failed: {e.Message}"); + throw; + } + finally + { + if (startedEmulator) + { + Console.WriteLine(""); + Console.WriteLine("Stopping emulator..."); + emulatorRunner.StopEmulator().WaitWithUnwrappedExceptions(); + Console.WriteLine(""); + } + } + } + + private static MethodInfo? GetSampleMethod(string sampleName) + { + try + { + var sampleClass = System.Type.GetType($"{SnippetsNamespace}.{sampleName}Sample"); + if (sampleClass == null) + { + Console.Error.WriteLine($"Unknown sample name: {sampleName}"); + PrintValidSampleNames(); + return null; + } + var sampleMethod = sampleClass.GetMethod("Run"); + if (sampleMethod == null) + { + Console.Error.WriteLine($"{sampleName} is not a valid sample as it does not contain a Run method"); + PrintValidSampleNames(); + return null; + } + return sampleMethod; + } + catch (Exception e) + { + Console.Error.WriteLine($"Could not load sample {sampleName}. Please check that the sample name is a valid sample name.\r\nException: {e.Message}"); + PrintValidSampleNames(); + return null; + } + } + + private static async Task CreateSampleDataModel(string connectionString) + { + var codeBaseUrl = new Uri(Assembly.GetExecutingAssembly().Location); + var codeBasePath = Uri.UnescapeDataString(codeBaseUrl.AbsolutePath); + var dirPath = Path.GetDirectoryName(codeBasePath); + var fileName = Path.Combine(dirPath, "SampleModel/SampleDataModel.sql"); + var script = await File.ReadAllTextAsync(fileName); + var statements = script.Split(";"); + for (var i = 0; i < statements.Length; i++) + { + // Remove license header from script + if (statements[i].IndexOf("/*", StringComparison.Ordinal) >= 0 && statements[i].IndexOf("*/", StringComparison.Ordinal) >= 0) + { + int startIndex = statements[i].IndexOf("/*", StringComparison.Ordinal); + int endIndex = statements[i].IndexOf("*/", startIndex, StringComparison.Ordinal) + "*/".Length; + statements[i] = statements[i].Remove(startIndex, endIndex - startIndex); + } + statements[i] = statements[i].Trim(new char[] { '\r', '\n' }); + } + int length = statements.Length; + if (statements[length - 1] == "") + { + length--; + } + await ExecuteDdlAsync(connectionString, statements, length); + } + + private static async Task ExecuteDdlAsync(string connectionString, string[] ddl, int length) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + var batch = connection.CreateBatch(); + foreach (var statement in ddl) + { + var cmd = batch.CreateBatchCommand(); + cmd.CommandText = statement; + batch.BatchCommands.Add(cmd); + } + await batch.ExecuteNonQueryAsync(); + } + + private static void PrintValidSampleNames() + { + var sampleClasses = GetSampleClasses(); + Console.Error.WriteLine(""); + Console.Error.WriteLine("Supported samples:"); + sampleClasses.ToList().ForEach(t => Console.Error.WriteLine($" * {t.Name}")); + } + + private static IEnumerable GetSampleClasses() + => from t in Assembly.GetExecutingAssembly().GetTypes() + where t.IsClass && t.Name.EndsWith("Sample") + select t; +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/HelloWorldSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/HelloWorldSample.cs new file mode 100644 index 00000000..fb685729 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/HelloWorldSample.cs @@ -0,0 +1,17 @@ +namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; + +public class HelloWorldSample +{ + public static async Task Run(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + await using var command = connection.CreateCommand(); + command.CommandText = "SELECT 'Hello World' as Message"; + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + Console.WriteLine($"Greeting from Spanner: {reader.GetString(0)}"); + } + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/spanner-ado-net-samples.csproj b/drivers/spanner-ado-net/spanner-ado-net-samples/spanner-ado-net-samples.csproj index 3c247b26..25b89314 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/spanner-ado-net-samples.csproj +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/spanner-ado-net-samples.csproj @@ -14,4 +14,8 @@ + + + + diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnectionStringBuilder.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnectionStringBuilder.cs index b00bca5c..489253bb 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnectionStringBuilder.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnectionStringBuilder.cs @@ -103,6 +103,24 @@ public uint Port set => SpannerConnectionStringOption.Port.SetValue(this, value); } + /// + /// Whether to automatically try to connect to the Spanner Emulator, and automatically create the Spanner + /// instance and database on the Emulator if these do not already exist. Setting this option to true will + /// instruct the driver to: + /// 1. Try to connect to localhost:9010 (unless a different Host/Port has been set in the connection string) + /// 2. Use plain text communication instead of SSL. + /// 3. Create the Spanner Instance and Database on the Emulator if these do not already exist. + /// + [Category("Connection")] + [DefaultValue(false)] + [Description("Whether to automatically try to connect to the Spanner Emulator and create the Instance and Database on the Emulator.")] + [DisplayName("AutoConfigEmulator")] + public bool AutoConfigEmulator + { + get => SpannerConnectionStringOption.AutoConfigEmulator.GetValue(this); + set => SpannerConnectionStringOption.AutoConfigEmulator.SetValue(this, value); + } + /// /// Whether to use plain text communication with the server. The default is SSL. /// @@ -352,6 +370,9 @@ internal abstract class SpannerConnectionStringOption public static readonly SpannerConnectionStringValueOption CommandTimeout; public static readonly SpannerConnectionStringValueOption TransactionTimeout; + // Emulator Options + public static readonly SpannerConnectionStringValueOption AutoConfigEmulator; + // SSL/TLS Options public static readonly SpannerConnectionStringValueOption UsePlainText; @@ -445,6 +466,11 @@ static SpannerConnectionStringOption() spannerLibKey: "transaction_timeout", defaultValue: 0u)); + // Emulator Options + AddOption(options, AutoConfigEmulator = new( + keys: ["AutoConfigEmulator", "Auto Config Emulator", "UseEmulator", "Use Emulator", "auto_config_emulator"], + defaultValue: false)); + // SSL/TLS Options AddOption(options, UsePlainText = new( keys: ["UsePlainText", "Use plain text", "Plain text", "use_plain_text"], From bec7c357782a0f1425902e765bc0702fa9d363ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Mon, 24 Nov 2025 18:39:43 +0100 Subject: [PATCH 13/29] chore: add more samples --- .../spanner-ado-net-samples/SampleRunner.cs | 48 ++++---- .../Snippets/HelloWorldSample.cs | 14 +++ .../Snippets/QueryParametersSample.cs | 37 ++++++ .../create_sample_tables.sql | 113 ++++++++++++++++++ .../insert_sample_data.sql | 19 +++ .../spanner-ado-net-samples.csproj | 9 ++ .../MockSpannerServer.cs | 6 - 7 files changed, 218 insertions(+), 28 deletions(-) create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/QueryParametersSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/create_sample_tables.sql create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/insert_sample_data.sql diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/SampleRunner.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/SampleRunner.cs index cf32eec5..a504ac7c 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/SampleRunner.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/SampleRunner.cs @@ -1,3 +1,17 @@ +// Copyright 2025 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 +// +// https://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. + using System.Reflection; using Google.Api.Gax; using Google.Cloud.Spanner.Common.V1; @@ -102,7 +116,9 @@ private static async Task RunSampleAsync(Func sampleMethod) DataSource = databaseName.ToString(), AutoConfigEmulator = true, }; - + + await ExecuteScript(connectionStringBuilder.ConnectionString, "create_sample_tables.sql"); + await ExecuteScript(connectionStringBuilder.ConnectionString, "insert_sample_data.sql"); await sampleMethod.Invoke(connectionStringBuilder.ConnectionString); } catch (Exception e) @@ -150,39 +166,27 @@ private static async Task RunSampleAsync(Func sampleMethod) } } - private static async Task CreateSampleDataModel(string connectionString) + private static async Task ExecuteScript(string connectionString, string file) { var codeBaseUrl = new Uri(Assembly.GetExecutingAssembly().Location); var codeBasePath = Uri.UnescapeDataString(codeBaseUrl.AbsolutePath); var dirPath = Path.GetDirectoryName(codeBasePath); - var fileName = Path.Combine(dirPath, "SampleModel/SampleDataModel.sql"); - var script = await File.ReadAllTextAsync(fileName); - var statements = script.Split(";"); - for (var i = 0; i < statements.Length; i++) - { - // Remove license header from script - if (statements[i].IndexOf("/*", StringComparison.Ordinal) >= 0 && statements[i].IndexOf("*/", StringComparison.Ordinal) >= 0) - { - int startIndex = statements[i].IndexOf("/*", StringComparison.Ordinal); - int endIndex = statements[i].IndexOf("*/", startIndex, StringComparison.Ordinal) + "*/".Length; - statements[i] = statements[i].Remove(startIndex, endIndex - startIndex); - } - statements[i] = statements[i].Trim(new char[] { '\r', '\n' }); - } - int length = statements.Length; - if (statements[length - 1] == "") + if (dirPath == null) { - length--; + throw new DirectoryNotFoundException("Could not find the sample directory"); } - await ExecuteDdlAsync(connectionString, statements, length); + var filePath = Path.Combine(dirPath, file); + var script = await File.ReadAllTextAsync(filePath); + var statements = script.Split(";").Where(statement => !string.IsNullOrWhiteSpace(statement)); + await ExecuteBatchAsync(connectionString, statements); } - private static async Task ExecuteDdlAsync(string connectionString, string[] ddl, int length) + private static async Task ExecuteBatchAsync(string connectionString, IEnumerable statements) { await using var connection = new SpannerConnection(connectionString); await connection.OpenAsync(); var batch = connection.CreateBatch(); - foreach (var statement in ddl) + foreach (var statement in statements) { var cmd = batch.CreateBatchCommand(); cmd.CommandText = statement; diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/HelloWorldSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/HelloWorldSample.cs index fb685729..49d747b6 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/HelloWorldSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/HelloWorldSample.cs @@ -1,3 +1,17 @@ +// Copyright 2025 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 +// +// https://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. + namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; public class HelloWorldSample diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/QueryParametersSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/QueryParametersSample.cs new file mode 100644 index 00000000..f633c81a --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/QueryParametersSample.cs @@ -0,0 +1,37 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; + +public class QueryParametersSample +{ + public static async Task Run(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + await using var command = connection.CreateCommand(); + command.CommandText = "SELECT SingerId, FullName " + + "FROM Singers " + + "WHERE LastName LIKE @lastName " + + " OR FirstName LIKE @firstName " + + "ORDER BY LastName, FirstName"; + command.Parameters.AddWithValue("lastName", "R%"); + command.Parameters.AddWithValue("firstName", "A%"); + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + Console.WriteLine($"Found singer: {reader.GetString(1)}"); + } + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/create_sample_tables.sql b/drivers/spanner-ado-net/spanner-ado-net-samples/create_sample_tables.sql new file mode 100644 index 00000000..4eb89576 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/create_sample_tables.sql @@ -0,0 +1,113 @@ +/* Copyright 2025 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 +* +* https://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. +*/ + +CREATE TABLE IF NOT EXISTS Singers ( + SingerId INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), + FirstName STRING(200), + LastName STRING(200) NOT NULL, + BirthDate DATE, + Picture BYTES(MAX), + Version INT64 NOT NULL, + FullName STRING(400) NOT NULL AS (COALESCE(FirstName || ' ', '') || LastName) STORED, +) PRIMARY KEY (SingerId); + +CREATE INDEX IF NOT EXISTS Idx_Singers_FullName ON Singers (FullName); + +CREATE TABLE IF NOT EXISTS Albums ( + AlbumId INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), + Title STRING(100) NOT NULL, + ReleaseDate DATE, + SingerId INT64 NOT NULL, + Version INT64 NOT NULL, + CONSTRAINT FK_Albums_Singers FOREIGN KEY (SingerId) REFERENCES Singers (SingerId), +) PRIMARY KEY (AlbumId); + +CREATE TABLE IF NOT EXISTS Tracks ( + AlbumId INT64 NOT NULL, + TrackId INT64 NOT NULL, + Title STRING(200) NOT NULL, + Duration NUMERIC, + RecordedAt TIMESTAMP DEFAULT (CURRENT_TIMESTAMP), + LyricsLanguages ARRAY, + Lyrics ARRAY, + Version INT64 NOT NULL, +) PRIMARY KEY (AlbumId, TrackId), INTERLEAVE IN PARENT Albums ON DELETE CASCADE; + +CREATE UNIQUE INDEX IF NOT EXISTS Idx_Tracks_AlbumId_Title ON Tracks (AlbumId, Title); + +CREATE TABLE IF NOT EXISTS Venues ( + Code STRING(10) NOT NULL, + Name STRING(100), + Description JSON, + Active BOOL NOT NULL, + Descriptions JSON, + Version INT64 NOT NULL, +) PRIMARY KEY (Code); + +CREATE TABLE IF NOT EXISTS Concerts ( + VenueCode STRING(10) NOT NULL, + StartTime TIMESTAMP NOT NULL, + SingerId INT64 NOT NULL, + Title STRING(200), + Version INT64 NOT NULL, + CONSTRAINT FK_Concerts_Venues FOREIGN KEY (VenueCode) REFERENCES Venues (Code), + CONSTRAINT FK_Concerts_Singers FOREIGN KEY (SingerId) REFERENCES Singers (SingerId), +) PRIMARY KEY (VenueCode, StartTime, SingerId); + +CREATE TABLE IF NOT EXISTS Performances ( + VenueCode STRING(10) NOT NULL, + ConcertStartTime TIMESTAMP NOT NULL, + SingerId INT64 NOT NULL, + AlbumId INT64 NOT NULL, + TrackId INT64 NOT NULL, + StartTime TIMESTAMP, + Rating FLOAT64, + CreatedAt TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true), + LastUpdatedAt TIMESTAMP OPTIONS (allow_commit_timestamp=true), + Version INT64 NOT NULL, + CONSTRAINT FK_Performances_Concerts FOREIGN KEY (VenueCode, ConcertStartTime, SingerId) REFERENCES Concerts (VenueCode, StartTime, SingerId), + CONSTRAINT FK_Performances_Singers FOREIGN KEY (SingerId) REFERENCES Singers (SingerId), + CONSTRAINT FK_Performances_Tracks FOREIGN KEY (AlbumId, TrackId) REFERENCES Tracks (AlbumId, TrackId), +) PRIMARY KEY (VenueCode, StartTime, SingerId); + +CREATE SEQUENCE IF NOT EXISTS TicketSalesSequence OPTIONS ( + sequence_kind='bit_reversed_positive', + start_with_counter=1, + skip_range_min=1, + skip_range_max=1000000 +); + +CREATE TABLE IF NOT EXISTS TicketSales ( + Id INT64 NOT NULL DEFAULT (GET_NEXT_SEQUENCE_VALUE(SEQUENCE TicketSalesSequence)), + CustomerName STRING(MAX) NOT NULL, + Seats ARRAY NOT NULL, + VenueCode STRING(10) NOT NULL, + ConcertStartTime TIMESTAMP NOT NULL, + SingerId INT64 NOT NULL, + Version INT64 NOT NULL, + CONSTRAINT FK_TicketSales_Concerts FOREIGN KEY (VenueCode, ConcertStartTime, SingerId) REFERENCES Concerts (VenueCode, StartTime, SingerId), +) PRIMARY KEY (Id); + +CREATE TABLE IF NOT EXISTS Invoices ( + InvoiceId INT64 GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE) PRIMARY KEY, + Description STRING(MAX), +); + +CREATE TABLE IF NOT EXISTS InvoiceLines ( + InvoiceId INT64, + InvoiceLineId INT64 GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), + Product STRING(MAX), + Quantity INT64, +) PRIMARY KEY (InvoiceId, InvoiceLineId), INTERLEAVE IN PARENT Invoices diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/insert_sample_data.sql b/drivers/spanner-ado-net/spanner-ado-net-samples/insert_sample_data.sql new file mode 100644 index 00000000..0e3406be --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/insert_sample_data.sql @@ -0,0 +1,19 @@ +/* Copyright 2025 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 +* +* https://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. +*/ + +INSERT OR UPDATE INTO Singers (SingerId, FirstName, LastName, BirthDate, Picture, Version) VALUES + (1, 'Mark', 'Richards', DATE '1990-11-09', NULL, 1), + (2, 'Catalina', 'Smith', DATE '1998-04-29', NULL, 1), + (3, 'Alice', 'Trentor', DATE '1979-10-15', NULL, 1); diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/spanner-ado-net-samples.csproj b/drivers/spanner-ado-net/spanner-ado-net-samples/spanner-ado-net-samples.csproj index 25b89314..25776c34 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/spanner-ado-net-samples.csproj +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/spanner-ado-net-samples.csproj @@ -18,4 +18,13 @@ + + + Always + + + Always + + + diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/MockSpannerServer.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/MockSpannerServer.cs index 9638e5ae..6cdb9b32 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/MockSpannerServer.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/MockSpannerServer.cs @@ -15,7 +15,6 @@ using System.Collections; using System.Collections.Concurrent; using System.Diagnostics; -using System.Reflection; using Google.Apis.Util; using Google.Cloud.Spanner.Admin.Database.V1; using Google.Cloud.Spanner.Common.V1; @@ -60,11 +59,6 @@ public static StatementResult CreateException(Exception exception) return new StatementResult(exception); } - public static StatementResult CreateSelectZeroResultSet() - { - return CreateSingleColumnResultSet(new Spanner.V1.Type { Code = Spanner.V1.TypeCode.Int64 }, "COL1", 0); - } - public static StatementResult CreateSelect1ResultSet() { return CreateSingleColumnResultSet(new Spanner.V1.Type { Code = Spanner.V1.TypeCode.Int64 }, "COL1", 1); From 1decd0324a95a7097e8aa59dcbebd4378c9e66b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Wed, 26 Nov 2025 11:31:18 +0100 Subject: [PATCH 14/29] chore: add more samples --- .../Snippets/CommitTimestampSample.cs | 47 +++++++ .../Snippets/CustomConfigurationSample.cs | 50 +++++++ .../Snippets/DataTypesSample.cs | 80 +++++++++++ .../Snippets/DdlBatchSample.cs | 99 +++++++++++++ .../Snippets/DmlBatchSample.cs | 132 ++++++++++++++++++ .../Snippets/MutationsSample.cs | 44 ++++++ .../Snippets/PartitionedDmlSample.cs | 50 +++++++ .../Snippets/QueryParametersSample.cs | 37 ++++- .../Snippets/ReadOnlyTransactionSample.cs | 52 +++++++ .../create_sample_tables.sql | 7 - .../insert_sample_data.sql | 8 +- .../spanner-ado-net/SpannerBatch.cs | 1 + .../SpannerBatchCommandCollection.cs | 15 ++ .../spanner-ado-net/SpannerConnection.cs | 50 ++++++- .../SpannerParameterCollection.cs | 7 +- 15 files changed, 663 insertions(+), 16 deletions(-) create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/CommitTimestampSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/CustomConfigurationSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DataTypesSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DdlBatchSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DmlBatchSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/MutationsSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/PartitionedDmlSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/ReadOnlyTransactionSample.cs diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/CommitTimestampSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/CommitTimestampSample.cs new file mode 100644 index 00000000..a0f09399 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/CommitTimestampSample.cs @@ -0,0 +1,47 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Data; + +namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; + +// Example for getting the commit timestamp of a read/write transaction. +public class CommitTimestampSample +{ + public static async Task Run(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // Execute a read/write transaction and retrieve the commit timestamp of that transaction. + await using var transaction = await connection.BeginTransactionAsync(); + + await using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.CommandText = "INSERT INTO Singers (SingerId, FirstName, LastName) VALUES (@id, @first, @last)"; + command.Parameters.AddWithValue("id", Random.Shared.NextInt64()); + command.Parameters.AddWithValue("first", "Bruce"); + command.Parameters.AddWithValue("last", "Allison"); + Console.WriteLine($"Inserted {await command.ExecuteNonQueryAsync()} singer(s)"); + + await transaction.CommitAsync(); + + // Retrieve the last commit timestamp of this connection through the COMMIT_TIMESTAMP variable. + await using var showCommand = connection.CreateCommand(); + showCommand.CommandText = "SHOW VARIABLE COMMIT_TIMESTAMP"; + var commitTimestamp = await showCommand.ExecuteScalarAsync(); + + Console.WriteLine($"Transaction committed at {commitTimestamp}"); + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/CustomConfigurationSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/CustomConfigurationSample.cs new file mode 100644 index 00000000..304535e5 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/CustomConfigurationSample.cs @@ -0,0 +1,50 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Data; + +namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; + +// Sample that shows how to supply a custom configuration for the Spanner client +// that is used by the driver. +public class CustomConfigurationSample +{ + public static async Task Run(string connectionString) + { + // Use a SpannerConnectionBuilder to programmatically set the various options for a SpannerConnection. + var builder = new SpannerConnectionStringBuilder(connectionString) + { + // Use the various properties to set commonly used configuration options, like the default isolation level. + DefaultIsolationLevel = IsolationLevel.RepeatableRead, + + // The Options property can be used to set any valid connection property that is not included as a + // programmatic option on the SpannerConnectionStringBuilder class. + Options = "disable_route_to_leader=true;statement_cache_size=100", + }; + + // Create a connection using the generated connection string from the builder. + await using var connection = new SpannerConnection(builder.ConnectionString); + await connection.OpenAsync(); + + // Execute a command on Spanner using the connection. + await using var command = connection.CreateCommand(); + command.CommandText = "SELECT @greeting as Message"; + command.Parameters.AddWithValue("greeting", "Hello from Spanner", DbType.String); + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + Console.WriteLine($"Greeting: {reader.GetString(0)}"); + } + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DataTypesSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DataTypesSample.cs new file mode 100644 index 00000000..d5cbc9f3 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DataTypesSample.cs @@ -0,0 +1,80 @@ +namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; + +// Sample showing how to work with the different data types that are supported by Spanner: +// 1. How to set data of each type as a statement parameter. +// 2. How to get data from columns of each type. +public class DataTypesSample +{ + private const string CreateAllTypesTable = @" + CREATE TABLE IF NOT EXISTS AllTypes ( + id int64 primary key, + col_bool bool, + col_bytes bytes(max), + col_date date, + col_float32 float32, + col_float64 float64, + col_int64 int64, + --col_interval interval, + col_json json, + col_numeric numeric, + col_string string(max), + col_timestamp timestamp, + )"; + + public static async Task Run(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // Create a table that contains all data types. + await using var createCommand = connection.CreateCommand(); + createCommand.CommandText = CreateAllTypesTable; + await createCommand.ExecuteNonQueryAsync(); + + // Insert a row into the table using query parameters. + await using var insertCommand = connection.CreateCommand(); + insertCommand.CommandText = + "insert or update into AllTypes " + + "(id, col_bool, col_bytes, col_date, col_float32, col_float64, col_int64, /*col_interval,*/ col_json, col_numeric, col_string, col_timestamp) " + + "values (@id, @bool, @bytes, @date, @float32, @float64, @int64, /*@interval,*/ @json, @numeric, @string, @timestamp)"; + insertCommand.Parameters.AddWithValue("id", 1); + // Use bool for BOOL values. + insertCommand.Parameters.AddWithValue("bool", true); + // Use byte[] for BYTES values. + insertCommand.Parameters.AddWithValue("bytes", new byte[] { 1, 2, 3 }); + // Use DateOnly for DATE values. + insertCommand.Parameters.AddWithValue("date", DateOnly.FromDateTime(DateTime.Now)); + // Use float for FLOAT32 values. + insertCommand.Parameters.AddWithValue("float32", 3.14f); + // Use double for FLOAT64 values. + insertCommand.Parameters.AddWithValue("float64", 3.14d); + // Use long for INT64 values. + insertCommand.Parameters.AddWithValue("int64", 100L); + // Use TimeSpan for INTERVAL values. + // TODO: Enable the following line when the Emulator supports INTERVAL. + // insertCommand.Parameters.AddWithValue("interval", TimeSpan.FromMinutes(1)); + // Use strings for JSON values. + insertCommand.Parameters.AddWithValue("json", "{\"key\": \"value\"}"); + // Use decimal for NUMERIC values. + insertCommand.Parameters.AddWithValue("numeric", 3.14m); + // Use string for STRING values. + insertCommand.Parameters.AddWithValue("string", "test-string"); + // Use DateTime for TIMESTAMP values. + insertCommand.Parameters.AddWithValue("timestamp", DateTime.Now); + + long inserted = await insertCommand.ExecuteNonQueryAsync(); + Console.WriteLine($"Inserted: {inserted}"); + + // Retrieve the row that was just inserted. + await using var selectCommand = connection.CreateCommand("select * from AllTypes order by id"); + await using var reader = await selectCommand.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + for (var col = 0; col < reader.FieldCount; col++) + { + Console.WriteLine($"{reader.GetName(col)}: {reader.GetValue(col)}"); + } + } + } + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DdlBatchSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DdlBatchSample.cs new file mode 100644 index 00000000..287b5846 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DdlBatchSample.cs @@ -0,0 +1,99 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; + +/// +/// This sample shows how to execute DDL batches using the Spanner ADO.NET data provider. +/// Executing multiple DDL statements as a single batch is much more efficient than +/// executing them as separate statements. +/// +public class DdlBatchSample +{ + public static async Task Run(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + string[] statements = [ + "create table if not exists table1 (id int64 primary key, value string(max))", + "create table if not exists table2 (id int64 primary key, value string(max))" + ]; + + // Execute a batch of DDL statements using the generic ADO.NET DbBatch API. + await ExecuteWithAdoBatch(connection, statements); + + // Execute a batch of DDL statements using START BATCH DDL / RUN BATCH commands. + await ExecuteWithStartBatchDdl(connection, statements); + + // Execute a batch of DDL statements as a single command text. + // TODO: Enable when the library supports multi-statement SQL strings. + // await ExecuteAsSingleCommand(connection, statements); + } + + private static async Task ExecuteAsSingleCommand(SpannerConnection connection, string[] statements) + { + // A batch of DDL statements can also be executed as a single SQL command text. + // Each statement should be separated by a semicolon. + await using var command = connection.CreateCommand(); + command.CommandText = string.Join(";", statements); + await command.ExecuteNonQueryAsync(); + + Console.WriteLine("Executed a single SQL string with multiple DDL statements as one batch."); + } + + /// + /// This method shows how to use the generic ADO.NET batch API to execute a batch of DDL statements. + /// + private static async Task ExecuteWithAdoBatch(SpannerConnection connection, string[] statements) + { + var batch = connection.CreateBatch(); + foreach (var statement in statements) + { + var command = batch.CreateBatchCommand(); + command.CommandText = statement; + batch.BatchCommands.Add(command); + } + // Execute the batch of DDL statements. + await batch.ExecuteNonQueryAsync(); + + Console.WriteLine("Executed ADO.NET batch"); + } + + /// + /// This method shows how to use the custom SQL statements START BATCH DDL / RUN BATCH to execute a batch of DDL + /// statements. + /// + private static async Task ExecuteWithStartBatchDdl(SpannerConnection connection, string[] statements) + { + var command = connection.CreateCommand(); + command.CommandText = "START BATCH DDL"; + await command.ExecuteNonQueryAsync(); + + // All following DDL statements will just be buffered in memory until we execute RUN BATCH. + foreach (var statement in statements) + { + command.CommandText = statement; + // This does not really execute anything on Spanner, as the DDL statement is just buffered in memory. + await command.ExecuteNonQueryAsync(); + } + + // Execute the batch of DDL statements. + command.CommandText = "RUN BATCH"; + await command.ExecuteNonQueryAsync(); + + Console.WriteLine("Executed DDL batch"); + } + +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DmlBatchSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DmlBatchSample.cs new file mode 100644 index 00000000..47ab909f --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DmlBatchSample.cs @@ -0,0 +1,132 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Data.Common; + +namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; + +/// +/// This sample shows how to execute DML batches using the Spanner ADO.NET data provider. +/// Executing multiple DML statements as a single batch is more efficient than +/// executing them as separate statements, as it reduces the number of round-trips to Spanner. +/// +public class DmlBatchSample +{ + private struct Singer + { + internal long Id; + internal string FirstName; + internal string LastName; + } + + public static async Task Run(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + Singer[] singers = [ + new() {Id = Random.Shared.NextInt64(), FirstName = "Lea", LastName = "Martin"}, + new() {Id = Random.Shared.NextInt64(), FirstName = "David", LastName = "Lomond"}, + new() {Id = Random.Shared.NextInt64(), FirstName = "Elena", LastName = "Campbell"}, + ]; + const string insertStatement = "insert or update into Singers (SingerId, FirstName, LastName) values (@Id, @FirstName, @LastName)"; + const string updateStatement = "update Singers set BirthDate = NULL where BirthDate < DATE '1900-01-01'"; + + // Execute a batch of DDL statements using the generic ADO.NET DbBatch API. + await ExecuteWithAdoBatch(connection, insertStatement, updateStatement, singers); + + // Execute a batch of DDL statements using START BATCH DDL / RUN BATCH commands. + await ExecuteWithStartBatchDml(connection, insertStatement, updateStatement, singers); + } + + /// + /// This method shows how to use the generic ADO.NET batch API to execute a batch of DML statements. + /// + private static async Task ExecuteWithAdoBatch(SpannerConnection connection, string insertStatement, string updateStatement, Singer[] singers) + { + var batch = connection.CreateBatch(); + // Add some INSERT statements to the batch. + foreach (var singer in singers) + { + var command = batch.CreateBatchCommand(); + command.CommandText = insertStatement; + AddBatchParameter(command, "Id", singer.Id); + AddBatchParameter(command, "FirstName", singer.FirstName); + AddBatchParameter(command, "LastName", singer.LastName); + batch.BatchCommands.Add(command); + } + // Add an UPDATE statement to the batch. + var updateCommand = batch.CreateBatchCommand(); + updateCommand.CommandText = updateStatement; + batch.BatchCommands.Add(updateCommand); + + // Execute the batch of DML statements. + long affected = await batch.ExecuteNonQueryAsync(); + + Console.WriteLine("Executed ADO.NET batch"); + Console.WriteLine("Affected: " + affected); + Console.WriteLine(); + } + + private static void AddBatchParameter(DbBatchCommand command, string name, object value) + { + var parameter = command.CreateParameter(); + parameter.ParameterName = name; + parameter.Value = value; + command.Parameters.Add(parameter); + } + + /// + /// This method shows how to use the custom SQL statements START BATCH DML / RUN BATCH to execute a batch of DML + /// statements. + /// + private static async Task ExecuteWithStartBatchDml(SpannerConnection connection, string insertStatement, string updateStatement, Singer[] singers) + { + await using var command = connection.CreateCommand(); + command.CommandText = "START BATCH DML"; + await command.ExecuteNonQueryAsync(); + + // All following DML statements will just be buffered in memory until we execute RUN BATCH. + foreach (var singer in singers) + { + await using var insertCommand = connection.CreateCommand(); + insertCommand.CommandText = insertStatement; + AddParameter(insertCommand, "Id", singer.Id); + AddParameter(insertCommand, "FirstName", singer.FirstName); + AddParameter(insertCommand, "LastName", singer.LastName); + // This does not really execute anything on Spanner, as the DML statement is just buffered in memory. + await insertCommand.ExecuteNonQueryAsync(); + } + await using var updateCommand = connection.CreateCommand(); + updateCommand.CommandText = updateStatement; + await updateCommand.ExecuteNonQueryAsync(); + + // Execute the batch of DML statements. + command.CommandText = "RUN BATCH"; + var affected = await command.ExecuteNonQueryAsync(); + + Console.WriteLine("Executed DML batch"); + Console.WriteLine("Affected: " + affected); + Console.WriteLine(); + } + + private static void AddParameter(DbCommand command, string name, object value) + { + var parameter = command.CreateParameter(); + parameter.ParameterName = name; + parameter.Value = value; + command.Parameters.Add(parameter); + } + +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/MutationsSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/MutationsSample.cs new file mode 100644 index 00000000..7286e102 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/MutationsSample.cs @@ -0,0 +1,44 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; + +/// +/// Example for using Mutations with the Spanner ADO.NET data provider. +/// +public class MutationsSample +{ + public static async Task Run(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // The following methods can be used to create commands that use Mutations instead of DML: + // - CreateInsertCommand + // - CreateInsertOrUpdateCommand + // - CreateUpdateCommand + // - CreateReplaceCommand + // - CreateDeleteCommand + // See https://docs.cloud.google.com/spanner/docs/dml-versus-mutations for more information on mutations. + await using var command = connection.CreateInsertCommand("Singers"); + // The parameter names must correspond to column names in the table. + command.Parameters.AddWithValue("SingerId", Random.Shared.NextInt64()); + command.Parameters.AddWithValue("FirstName", "Bruce"); + command.Parameters.AddWithValue("LastName", "Allison"); + long affected = await command.ExecuteNonQueryAsync(); + + Console.WriteLine($"Inserted data using mutations. Affected: {affected}"); + Console.WriteLine(); + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/PartitionedDmlSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/PartitionedDmlSample.cs new file mode 100644 index 00000000..d6f2a28b --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/PartitionedDmlSample.cs @@ -0,0 +1,50 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; + +/// +/// Example for using Partitioned DML with the Spanner ADO.NET data provider. +/// +public class PartitionedDmlSample +{ + public static async Task Run(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // Partitioned DML can be used for bulk updates and bulk deletes that exceed the mutation limit in Spanner. + // Partitioned DML statements must be executed outside transactions, also known as in auto-commit mode. + // Use the AUTOCOMMIT_DML_MODE variable to set the type of transaction that the ADO.NET driver should use for + // DML statements in auto-commit mode. + + // AUTOCOMMIT_DML_MODE supports two values: + // 1. 'TRANSACTIONAL': Use a normal, atomic read/write transaction for the statement. + // 2. 'PARTITIONED_NON_ATOMIC': Use a Partitioned DML transaction for the statement. + await using var command = connection.CreateCommand(); + command.CommandText = "set autocommit_dml_mode = 'partitioned_non_atomic'"; + await command.ExecuteNonQueryAsync(); + + // Do a bulk update to set a default value for all unknown birthdates. + command.CommandText = "update Singers set BirthDate=date '1900-01-01' where BirthDate is null"; + long affected = await command.ExecuteNonQueryAsync(); + + // Reset the mode to the default. + command.CommandText = "reset autocommit_dml_mode"; + await command.ExecuteNonQueryAsync(); + + Console.WriteLine($"Executed a Partitioned DML statement. Affected: {affected}"); + Console.WriteLine(); + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/QueryParametersSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/QueryParametersSample.cs index f633c81a..b17c111d 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/QueryParametersSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/QueryParametersSample.cs @@ -14,12 +14,28 @@ namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; +/// +/// This sample shows how to use query parameters with the Spanner ADO.NET data provider. +/// Using query parameters is recommended, as it allows Spanner to cache and reuse the query plan, +/// and it helps to prevent SQL injection attacks in your application. +/// public class QueryParametersSample { public static async Task Run(string connectionString) { await using var connection = new SpannerConnection(connectionString); await connection.OpenAsync(); + + // The Spanner ADO.NET driver supports named query parameters. + await NamedQueryParameters(connection); + + // The Spanner ADO.NET driver also supports positional query parameters. + // Use a question mark (?) for positional query parameters in the SQL string. + await PositionalQueryParameters(connection); + } + + private static async Task NamedQueryParameters(SpannerConnection connection) + { await using var command = connection.CreateCommand(); command.CommandText = "SELECT SingerId, FullName " + "FROM Singers " + @@ -31,7 +47,26 @@ public static async Task Run(string connectionString) await using var reader = await command.ExecuteReaderAsync(); while (await reader.ReadAsync()) { - Console.WriteLine($"Found singer: {reader.GetString(1)}"); + Console.WriteLine($"Found singer with named parameters: {reader.GetString(1)}"); + } + } + + private static async Task PositionalQueryParameters(SpannerConnection connection) + { + await using var command = connection.CreateCommand(); + command.CommandText = "SELECT SingerId, FullName " + + "FROM Singers " + + "WHERE LastName LIKE ? " + + " OR FirstName LIKE ? " + + "ORDER BY LastName, FirstName"; + // The driver allows you to just add the parameter values that you want to use when using positional parameters. + // The values must be added in the order of the positional parameters in the SQL string. + command.Parameters.Add("R%"); + command.Parameters.Add("A%"); + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + Console.WriteLine($"Found singer with positional parameters: {reader.GetString(1)}"); } } } diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/ReadOnlyTransactionSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/ReadOnlyTransactionSample.cs new file mode 100644 index 00000000..39e945c8 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/ReadOnlyTransactionSample.cs @@ -0,0 +1,52 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; + +/// +/// This sample shows how to execute a read-only transaction using the Spanner ADO.NET data provider. +/// Read-only transactions do not take locks on Spanner, and should be used when your application needs +/// to execute multiple queries that read from the same snapshot of the database. +/// +public class ReadOnlyTransactionSample +{ + public static async Task Run(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // Start a read-only transaction using the BeginReadOnlyTransaction method. + await using var transaction = connection.BeginReadOnlyTransaction(); + + // Execute a query that uses this transaction. + await using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.CommandText = "SELECT SingerId, FullName " + + "FROM Singers " + + "WHERE LastName LIKE @lastName " + + " OR FirstName LIKE @firstName " + + "ORDER BY LastName, FirstName"; + command.Parameters.AddWithValue("lastName", "R%"); + command.Parameters.AddWithValue("firstName", "A%"); + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + Console.WriteLine($"Found singer: {reader.GetString(1)}"); + } + + // The read-only transaction must be committed or rolled back to release it from the connection. + // Committing or rolling back a read-only transaction is a no-op on Spanner. + await transaction.CommitAsync(); + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/create_sample_tables.sql b/drivers/spanner-ado-net/spanner-ado-net-samples/create_sample_tables.sql index 4eb89576..95522b8b 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/create_sample_tables.sql +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/create_sample_tables.sql @@ -19,7 +19,6 @@ CREATE TABLE IF NOT EXISTS Singers ( LastName STRING(200) NOT NULL, BirthDate DATE, Picture BYTES(MAX), - Version INT64 NOT NULL, FullName STRING(400) NOT NULL AS (COALESCE(FirstName || ' ', '') || LastName) STORED, ) PRIMARY KEY (SingerId); @@ -30,7 +29,6 @@ CREATE TABLE IF NOT EXISTS Albums ( Title STRING(100) NOT NULL, ReleaseDate DATE, SingerId INT64 NOT NULL, - Version INT64 NOT NULL, CONSTRAINT FK_Albums_Singers FOREIGN KEY (SingerId) REFERENCES Singers (SingerId), ) PRIMARY KEY (AlbumId); @@ -42,7 +40,6 @@ CREATE TABLE IF NOT EXISTS Tracks ( RecordedAt TIMESTAMP DEFAULT (CURRENT_TIMESTAMP), LyricsLanguages ARRAY, Lyrics ARRAY, - Version INT64 NOT NULL, ) PRIMARY KEY (AlbumId, TrackId), INTERLEAVE IN PARENT Albums ON DELETE CASCADE; CREATE UNIQUE INDEX IF NOT EXISTS Idx_Tracks_AlbumId_Title ON Tracks (AlbumId, Title); @@ -53,7 +50,6 @@ CREATE TABLE IF NOT EXISTS Venues ( Description JSON, Active BOOL NOT NULL, Descriptions JSON, - Version INT64 NOT NULL, ) PRIMARY KEY (Code); CREATE TABLE IF NOT EXISTS Concerts ( @@ -61,7 +57,6 @@ CREATE TABLE IF NOT EXISTS Concerts ( StartTime TIMESTAMP NOT NULL, SingerId INT64 NOT NULL, Title STRING(200), - Version INT64 NOT NULL, CONSTRAINT FK_Concerts_Venues FOREIGN KEY (VenueCode) REFERENCES Venues (Code), CONSTRAINT FK_Concerts_Singers FOREIGN KEY (SingerId) REFERENCES Singers (SingerId), ) PRIMARY KEY (VenueCode, StartTime, SingerId); @@ -76,7 +71,6 @@ CREATE TABLE IF NOT EXISTS Performances ( Rating FLOAT64, CreatedAt TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true), LastUpdatedAt TIMESTAMP OPTIONS (allow_commit_timestamp=true), - Version INT64 NOT NULL, CONSTRAINT FK_Performances_Concerts FOREIGN KEY (VenueCode, ConcertStartTime, SingerId) REFERENCES Concerts (VenueCode, StartTime, SingerId), CONSTRAINT FK_Performances_Singers FOREIGN KEY (SingerId) REFERENCES Singers (SingerId), CONSTRAINT FK_Performances_Tracks FOREIGN KEY (AlbumId, TrackId) REFERENCES Tracks (AlbumId, TrackId), @@ -96,7 +90,6 @@ CREATE TABLE IF NOT EXISTS TicketSales ( VenueCode STRING(10) NOT NULL, ConcertStartTime TIMESTAMP NOT NULL, SingerId INT64 NOT NULL, - Version INT64 NOT NULL, CONSTRAINT FK_TicketSales_Concerts FOREIGN KEY (VenueCode, ConcertStartTime, SingerId) REFERENCES Concerts (VenueCode, StartTime, SingerId), ) PRIMARY KEY (Id); diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/insert_sample_data.sql b/drivers/spanner-ado-net/spanner-ado-net-samples/insert_sample_data.sql index 0e3406be..f10a6a4c 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/insert_sample_data.sql +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/insert_sample_data.sql @@ -13,7 +13,7 @@ * limitations under the License. */ -INSERT OR UPDATE INTO Singers (SingerId, FirstName, LastName, BirthDate, Picture, Version) VALUES - (1, 'Mark', 'Richards', DATE '1990-11-09', NULL, 1), - (2, 'Catalina', 'Smith', DATE '1998-04-29', NULL, 1), - (3, 'Alice', 'Trentor', DATE '1979-10-15', NULL, 1); +INSERT OR UPDATE INTO Singers (SingerId, FirstName, LastName, BirthDate, Picture) VALUES + (1, 'Mark', 'Richards', DATE '1990-11-09', NULL), + (2, 'Catalina', 'Smith', DATE '1998-04-29', NULL), + (3, 'Alice', 'Trentor', DATE '1979-10-15', NULL); diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs index 30ca2cab..41a83e59 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs @@ -31,6 +31,7 @@ namespace Google.Cloud.Spanner.DataProvider; public class SpannerBatch : DbBatch { private SpannerConnection SpannerConnection => (SpannerConnection)Connection!; + public new SpannerBatchCommandCollection BatchCommands => (SpannerBatchCommandCollection)base.BatchCommands; protected override SpannerBatchCommandCollection DbBatchCommands { get; } = new(); public override int Timeout { get; set; } protected override DbConnection? DbConnection { get; set; } diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerBatchCommandCollection.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatchCommandCollection.cs index 84f91f9e..2a307834 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerBatchCommandCollection.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatchCommandCollection.cs @@ -37,6 +37,21 @@ public override IEnumerator GetEnumerator() return _commands.GetEnumerator(); } + /// + /// Adds a new command to the batch with the given command text. + /// + /// The command text for the batch command + /// The new batch command + public SpannerBatchCommand Add(string commandText) + { + var cmd = new SpannerBatchCommand + { + CommandText = commandText + }; + _commands.Add(cmd); + return cmd; + } + public override void Add(DbBatchCommand item) { GaxPreconditions.CheckNotNull(item, nameof(item)); diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs index db653621..fb674919 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs @@ -343,6 +343,18 @@ private void AssertClosed() EnsureOpen(); return LibConnection!.WriteMutationsAsync(mutations, cancellationToken); } + + /// + /// Create a new command for this connection with the given command text. + /// + /// The command text to set for the command + /// A new command with the given command text + public SpannerCommand CreateCommand(string commandText) + { + var cmd = CreateCommand(); + cmd.CommandText = commandText; + return cmd; + } public new SpannerCommand CreateCommand() => (SpannerCommand) base.CreateCommand(); @@ -351,6 +363,8 @@ protected override DbCommand CreateDbCommand() var cmd = new SpannerCommand(this); return cmd; } + + public new SpannerBatch CreateBatch() => (SpannerBatch) base.CreateBatch(); protected override DbBatch CreateDbBatch() { @@ -378,17 +392,47 @@ public long[] ExecuteBatchDml(List commands) return TranslateException(() => LibConnection!.ExecuteBatch(statements)); } - public DbCommand CreateInsertCommand(string table) + /// + /// Creates a command to insert data into Spanner using mutations. + /// + /// The table to insert data into + public SpannerCommand CreateInsertCommand(string table) { return new SpannerCommand(this, new Mutation { Insert = new Mutation.Types.Write { Table = table } }); } - public DbCommand CreateUpdateCommand(string table) + /// + /// Creates a command to insert-or-update data into Spanner using mutations. + /// + /// The table to insert-or-update data into + public SpannerCommand CreateInsertOrUpdateCommand(string table) + { + return new SpannerCommand(this, new Mutation { InsertOrUpdate = new Mutation.Types.Write { Table = table } }); + } + + /// + /// Creates a command to update data in Spanner using mutations. + /// + /// The table that contains the data that should be updated + public SpannerCommand CreateUpdateCommand(string table) { return new SpannerCommand(this, new Mutation { Update = new Mutation.Types.Write { Table = table } }); } - public DbCommand CreateDeleteCommand(string table) + /// + /// Creates a command to replace data in Spanner using mutations. + /// + /// The table that contains the data that should be replaced + public SpannerCommand CreateReplaceCommand(string table) + { + return new SpannerCommand(this, new Mutation { Replace = new Mutation.Types.Write { Table = table } }); + } + + /// + /// Creates a command to delete data in Spanner using mutations. + /// + /// The table that contains the data that should be deleted + public SpannerCommand CreateDeleteCommand(string table) { return new SpannerCommand(this, new Mutation { Delete = new Mutation.Types.Delete { Table = table } }); } diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerParameterCollection.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerParameterCollection.cs index 26691ae5..5f17a151 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerParameterCollection.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerParameterCollection.cs @@ -15,6 +15,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Data; using System.Data.Common; using Google.Api.Gax; using Google.Protobuf.Collections; @@ -48,13 +49,17 @@ public override int Add(object value) return index; } - public SpannerParameter AddWithValue(string parameterName, object? value) + public SpannerParameter AddWithValue(string parameterName, object? value, DbType? type = null) { var parameter = new SpannerParameter { ParameterName = parameterName, Value = value, }; + if (type != null) + { + parameter.DbType = type.Value; + } Add(parameter); return parameter; } From 58121f35a5590f975e7f95b7ddeaf184092eb284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Wed, 26 Nov 2025 17:44:59 +0100 Subject: [PATCH 15/29] chore: add support for tags --- .../Snippets/StaleReadSample.cs | 85 ++++++ .../Snippets/TagsSample.cs | 45 +++ .../spanner-ado-net-tests/TagTests.cs | 275 ++++++++++++++++++ .../spanner-ado-net/SpannerBatch.cs | 24 ++ .../spanner-ado-net/SpannerCommand.cs | 17 +- .../spanner-ado-net/SpannerConnection.cs | 21 +- .../spanner-ado-net/SpannerTransaction.cs | 24 ++ spannerlib/api/connection.go | 17 +- 8 files changed, 503 insertions(+), 5 deletions(-) create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/StaleReadSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/TagsSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-tests/TagTests.cs diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/StaleReadSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/StaleReadSample.cs new file mode 100644 index 00000000..b4b602f0 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/StaleReadSample.cs @@ -0,0 +1,85 @@ +// Copyright 2025 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 +// +// https://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. + +using Google.Cloud.Spanner.V1; +using Google.Protobuf.WellKnownTypes; + +namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; + +/// +/// This sample shows how to execute stale reads using both single statements and read-only transaction +/// with the Spanner ADO.NET data provider. +/// +public class StaleReadSample +{ + public static async Task Run(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // Execute a single stale query. + await SingleStaleQuery(connection); + // Execute a read-only transaction with a staleness setting. + await StaleReadOnlyTransaction(connection); + } + + /// + /// Executes a single query that uses a max staleness setting. + /// + private static async Task SingleStaleQuery(SpannerConnection connection) + { + await using var command = connection.CreateCommand(); + command.SingleUseReadOnlyTransactionOptions = new TransactionOptions.Types.ReadOnly + { + MaxStaleness = Duration.FromTimeSpan(TimeSpan.FromSeconds(10)), + }; + command.CommandText = "SELECT SingerId, FullName " + + "FROM Singers " + + "ORDER BY LastName, FirstName"; + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + Console.WriteLine($"Found singer using a single stale query: {reader.GetString(1)}"); + } + } + + /// + /// Executes a read-only transaction with an exact staleness. + /// + /// + private static async Task StaleReadOnlyTransaction(SpannerConnection connection) + { + // Start a read-only transaction using the BeginReadOnlyTransaction method. + await using var transaction = connection.BeginReadOnlyTransaction(new TransactionOptions.Types.ReadOnly + { + ExactStaleness = Duration.FromTimeSpan(TimeSpan.FromSeconds(10)), + }); + + // Execute a query that uses this transaction. + await using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.CommandText = "SELECT SingerId, FullName " + + "FROM Singers " + + "ORDER BY LastName, FirstName"; + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + Console.WriteLine($"Found singer using a stale read-only transaction: {reader.GetString(1)}"); + } + + // The read-only transaction must be committed or rolled back to release it from the connection. + // Committing or rolling back a read-only transaction is a no-op on Spanner. + await transaction.CommitAsync(); + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/TagsSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/TagsSample.cs new file mode 100644 index 00000000..9b516145 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/TagsSample.cs @@ -0,0 +1,45 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; + +/// +/// This sample shows how to use request and transaction tags with the Spanner ADO.NET data provider. +/// +public class TagsSample +{ + public static async Task Run(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // Set a transaction tag on a read/write transaction. + await using var transaction = await connection.BeginTransactionAsync(); + transaction.Tag = "my_transaction_tag"; + + // Set a request tag on a command. + // Assign the transaction to a command to instruct the command to use the transaction and transaction tag. + await using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.Tag = "my_query_tag"; + command.CommandText = "SELECT 'Hello World' as Message"; + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + Console.WriteLine($"Greeting from Spanner: {reader.GetString(0)}"); + } + + await transaction.CommitAsync(); + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/TagTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/TagTests.cs new file mode 100644 index 00000000..e0bb4ae8 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/TagTests.cs @@ -0,0 +1,275 @@ +// Copyright 2025 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 +// +// https://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. + +using Google.Cloud.Spanner.V1; +using Google.Cloud.SpannerLib.MockServer; + +namespace Google.Cloud.Spanner.DataProvider.Tests; + +public class TagTests : AbstractMockServerTests +{ + [Test] + public async Task TestRequestTag([Values] bool async) + { + const string sql = "insert into my_table (id, value) values (1, 'One')"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(1L)); + + await using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + await connection.OpenAsync(); + + await using var command = connection.CreateCommand(); + command.CommandText = sql; + command.Tag = "my_tag"; + if (async) + { + await command.ExecuteNonQueryAsync(); + } + else + { + command.ExecuteNonQuery(); + } + + var requests = Fixture.SpannerMock.Requests.ToList(); + var request = requests.OfType().First(); + Assert.That(request, Is.Not.Null); + Assert.That(request.RequestOptions, Is.Not.Null); + Assert.That(request.RequestOptions.RequestTag, Is.EqualTo("my_tag")); + Assert.That(request.RequestOptions.TransactionTag, Is.EqualTo("")); + } + + [Test] + public async Task TestTransactionTag([Values] bool async) + { + const string sql = "insert into my_table (id, value) values (1, 'One')"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(1L)); + + await using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + await connection.OpenAsync(); + + await using var transaction = await connection.BeginTransactionAsync(); + transaction.Tag = "my_tx_tag"; + + await using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.CommandText = sql; + command.Tag = "my_tag1"; + if (async) + { + await command.ExecuteNonQueryAsync(); + } + else + { + command.ExecuteNonQuery(); + } + + command.Tag = "my_tag2"; + if (async) + { + await command.ExecuteNonQueryAsync(); + } + else + { + command.ExecuteNonQuery(); + } + + var requests = Fixture.SpannerMock.Requests.ToList(); + var request = requests.OfType().First(); + Assert.That(request, Is.Not.Null); + Assert.That(request.RequestOptions, Is.Not.Null); + Assert.That(request.RequestOptions.RequestTag, Is.EqualTo("my_tag1")); + Assert.That(request.RequestOptions.TransactionTag, Is.EqualTo("my_tx_tag")); + + request = requests.OfType().ToList()[1]; + Assert.That(request.RequestOptions.RequestTag, Is.EqualTo("my_tag2")); + Assert.That(request.RequestOptions.TransactionTag, Is.EqualTo("my_tx_tag")); + } + + [Test] + public async Task TestRequestTagBatch([Values] bool async) + { + const string sql = "insert into my_table (id, value) values (1, 'One')"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(1L)); + + await using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + await connection.OpenAsync(); + + await using var batch = connection.CreateBatch(); + batch.Tag = "my_tag"; + batch.BatchCommands.Add(sql); + batch.BatchCommands.Add(sql); + if (async) + { + await batch.ExecuteNonQueryAsync(); + } + else + { + // ReSharper disable once MethodHasAsyncOverload + batch.ExecuteNonQuery(); + } + + var requests = Fixture.SpannerMock.Requests.ToList(); + var request = requests.OfType().First(); + Assert.That(request, Is.Not.Null); + Assert.That(request.RequestOptions, Is.Not.Null); + Assert.That(request.RequestOptions.RequestTag, Is.EqualTo("my_tag")); + Assert.That(request.RequestOptions.TransactionTag, Is.EqualTo("")); + } + + [Test] + public async Task TestTransactionTagBatch([Values] bool async) + { + const string sql = "insert into my_table (id, value) values (1, 'One')"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(1L)); + + await using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + await connection.OpenAsync(); + + await using var transaction = await connection.BeginTransactionAsync(); + transaction.Tag = "my_tx_tag"; + + await using var batch = connection.CreateBatch(); + batch.Transaction = transaction; + batch.Tag = "my_tag"; + batch.BatchCommands.Add(sql); + batch.BatchCommands.Add(sql); + if (async) + { + await batch.ExecuteNonQueryAsync(); + } + else + { + // ReSharper disable once MethodHasAsyncOverload + batch.ExecuteNonQuery(); + } + + var requests = Fixture.SpannerMock.Requests.ToList(); + var request = requests.OfType().First(); + Assert.That(request, Is.Not.Null); + Assert.That(request.RequestOptions, Is.Not.Null); + Assert.That(request.RequestOptions.RequestTag, Is.EqualTo("my_tag")); + Assert.That(request.RequestOptions.TransactionTag, Is.EqualTo("my_tx_tag")); + } + + [Test] + public async Task TestMultipleStatements([Values] bool async) + { + const string dml = "insert into my_table (id, value) values (1, 'One')"; + Fixture.SpannerMock.AddOrUpdateStatementResult(dml, StatementResult.CreateUpdateCount(1L)); + const string query = "select * from my_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(query, StatementResult.CreateSelect1ResultSet()); + + await using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + await connection.OpenAsync(); + + await using var transaction = await connection.BeginTransactionAsync(); + transaction.Tag = "my_tx_tag"; + + await using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.CommandText = query; + command.Tag = "my_query"; + if (async) + { + await using var reader = await command.ExecuteReaderAsync(); + } + else + { + // ReSharper disable once MethodHasAsyncOverload + await using var reader = command.ExecuteReader(); + } + + command.Tag = "my_scalar"; + if (async) + { + await command.ExecuteScalarAsync(); + } + else + { + command.ExecuteScalar(); + } + + command.Tag = "my_dml"; + command.CommandText = dml; + if (async) + { + await command.ExecuteNonQueryAsync(); + } + else + { + command.ExecuteNonQuery(); + } + + await using var batch = connection.CreateBatch(); + batch.Transaction = transaction; + batch.Tag = "my_batch"; + batch.BatchCommands.Add(dml); + batch.BatchCommands.Add(dml); + if (async) + { + await batch.ExecuteNonQueryAsync(); + } + else + { + // ReSharper disable once MethodHasAsyncOverload + batch.ExecuteNonQuery(); + } + + if (async) + { + await transaction.CommitAsync(); + } + else + { + // ReSharper disable once MethodHasAsyncOverload + transaction.Commit(); + } + + var requests = Fixture.SpannerMock.Requests.ToList(); + var batchRequest = requests.OfType().First(); + Assert.That(batchRequest, Is.Not.Null); + Assert.That(batchRequest.RequestOptions, Is.Not.Null); + Assert.That(batchRequest.RequestOptions.RequestTag, Is.EqualTo("my_batch")); + Assert.That(batchRequest.RequestOptions.TransactionTag, Is.EqualTo("my_tx_tag")); + + var executeRequests = requests.OfType().ToList(); + Assert.That(executeRequests, Has.Count.EqualTo(3)); + var queryRequest = executeRequests[0]; + Assert.That(queryRequest, Is.Not.Null); + Assert.That(queryRequest.RequestOptions, Is.Not.Null); + Assert.That(queryRequest.RequestOptions.RequestTag, Is.EqualTo("my_query")); + Assert.That(queryRequest.RequestOptions.TransactionTag, Is.EqualTo("my_tx_tag")); + + var scalarRequest = executeRequests[1]; + Assert.That(scalarRequest, Is.Not.Null); + Assert.That(scalarRequest.RequestOptions, Is.Not.Null); + Assert.That(scalarRequest.RequestOptions.RequestTag, Is.EqualTo("my_scalar")); + Assert.That(scalarRequest.RequestOptions.TransactionTag, Is.EqualTo("my_tx_tag")); + + var dmlRequest = executeRequests[2]; + Assert.That(dmlRequest, Is.Not.Null); + Assert.That(dmlRequest.RequestOptions, Is.Not.Null); + Assert.That(dmlRequest.RequestOptions.RequestTag, Is.EqualTo("my_dml")); + Assert.That(dmlRequest.RequestOptions.TransactionTag, Is.EqualTo("my_tx_tag")); + + var commitRequest = requests.OfType().First(); + Assert.That(commitRequest, Is.Not.Null); + Assert.That(commitRequest.RequestOptions, Is.Not.Null); + Assert.That(commitRequest.RequestOptions.TransactionTag, Is.EqualTo("my_tx_tag")); + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs index 41a83e59..ef038b10 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs @@ -36,6 +36,8 @@ public class SpannerBatch : DbBatch public override int Timeout { get; set; } protected override DbConnection? DbConnection { get; set; } protected override DbTransaction? DbTransaction { get; set; } + private SpannerTransaction? SpannerTransaction => DbTransaction as SpannerTransaction; + public string? Tag { get; set; } public SpannerBatch() {} @@ -74,6 +76,24 @@ protected override Task ExecuteDbDataReaderAsync(CommandBehavior b return statements; } + private void SetRequestTag() + { + if (Tag != null) + { + var command = SpannerConnection.CreateCommand($"set statement_tag = '{Tag}'"); + command.ExecuteNonQuery(); + } + } + + private async Task SetRequestTagAsync(CancellationToken cancellationToken) + { + if (Tag != null) + { + var command = SpannerConnection.CreateCommand($"set statement_tag = '{Tag}'"); + await command.ExecuteNonQueryAsync(cancellationToken); + } + } + public override int ExecuteNonQuery() { if (DbBatchCommands.Count == 0) @@ -81,6 +101,8 @@ public override int ExecuteNonQuery() return 0; } var statements = CreateStatements(); + SpannerTransaction?.MarkUsed(initTag: true); + SetRequestTag(); var results = SpannerConnection.LibConnection!.ExecuteBatch(statements); DbBatchCommands.SetAffected(results); return (int) results.Sum(); @@ -93,6 +115,8 @@ public override async Task ExecuteNonQueryAsync(CancellationToken cancellat return 0; } var statements = CreateStatements(); + SpannerTransaction?.MarkUsed(initTag: true); + await SetRequestTagAsync(cancellationToken); var results = await SpannerConnection.LibConnection!.ExecuteBatchAsync(statements); DbBatchCommands.SetAffected(results); return (int) results.Sum(); diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs index bc2f0661..c9f1a62c 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs @@ -69,7 +69,14 @@ protected override DbTransaction? DbTransaction private readonly Mutation? _mutation; public TransactionOptions.Types.ReadOnly? SingleUseReadOnlyTransactionOptions { get; set; } - public RequestOptions? RequestOptions { get; set; } + + public string Tag + { + get => RequestOptions.RequestTag; + set => RequestOptions.RequestTag = value; + } + + private RequestOptions RequestOptions { get; } = new (); private bool _disposed; @@ -135,6 +142,11 @@ internal ExecuteSqlRequest BuildStatement(ExecuteSqlRequest.Types.QueryMode mode RequestOptions = RequestOptions, QueryMode = mode, }; + if (_transaction?.Tag != null) + { + RequestOptions.TransactionTag = _transaction?.Tag; + } + statement.ParamTypes.Add(paramTypes); if (SingleUseReadOnlyTransactionOptions != null) { @@ -220,12 +232,14 @@ private void ExecuteMutation() { Mutations = { BuildMutation() } }; + _transaction?.MarkUsed(); SpannerConnection.LibConnection!.WriteMutations(mutations); } private Rows Execute(ExecuteSqlRequest.Types.QueryMode mode = ExecuteSqlRequest.Types.QueryMode.Normal) { CheckCommandStateForExecution(); + _transaction?.MarkUsed(); return TranslateException(() => SpannerConnection.LibConnection!.Execute(BuildStatement(mode))); } @@ -241,6 +255,7 @@ private Task ExecuteAsync(ExecuteSqlRequest.Types.QueryMode mode, Cancella { return Task.FromCanceled(cancellationToken); } + _transaction?.MarkUsed(); return TranslateException(() => SpannerConnection.LibConnection!.ExecuteAsync(BuildStatement(mode))); } diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs index fb674919..605885b1 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs @@ -196,6 +196,11 @@ protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLeve }); } + /// + /// Starts a new read-only transaction with default options. + /// + /// The new transaction + /// If the connection has an active transaction public SpannerTransaction BeginReadOnlyTransaction() { return BeginTransaction(new TransactionOptions @@ -204,13 +209,27 @@ public SpannerTransaction BeginReadOnlyTransaction() }); } + /// + /// Starts a new read-only transaction using the given options. + /// + /// The options to use for the new read-only transaction + /// The new transaction + /// If the connection has an active transaction + public SpannerTransaction BeginReadOnlyTransaction(TransactionOptions.Types.ReadOnly readOnlyOptions) + { + return BeginTransaction(new TransactionOptions + { + ReadOnly = readOnlyOptions, + }); + } + /// /// Start a new transaction using the given TransactionOptions. /// /// The options to use for the new transaction /// The new transaction /// If the connection has an active transaction - public SpannerTransaction BeginTransaction(TransactionOptions transactionOptions) + private SpannerTransaction BeginTransaction(TransactionOptions transactionOptions) { EnsureOpen(); GaxPreconditions.CheckState(!HasTransaction, "This connection has a transaction."); diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs index 2264ed79..b99ed1af 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs @@ -36,10 +36,23 @@ protected override DbConnection? DbConnection } public override IsolationLevel IsolationLevel { get; } + private string? _tag; + public string? Tag + { + get => _tag; + set + { + GaxPreconditions.CheckState(!_used, "Tag cannot be changed once the transaction has been used"); + _tag = value; + } + } + // TODO: Implement savepoint support in the shared library. public override bool SupportsSavepoints => false; private SpannerLib.Connection LibConnection { get; } + + private bool _used; internal bool IsCompleted => _spannerConnection == null; @@ -85,6 +98,17 @@ private static IsolationLevel TranslateIsolationLevel(TransactionOptions.Types.I } } + internal void MarkUsed(bool initTag = false) + { + if (initTag && !_used && _tag != null) + { + // TODO: Add some option to the shared library to pass in tags for batches as an API option + var command = _spannerConnection?.CreateCommand($"set local transaction_tag = '{_tag}'"); + command?.ExecuteNonQuery(); + } + _used = true; + } + protected override void Dispose(bool disposing) { if (!IsCompleted) diff --git a/spannerlib/api/connection.go b/spannerlib/api/connection.go index 45340706..0dc2e0c8 100644 --- a/spannerlib/api/connection.go +++ b/spannerlib/api/connection.go @@ -430,8 +430,14 @@ func extractParams(directExecuteContext context.Context, statement *spannerpb.Ex if statement.Params != nil { paramsLen = 1 + len(statement.Params.Fields) } + requestTag := "" + transactionTag := "" + if statement.RequestOptions != nil { + requestTag = statement.RequestOptions.RequestTag + transactionTag = statement.RequestOptions.TransactionTag + } params := make([]any, paramsLen) - params = append(params, spannerdriver.ExecOptions{ + execOptions := spannerdriver.ExecOptions{ DecodeOption: spannerdriver.DecodeOptionProto, TimestampBound: extractTimestampBound(statement), ReturnResultSetMetadata: true, @@ -439,9 +445,14 @@ func extractParams(directExecuteContext context.Context, statement *spannerpb.Ex DirectExecuteQuery: true, DirectExecuteContext: directExecuteContext, QueryOptions: spanner.QueryOptions{ - Mode: &statement.QueryMode, + Mode: &statement.QueryMode, + RequestTag: requestTag, }, - }) + } + if transactionTag != "" { + execOptions.PropertyValues = []spannerdriver.PropertyValue{{Identifier: parser.Identifier{Parts: []string{"transaction_tag"}}, Value: transactionTag}} + } + params = append(params, execOptions) if statement.Params != nil { if statement.ParamTypes == nil { statement.ParamTypes = make(map[string]*spannerpb.Type) From 2ce49aa207b9673bdf92e1d586384e08672d5a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 27 Nov 2025 12:10:02 +0100 Subject: [PATCH 16/29] test: add query all data types test --- .../Snippets/TransactionSample.cs | 53 ++++++++++++++++++ .../insert_sample_data.sql | 3 +- .../DataReaderTests.cs | 1 + .../GetValueConversionTests.cs | 4 ++ .../spanner-ado-net-tests/BasicTests.cs | 55 +++++++++++++++++++ .../spanner-ado-net-tests/BatchTests.cs | 55 ++++++++++++++++--- .../spanner-ado-net/SpannerDataReader.cs | 39 ++++++++++++- .../RandomResultSetGenerator.cs | 22 +++++--- 8 files changed, 211 insertions(+), 21 deletions(-) create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/TransactionSample.cs diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/TransactionSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/TransactionSample.cs new file mode 100644 index 00000000..aac9970f --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/TransactionSample.cs @@ -0,0 +1,53 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Data; + +namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; + +/// +/// This sample shows how to execute a read/write transaction using the Spanner ADO.NET data provider. +/// +public class TransactionSample +{ + public static async Task Run(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // Start a read/write transaction by calling th standard BeginTransaction method. + await using var transaction = await connection.BeginTransactionAsync(IsolationLevel.RepeatableRead); + + // Execute a query that uses this transaction. + await using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.CommandText = "SELECT SingerId " + + "FROM Singers " + + "WHERE BirthDate IS NULL"; + var updateCount = 0; + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + // Update the birthdate of each Singer without a known birthdate to a default value. + await using var updateCommand = connection.CreateCommand(); + updateCommand.Transaction = transaction; + updateCommand.CommandText = "UPDATE Singers SET BirthDate=DATE '1900-01-01' WHERE SingerId=@singerId"; + updateCommand.Parameters.AddWithValue("singerId", reader["SingerId"]); + await updateCommand.ExecuteNonQueryAsync(); + updateCount++; + } + await transaction.CommitAsync(); + Console.WriteLine($"Set a default birthdate for {updateCount} singers"); + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/insert_sample_data.sql b/drivers/spanner-ado-net/spanner-ado-net-samples/insert_sample_data.sql index f10a6a4c..89a69457 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/insert_sample_data.sql +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/insert_sample_data.sql @@ -16,4 +16,5 @@ INSERT OR UPDATE INTO Singers (SingerId, FirstName, LastName, BirthDate, Picture) VALUES (1, 'Mark', 'Richards', DATE '1990-11-09', NULL), (2, 'Catalina', 'Smith', DATE '1998-04-29', NULL), - (3, 'Alice', 'Trentor', DATE '1979-10-15', NULL); + (3, 'Alice', 'Trentor', DATE '1979-10-15', NULL), + (4, 'Lea', 'Martin', NULL, NULL); diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DataReaderTests.cs b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DataReaderTests.cs index 028b0836..25f12c9b 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DataReaderTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DataReaderTests.cs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Data; using AdoNet.Specification.Tests; using Google.Cloud.SpannerLib.MockServer; using Xunit; diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/GetValueConversionTests.cs b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/GetValueConversionTests.cs index 70c195a5..7a1c5d78 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/GetValueConversionTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/GetValueConversionTests.cs @@ -115,6 +115,10 @@ public class GetValueConversionTests(DbFactoryFixture fixture) : GetValueConvers public override void GetString_throws_for_zero_Single() => TestGetValue(DbType.Single, ValueKind.Zero, x => x.GetString(0), "0"); + public override void GetString_throws_for_null_String_with_GetFieldValue() => TestGetValue(DbType.String, ValueKind.Null, x => x.GetFieldValue(0), null); + + public override async Task GetString_throws_for_null_String_with_GetFieldValueAsync() => await TestGetValueAsync(DbType.String, ValueKind.Null, async x => await x.GetFieldValueAsync(0), null); + public override void GetDouble_throws_for_one_String_with_GetFieldValue() => TestGetValue(DbType.String, ValueKind.One, x => x.GetFieldValue(0), 1.0d); public override async Task GetDouble_throws_for_one_String_with_GetFieldValueAsync() => await TestGetValueAsync(DbType.String, ValueKind.One, async x => await x.GetFieldValueAsync(0), 1.0d); diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/BasicTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/BasicTests.cs index 05142428..3406fa3c 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/BasicTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/BasicTests.cs @@ -142,6 +142,61 @@ public void TestInsertAllDataTypes() Assert.That(request.ParamTypes.Count, Is.EqualTo(0)); } + + [Test] + public void TestQueryAllDataTypes() + { + const string sql = "select * from all_types"; + var result = RandomResultSetGenerator.Generate(10, allowNull: true); + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateQuery(result)); + + using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + connection.Open(); + + using var cmd = connection.CreateCommand(); + cmd.CommandText = sql; + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + var index = 0; + foreach (var field in result.Metadata.RowType.Fields) + { + Assert.That(reader[index], Is.EqualTo(reader[field.Name])); + index++; + } + Assert.That(reader.GetFieldValue(reader.GetOrdinal("col_bool")), Is.EqualTo(ValueOrNull(reader["col_bool"]))); + Assert.That(reader.GetFieldValue(reader.GetOrdinal("col_bytes")), Is.EqualTo(ValueOrNull(reader["col_bytes"]))); + Assert.That(reader.GetFieldValue(reader.GetOrdinal("col_date")), Is.EqualTo(ValueOrNull(reader["col_date"]))); + Assert.That(reader.GetFieldValue(reader.GetOrdinal("col_float32")), Is.EqualTo(ValueOrNull(reader["col_float32"]))); + Assert.That(reader.GetFieldValue(reader.GetOrdinal("col_float64")), Is.EqualTo(ValueOrNull(reader["col_float64"]))); + Assert.That(reader.GetFieldValue(reader.GetOrdinal("col_int64")), Is.EqualTo(ValueOrNull(reader["col_int64"]))); + Assert.That(reader.GetFieldValue(reader.GetOrdinal("col_interval")), Is.EqualTo(ValueOrNull(reader["col_interval"]))); + Assert.That(reader.GetFieldValue(reader.GetOrdinal("col_json")), Is.EqualTo(ValueOrNull(reader["col_json"]))); + Assert.That(reader.GetFieldValue(reader.GetOrdinal("col_numeric")), Is.EqualTo(ValueOrNull(reader["col_numeric"]))); + Assert.That(reader.GetFieldValue(reader.GetOrdinal("col_string")), Is.EqualTo(ValueOrNull(reader["col_string"]))); + Assert.That(reader.GetFieldValue(reader.GetOrdinal("col_timestamp")), Is.EqualTo(ValueOrNull(reader["col_timestamp"]))); + Assert.That(reader.GetFieldValue(reader.GetOrdinal("col_uuid")), Is.EqualTo(ValueOrNull(reader["col_uuid"]))); + + Assert.That(reader.GetFieldValue>(reader.GetOrdinal("col_array_bool")), Is.EqualTo(ValueOrNull(reader["col_array_bool"]))); + Assert.That(reader.GetFieldValue>(reader.GetOrdinal("col_array_bytes")), Is.EqualTo(ValueOrNull(reader["col_array_bytes"]))); + Assert.That(reader.GetFieldValue>(reader.GetOrdinal("col_array_date")), Is.EqualTo(ValueOrNull(reader["col_array_date"]))); + Assert.That(reader.GetFieldValue>(reader.GetOrdinal("col_array_float32")), Is.EqualTo(ValueOrNull(reader["col_array_float32"]))); + Assert.That(reader.GetFieldValue>(reader.GetOrdinal("col_array_float64")), Is.EqualTo(ValueOrNull(reader["col_array_float64"]))); + Assert.That(reader.GetFieldValue>(reader.GetOrdinal("col_array_int64")), Is.EqualTo(ValueOrNull(reader["col_array_int64"]))); + Assert.That(reader.GetFieldValue>(reader.GetOrdinal("col_array_interval")), Is.EqualTo(ValueOrNull(reader["col_array_interval"]))); + Assert.That(reader.GetFieldValue>(reader.GetOrdinal("col_array_json")), Is.EqualTo(ValueOrNull(reader["col_array_json"]))); + Assert.That(reader.GetFieldValue>(reader.GetOrdinal("col_array_numeric")), Is.EqualTo(ValueOrNull(reader["col_array_numeric"]))); + Assert.That(reader.GetFieldValue>(reader.GetOrdinal("col_array_string")), Is.EqualTo(ValueOrNull(reader["col_array_string"]))); + Assert.That(reader.GetFieldValue>(reader.GetOrdinal("col_array_timestamp")), Is.EqualTo(ValueOrNull(reader["col_array_timestamp"]))); + Assert.That(reader.GetFieldValue>(reader.GetOrdinal("col_array_uuid")), Is.EqualTo(ValueOrNull(reader["col_array_uuid"]))); + } + } + + private static object? ValueOrNull(object value) + { + return value is DBNull ? null : value; + } [Test] public void TestExecuteDdl() diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/BatchTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/BatchTests.cs index 6590020c..772d495c 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/BatchTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/BatchTests.cs @@ -22,22 +22,47 @@ namespace Google.Cloud.Spanner.DataProvider.Tests; public class BatchTests : AbstractMockServerTests { - [TestCase(1, false)] - [TestCase(2, false)] - [TestCase(5, false)] - [TestCase(1, true)] - [TestCase(2, true)] - [TestCase(5, true)] - public async Task TestAllParameterTypes(int numCommands, bool executeAsync) + [TestCase(1, false, false)] + [TestCase(2, false, false)] + [TestCase(5, false, false)] + [TestCase(1, true, false)] + [TestCase(2, true, false)] + [TestCase(5, true, false)] + [TestCase(1, false, true)] + [TestCase(2, false, true)] + [TestCase(5, false, true)] + [TestCase(1, true, true)] + [TestCase(2, true, true)] + [TestCase(5, true, true)] + public async Task TestAllParameterTypes(int numCommands, bool executeAsync, bool useTransaction) { await using var connection = new SpannerConnection(); connection.ConnectionString = ConnectionString; await connection.OpenAsync(); - const string insert = "insert into my_table values (@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12)"; + SpannerTransaction? transaction = null; + if (useTransaction) + { + if (executeAsync) + { + transaction = await connection.BeginTransactionAsync(); + } + else + { + // ReSharper disable once MethodHasAsyncOverload + transaction = connection.BeginTransaction(); + } + } + + const string insert = "insert into my_table values " + + "(@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12, @p13, @p14, @p15, @p16, @p17, @p18, @p19, @p20, @p21, @p22, @p23)"; Fixture.SpannerMock.AddOrUpdateStatementResult(insert, StatementResult.CreateUpdateCount(1)); await using var batch = connection.CreateBatch(); + if (transaction != null) + { + batch.Transaction = transaction; + } for (var i = 0; i < numCommands; i++) { @@ -90,6 +115,18 @@ public async Task TestAllParameterTypes(int numCommands, bool executeAsync) { Assert.That(command.RecordsAffected, Is.EqualTo(1)); } + if (transaction != null) + { + if (executeAsync) + { + await transaction.CommitAsync(); + } + else + { + // ReSharper disable once MethodHasAsyncOverload + transaction.Commit(); + } + } var requests = Fixture.SpannerMock.Requests.ToList(); Assert.That(requests.OfType().Count, Is.EqualTo(1)); @@ -196,6 +233,8 @@ public async Task TestAllParameterTypes(int numCommands, bool executeAsync) Assert.That(fields["p23"].ListValue.Values[0].NumberValue, Is.EqualTo(3.14f)); Assert.That(fields["p23"].ListValue.Values[1].HasNullValue, Is.True); } + var commitRequest = requests.OfType().First(); + Assert.That(commitRequest, Is.Not.Null); } [TestCase(true)] diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs index bd69d370..e7517699 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs @@ -19,6 +19,7 @@ using System.Data.Common; using System.Globalization; using System.IO; +using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -528,11 +529,21 @@ public override System.Type GetFieldType(int ordinal) return GetClrType(Metadata!.RowType.Fields[ordinal].Type); } + private static System.Type GetNullableClrType(Google.Cloud.Spanner.V1.Type type) + { + var clr = GetClrType(type); + if (clr.IsValueType) + { + return typeof(Nullable<>).MakeGenericType(clr); + } + return clr; + } + private static System.Type GetClrType(Google.Cloud.Spanner.V1.Type type) { return type.Code switch { - TypeCode.Array => typeof(List<>).MakeGenericType(GetClrType(type.ArrayElementType)), + TypeCode.Array => typeof(List<>).MakeGenericType(GetNullableClrType(type.ArrayElementType)), TypeCode.Bool => typeof(bool), TypeCode.Bytes => typeof(byte[]), TypeCode.Date => typeof(DateOnly), @@ -708,6 +719,10 @@ public override T GetFieldValue(int ordinal) CheckNotClosed(); CheckValidPosition(); CheckValidOrdinal(ordinal); + if (typeof(T) == typeof(object)) + { + return base.GetFieldValue(ordinal); + } if (typeof(T) == typeof(Stream)) { CheckNotNull(ordinal); @@ -775,6 +790,15 @@ public override T GetFieldValue(int ordinal) return (T)(object)GetInt64(ordinal); } + if (IsDBNull(ordinal) && default(T) == null) + { + if (typeof(T) == typeof(DBNull)) + { + return (T)(object)DBNull.Value; + } + return (T)(object)null; + } + return base.GetFieldValue(ordinal); } @@ -797,11 +821,20 @@ private static object GetUnderlyingValue(Google.Cloud.Spanner.V1.Type type, Valu switch (type.Code) { case TypeCode.Array: - var listType = typeof(List<>).MakeGenericType(GetClrType(type.ArrayElementType)); + var listType = GetClrType(type); var list = (IList)Activator.CreateInstance(listType); + if (list == null) + { + throw new InvalidOperationException($"Failed to create instance of type {listType}."); + } foreach (var element in value.ListValue.Values) { - list.Add(GetUnderlyingValue(type.ArrayElementType, element)); + var underlyingValue = GetUnderlyingValue(type.ArrayElementType, element); + if (underlyingValue is DBNull) + { + underlyingValue = null; + } + list.Add(underlyingValue); } return list; case TypeCode.Bool: diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/RandomResultSetGenerator.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/RandomResultSetGenerator.cs index 63365b51..0287bcae 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/RandomResultSetGenerator.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/RandomResultSetGenerator.cs @@ -57,12 +57,12 @@ public static StructType GenerateAllTypesRowType() }; } - public static ResultSet Generate(int numRows) + public static ResultSet Generate(int numRows, bool allowNull = false) { - return Generate(GenerateAllTypesRowType(), numRows); + return Generate(GenerateAllTypesRowType(), numRows, allowNull); } - public static ResultSet Generate(StructType rowType, int numRows) + public static ResultSet Generate(StructType rowType, int numRows, bool allowNull = false) { var result = new ResultSet { @@ -73,23 +73,27 @@ public static ResultSet Generate(StructType rowType, int numRows) }; for (var i = 0; i < numRows; i++) { - result.Rows.Add(GenerateRow(rowType)); + result.Rows.Add(GenerateRow(rowType, allowNull)); } return result; } - private static ListValue GenerateRow(StructType rowType) + private static ListValue GenerateRow(StructType rowType, bool allowNull) { var row = new ListValue(); foreach (var field in rowType.Fields) { - row.Values.Add(GenerateValue(field.Type)); + row.Values.Add(GenerateValue(field.Type, allowNull)); } return row; } - private static Value GenerateValue(Spanner.V1.Type type) + private static Value GenerateValue(Spanner.V1.Type type, bool allowNull) { + if (allowNull && Random.Shared.Next(10) == 5) + { + return Value.ForNull(); + } switch (type.Code) { case TypeCode.Bool: @@ -106,7 +110,7 @@ private static Value GenerateValue(Spanner.V1.Type type) case TypeCode.Float64: return Value.ForNumber(Random.Shared.NextDouble() * double.MaxValue); case TypeCode.Int64: - return Value.ForNumber(Random.Shared.NextInt64()); + return Value.ForString(Random.Shared.NextInt64().ToString()); case TypeCode.Interval: var timespan = TimeSpan.FromTicks(Random.Shared.NextInt64()); return Value.ForString(XmlConvert.ToString(timespan)); @@ -129,7 +133,7 @@ private static Value GenerateValue(Spanner.V1.Type type) var values = new Value[length]; for (var i = 0; i < length; i++) { - values[i] = GenerateValue(type.ArrayElementType); + values[i] = GenerateValue(type.ArrayElementType, allowNull); } return Value.ForList(values); default: From b2ac6823916ae5569d0e65654b85402fadd4d37b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 27 Nov 2025 15:51:34 +0100 Subject: [PATCH 17/29] test: add more tests --- .../spanner-ado-net-tests/CommandTests.cs | 22 +-- .../spanner-ado-net-tests/TagTests.cs | 31 ++++- .../spanner-ado-net-tests/TransactionTests.cs | 130 +++++++++++++----- .../spanner-ado-net/SpannerBatch.cs | 43 ++++-- .../spanner-ado-net/SpannerCommand.cs | 68 ++++++--- .../spanner-ado-net/SpannerConnection.cs | 48 ++++++- .../spanner-ado-net/SpannerTransaction.cs | 13 +- spannerlib/grpc-server/server_test.go | 91 ++++++------ .../spannerlib-dotnet/Connection.cs | 10 +- transaction_test.go | 54 +++++++- 10 files changed, 362 insertions(+), 148 deletions(-) diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs index 7bee948b..c8cf14f3 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs @@ -286,13 +286,13 @@ public async Task Timeout() Fixture.SpannerMock.AddOrUpdateExecutionTime(nameof(Fixture.SpannerMock.ExecuteStreamingSql), ExecutionTime.FromMillis(10, 0)); await using var dataSource = CreateDataSource(csb => csb.CommandTimeout = 1); - await using var conn = await dataSource.OpenConnectionAsync() as SpannerConnection; - await using var cmd = new SpannerCommand("SELECT 1", conn!); + await using var conn = await dataSource.OpenConnectionAsync(); + await using var cmd = new SpannerCommand("SELECT 1", conn); Assert.That(() => cmd.ExecuteScalar(), Throws.Exception .TypeOf() .With.InnerException.TypeOf() ); - Assert.That(conn!.State, Is.EqualTo(ConnectionState.Open)); + Assert.That(conn.State, Is.EqualTo(ConnectionState.Open)); } [Test] @@ -302,13 +302,13 @@ public async Task TimeoutAsync() Fixture.SpannerMock.AddOrUpdateExecutionTime(nameof(Fixture.SpannerMock.ExecuteStreamingSql), ExecutionTime.FromMillis(10, 0)); await using var dataSource = CreateDataSource(csb => csb.CommandTimeout = 1); - await using var conn = await dataSource.OpenConnectionAsync() as SpannerConnection; - await using var cmd = new SpannerCommand("SELECT 1", conn!); + await using var conn = await dataSource.OpenConnectionAsync(); + await using var cmd = new SpannerCommand("SELECT 1", conn); Assert.That(async () => await cmd.ExecuteScalarAsync(), Throws.Exception .TypeOf() .With.InnerException.TypeOf()); - Assert.That(conn!.State, Is.EqualTo(ConnectionState.Open)); + Assert.That(conn.State, Is.EqualTo(ConnectionState.Open)); } [Test] @@ -379,7 +379,7 @@ public async Task CloseConnection() public async Task CloseDuringRead() { await using var dataSource = CreateDataSource(); - await using var conn = (await dataSource.OpenConnectionAsync() as SpannerConnection)!; + await using var conn = await dataSource.OpenConnectionAsync(); await using (var cmd = new SpannerCommand("SELECT 1", conn)) await using (var reader = await cmd.ExecuteReaderAsync()) { @@ -608,8 +608,8 @@ public async Task InvalidUtf8() new List([["abc��d"]]))); await using var dataSource = CreateDataSource(); - await using var conn = await dataSource.OpenConnectionAsync() as SpannerConnection; - var value = await conn!.ExecuteScalarAsync(sql); + await using var conn = await dataSource.OpenConnectionAsync(); + var value = await conn.ExecuteScalarAsync(sql); Assert.That(value, Is.EqualTo("abc��d")); } @@ -648,11 +648,11 @@ public void ConnectionNotSetThrows() } [Test] - public void ConnectionNotOpenTrows() + public void ConnectionNotOpen_OpensConnection() { using var conn = new SpannerConnection(ConnectionString); var cmd = new SpannerCommand("SELECT 1", conn); - Assert.That(() => cmd.ExecuteScalarAsync(), Throws.Exception.TypeOf()); + Assert.That(() => cmd.ExecuteScalarAsync(), Throws.Nothing); } [Test] diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/TagTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/TagTests.cs index e0bb4ae8..1eabb1dc 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/TagTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/TagTests.cs @@ -166,7 +166,7 @@ public async Task TestTransactionTagBatch([Values] bool async) } [Test] - public async Task TestMultipleStatements([Values] bool async) + public async Task TestMultipleStatements([Values] bool async, [Values] bool batchFirst) { const string dml = "insert into my_table (id, value) values (1, 'One')"; Fixture.SpannerMock.AddOrUpdateStatementResult(dml, StatementResult.CreateUpdateCount(1L)); @@ -179,6 +179,24 @@ public async Task TestMultipleStatements([Values] bool async) await using var transaction = await connection.BeginTransactionAsync(); transaction.Tag = "my_tx_tag"; + + if (batchFirst) + { + await using var firstBatch = connection.CreateBatch(); + firstBatch.Transaction = transaction; + firstBatch.Tag = "first_batch"; + firstBatch.BatchCommands.Add(dml); + firstBatch.BatchCommands.Add(dml); + if (async) + { + await firstBatch.ExecuteNonQueryAsync(); + } + else + { + // ReSharper disable once MethodHasAsyncOverload + firstBatch.ExecuteNonQuery(); + } + } await using var command = connection.CreateCommand(); command.Transaction = transaction; @@ -241,7 +259,16 @@ public async Task TestMultipleStatements([Values] bool async) } var requests = Fixture.SpannerMock.Requests.ToList(); - var batchRequest = requests.OfType().First(); + if (batchFirst) + { + var firstBatchRequest = requests.OfType().First(); + Assert.That(firstBatchRequest, Is.Not.Null); + Assert.That(firstBatchRequest.RequestOptions, Is.Not.Null); + Assert.That(firstBatchRequest.RequestOptions.RequestTag, Is.EqualTo("first_batch")); + Assert.That(firstBatchRequest.RequestOptions.TransactionTag, Is.EqualTo("my_tx_tag")); + } + + var batchRequest = requests.OfType().Last(); Assert.That(batchRequest, Is.Not.Null); Assert.That(batchRequest.RequestOptions, Is.Not.Null); Assert.That(batchRequest.RequestOptions.RequestTag, Is.EqualTo("my_batch")); diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/TransactionTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/TransactionTests.cs index fe5c9e51..9299ef6e 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/TransactionTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/TransactionTests.cs @@ -16,6 +16,7 @@ using System.Diagnostics.CodeAnalysis; using Google.Cloud.Spanner.V1; using Google.Cloud.SpannerLib.MockServer; +using Google.Protobuf.WellKnownTypes; using Grpc.Core; namespace Google.Cloud.Spanner.DataProvider.Tests; @@ -70,43 +71,71 @@ public async Task TestReadOnlyTransaction() await using var connection = new SpannerConnection(); connection.ConnectionString = ConnectionString; await connection.OpenAsync(); - await using var transaction = connection.BeginReadOnlyTransaction(); - await using var command = connection.CreateCommand(); - command.CommandText = sql; - var paramId = command.CreateParameter(); - paramId.ParameterName = "id"; - paramId.Value = 1; - command.Parameters.Add(paramId); - await using var reader = await command.ExecuteReaderAsync(); - Assert.That(await reader.ReadAsync()); - Assert.That(reader.FieldCount, Is.EqualTo(1)); - Assert.That(reader.GetValue(0), Is.EqualTo("One")); - Assert.That(await reader.ReadAsync(), Is.False); - - // We must commit the transaction in order to end it. - await transaction.CommitAsync(); - - var requests = Fixture.SpannerMock.Requests.ToList(); - // The transaction should use inline-begin. - Assert.That(requests.OfType().Count(), Is.EqualTo(0)); - Assert.That(requests.OfType().Count(), Is.EqualTo(1)); - // Committing a read-only transaction is a no-op on Spanner. - Assert.That(requests.OfType().Count(), Is.EqualTo(0)); - var executeRequest = requests.OfType().First(); - Assert.That(executeRequest.Transaction, Is.EqualTo(new TransactionSelector + + foreach (var options in new TransactionOptions.Types.ReadOnly?[] + { + null, + new() { Strong = true }, + new() { ExactStaleness = Duration.FromTimeSpan(TimeSpan.FromSeconds(25)) }, + new() { ReadTimestamp = Timestamp.FromDateTime(DateTime.UtcNow) }, + }) { - Begin = new TransactionOptions + await using var transaction = options == null + ? connection.BeginReadOnlyTransaction() + : connection.BeginReadOnlyTransaction(options); + await using var command = connection.CreateCommand(); + command.CommandText = sql; + var paramId = command.CreateParameter(); + paramId.ParameterName = "id"; + paramId.Value = 1; + command.Parameters.Add(paramId); + await using var reader = await command.ExecuteReaderAsync(); + Assert.That(await reader.ReadAsync()); + Assert.That(reader.FieldCount, Is.EqualTo(1)); + Assert.That(reader.GetValue(0), Is.EqualTo("One")); + Assert.That(await reader.ReadAsync(), Is.False); + + // We must commit the transaction in order to end it. + await transaction.CommitAsync(); + + var requests = Fixture.SpannerMock.Requests.ToList(); + Fixture.SpannerMock.ClearRequests(); + + // The transaction should use inline-begin. + Assert.That(requests.OfType().Count(), Is.EqualTo(0)); + Assert.That(requests.OfType().Count(), Is.EqualTo(1)); + // Committing a read-only transaction is a no-op on Spanner. + Assert.That(requests.OfType().Count(), Is.EqualTo(0)); + var executeRequest = requests.OfType().First(); + if (options == null) { - ReadOnly = new TransactionOptions.Types.ReadOnly + Assert.That(executeRequest.Transaction, Is.EqualTo(new TransactionSelector { - Strong = true, - ReturnReadTimestamp = true, - }, + Begin = new TransactionOptions + { + ReadOnly = new TransactionOptions.Types.ReadOnly + { + Strong = true, + ReturnReadTimestamp = true, + }, + } + })); } - })); + else + { + var expectedOptions = options; + expectedOptions.ReturnReadTimestamp = true; + Assert.That(executeRequest.Transaction, Is.EqualTo(new TransactionSelector + { + Begin = new TransactionOptions + { + ReadOnly = expectedOptions, + } + })); + } + } } - [Ignore("Needs a fix in SpannerLib")] [Test] public async Task TestTransactionTag() { @@ -118,11 +147,12 @@ public async Task TestTransactionTag() await using var connection = new SpannerConnection(); connection.ConnectionString = ConnectionString; await connection.OpenAsync(); + await using var setTagCommand = connection.CreateCommand(); setTagCommand.CommandText = "set transaction_tag='test_tag'"; await setTagCommand.ExecuteNonQueryAsync(); - await using var transaction = await connection.BeginTransactionAsync(); + await using var transaction = await connection.BeginTransactionAsync(); await using var command = connection.CreateCommand(); command.CommandText = select; var selectParamId = command.CreateParameter(); @@ -177,10 +207,42 @@ public async Task TestTransactionTag() await command2.ExecuteNonQueryAsync(); await tx2.CommitAsync(); + requests = Fixture.SpannerMock.Requests.ToList(); var lastRequest = requests.OfType().Last(request => request.Sql == update); - Assert.That(lastRequest.RequestOptions.TransactionTag, Is.Null); + Assert.That(lastRequest.RequestOptions.TransactionTag, Is.EqualTo("")); var lastCommitRequest = requests.OfType().Last(); - Assert.That(lastCommitRequest.RequestOptions.TransactionTag, Is.Null); + Assert.That(lastCommitRequest.RequestOptions.TransactionTag, Is.EqualTo("")); + } + + [Test] + public void TestChangeTransactionTagAfterStart() + { + const string update = "update my_table set my_column='test' where id=1"; + Fixture.SpannerMock.AddOrUpdateStatementResult(update, StatementResult.CreateUpdateCount(1L)); + + using var connection = new SpannerConnection(); + connection.ConnectionString = ConnectionString; + connection.Open(); + + using var transaction = connection.BeginTransaction(); + transaction.Tag = "first_tag"; + using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.CommandText = update; + + // We can still change the transaction tag as long as no statement has been executed. + transaction.Tag = "second_tag"; + // Execute a statement. From this point on the transaction tag can no longer be changed. + command.ExecuteNonQuery(); + + Assert.Throws(() => transaction.Tag = "third_tag"); + transaction.Commit(); + + var requests = Fixture.SpannerMock.Requests.ToList(); + var executeRequest = requests.OfType().Single(); + Assert.That(executeRequest.RequestOptions.TransactionTag, Is.EqualTo("second_tag")); + var commitRequest = requests.OfType().Single(); + Assert.That(commitRequest.RequestOptions.TransactionTag, Is.EqualTo("second_tag")); } [Test] diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs index ef038b10..3e9c8535 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs @@ -76,22 +76,41 @@ protected override Task ExecuteDbDataReaderAsync(CommandBehavior b return statements; } - private void SetRequestTag() + private SpannerCommand? CreateSetTagsCommandText() { - if (Tag != null) + if (!string.IsNullOrEmpty(Tag) || !string.IsNullOrEmpty(SpannerConnection.Transaction?.Tag)) { - var command = SpannerConnection.CreateCommand($"set statement_tag = '{Tag}'"); - command.ExecuteNonQuery(); + string commandText; + if (!string.IsNullOrEmpty(SpannerConnection.Transaction?.Tag) && string.IsNullOrEmpty(Tag)) + { + commandText = $"set local transaction_tag='{SpannerConnection.Transaction.Tag}'"; + } + else if (!string.IsNullOrEmpty(Tag) && string.IsNullOrEmpty(SpannerConnection.Transaction?.Tag)) + { + commandText = $"set statement_tag = '{Tag}'"; + } + else + { + commandText = $"set local transaction_tag='{SpannerConnection.Transaction!.Tag}';set statement_tag = '{Tag}'"; + } + return SpannerConnection.CreateCommand(commandText); } + return null; + } + + private void SetTags() + { + CreateSetTagsCommandText()?.ExecuteNonQuery(); } - private async Task SetRequestTagAsync(CancellationToken cancellationToken) + private Task SetRequestTagAsync(CancellationToken cancellationToken) { - if (Tag != null) + var command = CreateSetTagsCommandText(); + if (command != null) { - var command = SpannerConnection.CreateCommand($"set statement_tag = '{Tag}'"); - await command.ExecuteNonQueryAsync(cancellationToken); + return command.ExecuteNonQueryAsync(cancellationToken); } + return Task.CompletedTask; } public override int ExecuteNonQuery() @@ -101,9 +120,8 @@ public override int ExecuteNonQuery() return 0; } var statements = CreateStatements(); - SpannerTransaction?.MarkUsed(initTag: true); - SetRequestTag(); - var results = SpannerConnection.LibConnection!.ExecuteBatch(statements); + SetTags(); + var results = SpannerConnection.ExecuteBatch(statements); DbBatchCommands.SetAffected(results); return (int) results.Sum(); } @@ -115,9 +133,8 @@ public override async Task ExecuteNonQueryAsync(CancellationToken cancellat return 0; } var statements = CreateStatements(); - SpannerTransaction?.MarkUsed(initTag: true); await SetRequestTagAsync(cancellationToken); - var results = await SpannerConnection.LibConnection!.ExecuteBatchAsync(statements); + var results = await SpannerConnection.ExecuteBatchAsync(statements, cancellationToken); DbBatchCommands.SetAffected(results); return (int) results.Sum(); } diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs index c9f1a62c..9252c745 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs @@ -22,7 +22,6 @@ using Google.Cloud.Spanner.V1; using Google.Cloud.SpannerLib; using Google.Protobuf.WellKnownTypes; -using static Google.Cloud.Spanner.DataProvider.SpannerDbException; namespace Google.Cloud.Spanner.DataProvider; @@ -37,7 +36,7 @@ public class SpannerCommand : DbCommand, ICloneable public override int CommandTimeout { - get => _timeout ?? (int?) SpannerConnection?.DefaultCommandTimeout ?? 0; + get => _timeout ?? (int?) (Connection as SpannerConnection)?.DefaultCommandTimeout ?? 0; set => _timeout = value; } @@ -225,22 +224,29 @@ private Mutation BuildMutation() return mutation; } - private void ExecuteMutation() + private BatchWriteRequest.Types.MutationGroup CreateMutationGroup() { GaxPreconditions.CheckState(_mutation != null, "Cannot execute mutation"); - var mutations = new BatchWriteRequest.Types.MutationGroup + return new BatchWriteRequest.Types.MutationGroup { Mutations = { BuildMutation() } }; - _transaction?.MarkUsed(); - SpannerConnection.LibConnection!.WriteMutations(mutations); + } + + private void ExecuteMutation() + { + SpannerConnection.WriteMutations(CreateMutationGroup()); + } + + private Task ExecuteMutationAsync(CancellationToken cancellationToken) + { + return SpannerConnection.WriteMutationsAsync(CreateMutationGroup(), cancellationToken); } private Rows Execute(ExecuteSqlRequest.Types.QueryMode mode = ExecuteSqlRequest.Types.QueryMode.Normal) { CheckCommandStateForExecution(); - _transaction?.MarkUsed(); - return TranslateException(() => SpannerConnection.LibConnection!.Execute(BuildStatement(mode))); + return SpannerConnection.Execute(BuildStatement(mode)); } private Task ExecuteAsync(CancellationToken cancellationToken) @@ -255,8 +261,7 @@ private Task ExecuteAsync(ExecuteSqlRequest.Types.QueryMode mode, Cancella { return Task.FromCanceled(cancellationToken); } - _transaction?.MarkUsed(); - return TranslateException(() => SpannerConnection.LibConnection!.ExecuteAsync(BuildStatement(mode))); + return SpannerConnection.ExecuteAsync(BuildStatement(mode), cancellationToken); } private void CheckCommandStateForExecution() @@ -276,22 +281,28 @@ public override int ExecuteNonQuery() return 1; } - var rows = Execute(); - try - { - return (int)rows.UpdateCount; - } - finally + using var rows = Execute(); + return (int)rows.UpdateCount; + } + + public override async Task ExecuteNonQueryAsync(CancellationToken cancellationToken) + { + CheckDisposed(); + if (_mutation != null) { - rows.Close(); + await ExecuteMutationAsync(cancellationToken); + return 1; } - } + await using var rows = await ExecuteAsync(cancellationToken); + return (int)rows.UpdateCount; + } + public override object? ExecuteScalar() { CheckDisposed(); GaxPreconditions.CheckState(_mutation == null, "Cannot execute mutations with ExecuteScalar()"); - var rows = Execute(); + using var rows = Execute(); using var reader = new SpannerDataReader(SpannerConnection, rows, CommandBehavior.Default); if (reader.Read()) { @@ -300,7 +311,22 @@ public override int ExecuteNonQuery() return reader.GetValue(0); } } + return null; + } + public override async Task ExecuteScalarAsync(CancellationToken cancellationToken) + { + CheckDisposed(); + GaxPreconditions.CheckState(_mutation == null, "Cannot execute mutations with ExecuteScalarAsync()"); + await using var rows = await ExecuteAsync(cancellationToken); + await using var reader = new SpannerDataReader(SpannerConnection, rows, CommandBehavior.Default); + if (await reader.ReadAsync(cancellationToken)) + { + if (reader.FieldCount > 0) + { + return reader.GetValue(0); + } + } return null; } @@ -369,7 +395,7 @@ protected override async Task ExecuteDbDataReaderAsync(CommandBeha { if (behavior.HasFlag(CommandBehavior.CloseConnection)) { - SpannerConnection.Close(); + await SpannerConnection.CloseAsync(); } throw new SpannerDbException(exception); } @@ -396,7 +422,7 @@ public virtual SpannerCommand Clone() CommandType = CommandType, DesignTimeVisible = DesignTimeVisible, }; - (DbParameterCollection as SpannerParameterCollection)?.CloneTo((clone.Parameters as SpannerParameterCollection)!); + (DbParameterCollection as SpannerParameterCollection)?.CloneTo(clone.Parameters); return clone; } diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs index 605885b1..bf181bc5 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs @@ -118,12 +118,12 @@ public override string ServerVersion private Connection? _libConnection; - internal Connection? LibConnection + private Connection LibConnection { get { AssertOpen(); - return _libConnection; + return _libConnection!; } } @@ -131,6 +131,8 @@ internal Connection? LibConnection private SpannerTransaction? _transaction; + internal SpannerTransaction? Transaction => _transaction; + private System.Transactions.Transaction? EnlistedTransaction { get; set; } private SpannerSchemaProvider? _mSchemaProvider; @@ -233,7 +235,7 @@ private SpannerTransaction BeginTransaction(TransactionOptions transactionOption { EnsureOpen(); GaxPreconditions.CheckState(!HasTransaction, "This connection has a transaction."); - _transaction = new SpannerTransaction(this, transactionOptions); + _transaction = new SpannerTransaction(this, LibConnection, transactionOptions); return _transaction; } @@ -337,7 +339,7 @@ private void EnsureOpen() private void AssertOpen() { - if (InternalState != ConnectionState.Open) + if (InternalState != ConnectionState.Open || _libConnection == null) { throw new InvalidOperationException("Connection is not open"); } @@ -354,13 +356,15 @@ private void AssertClosed() public CommitResponse? WriteMutations(BatchWriteRequest.Types.MutationGroup mutations) { EnsureOpen(); - return TranslateException(() => LibConnection!.WriteMutations(mutations)); + _transaction?.MarkUsed(); + return LibConnection.WriteMutations(mutations); } public Task WriteMutationsAsync(BatchWriteRequest.Types.MutationGroup mutations, CancellationToken cancellationToken = default) { EnsureOpen(); - return LibConnection!.WriteMutationsAsync(mutations, cancellationToken); + _transaction?.MarkUsed(); + return LibConnection.WriteMutationsAsync(mutations, cancellationToken); } /// @@ -383,6 +387,20 @@ protected override DbCommand CreateDbCommand() return cmd; } + public Rows Execute(ExecuteSqlRequest statement) + { + EnsureOpen(); + _transaction?.MarkUsed(); + return TranslateException(() => LibConnection.Execute(statement)); + } + + public Task ExecuteAsync(ExecuteSqlRequest statement, CancellationToken cancellationToken = default) + { + EnsureOpen(); + _transaction?.MarkUsed(); + return TranslateException(LibConnection.ExecuteAsync(statement, cancellationToken)); + } + public new SpannerBatch CreateBatch() => (SpannerBatch) base.CreateBatch(); protected override DbBatch CreateDbBatch() @@ -390,6 +408,21 @@ protected override DbBatch CreateDbBatch() return new SpannerBatch(this); } + public long[] ExecuteBatch(IEnumerable statements) + { + EnsureOpen(); + _transaction?.MarkUsed(); + return TranslateException(() => LibConnection.ExecuteBatch(statements)); + } + + public Task ExecuteBatchAsync(List statements, + CancellationToken cancellationToken = default) + { + EnsureOpen(); + _transaction?.MarkUsed(); + return TranslateException(LibConnection.ExecuteBatchAsync(statements, cancellationToken)); + } + public long[] ExecuteBatchDml(List commands) { EnsureOpen(); @@ -408,7 +441,8 @@ public long[] ExecuteBatchDml(List commands) statements.Add(batchStatement); } } - return TranslateException(() => LibConnection!.ExecuteBatch(statements)); + _transaction?.MarkUsed(); + return TranslateException(() => LibConnection.ExecuteBatch(statements)); } /// diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs index b99ed1af..cc6c71b3 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs @@ -60,11 +60,12 @@ public string? Tag private bool _disposed; - internal SpannerTransaction(SpannerConnection connection, TransactionOptions options) + internal SpannerTransaction(SpannerConnection connection, SpannerLib.Connection libConnection, TransactionOptions options) { _spannerConnection = connection; IsolationLevel = TranslateIsolationLevel(options.IsolationLevel); - LibConnection = connection.LibConnection!; + LibConnection = libConnection; + // This call to BeginTransaction does not trigger an RPC. It only registers the transaction on the connection. LibConnection.BeginTransaction(options); } @@ -98,14 +99,8 @@ private static IsolationLevel TranslateIsolationLevel(TransactionOptions.Types.I } } - internal void MarkUsed(bool initTag = false) + internal void MarkUsed() { - if (initTag && !_used && _tag != null) - { - // TODO: Add some option to the shared library to pass in tags for batches as an API option - var command = _spannerConnection?.CreateCommand($"set local transaction_tag = '{_tag}'"); - command?.ExecuteNonQuery(); - } _used = true; } diff --git a/spannerlib/grpc-server/server_test.go b/spannerlib/grpc-server/server_test.go index b541946e..a5741398 100644 --- a/spannerlib/grpc-server/server_test.go +++ b/spannerlib/grpc-server/server_test.go @@ -622,53 +622,60 @@ func TestTransaction(t *testing.T) { }); err != nil { t.Fatalf("failed to set transaction_tag: %v", err) } - if _, err := client.BeginTransaction(ctx, &pb.BeginTransactionRequest{ - Connection: connection, - TransactionOptions: &sppb.TransactionOptions{}, - }); err != nil { - t.Fatalf("failed to begin transaction: %v", err) - } - rows, err := client.Execute(ctx, &pb.ExecuteRequest{ - Connection: connection, - ExecuteSqlRequest: &sppb.ExecuteSqlRequest{Sql: testutil.UpdateBarSetFoo}, - }) - if err != nil { - t.Fatalf("failed to execute: %v", err) - } - row, err := client.Next(ctx, &pb.NextRequest{Rows: rows, NumRows: 1}) - if err != nil { - t.Fatalf("failed to fetch next row: %v", err) - } - if row.Values != nil { - t.Fatalf("row values should be nil: %v", row.Values) - } - stats, err := client.ResultSetStats(ctx, rows) - if err != nil { - t.Fatalf("failed to get stats: %v", err) - } - if g, w := stats.GetRowCountExact(), int64(testutil.UpdateBarSetFooRowCount); g != w { - t.Fatalf("row count mismatch\n Got: %v\nWant: %v", g, w) - } - if _, err := client.CloseRows(ctx, rows); err != nil { - t.Fatalf("failed to close rows: %v", err) - } - if _, err := client.Commit(ctx, connection); err != nil { - t.Fatalf("failed to commit: %v", err) + + for i := 0; i < 2; i++ { + if _, err := client.BeginTransaction(ctx, &pb.BeginTransactionRequest{ + Connection: connection, + TransactionOptions: &sppb.TransactionOptions{}, + }); err != nil { + t.Fatalf("failed to begin transaction: %v", err) + } + rows, err := client.Execute(ctx, &pb.ExecuteRequest{ + Connection: connection, + ExecuteSqlRequest: &sppb.ExecuteSqlRequest{Sql: testutil.UpdateBarSetFoo}, + }) + if err != nil { + t.Fatalf("failed to execute: %v", err) + } + row, err := client.Next(ctx, &pb.NextRequest{Rows: rows, NumRows: 1}) + if err != nil { + t.Fatalf("failed to fetch next row: %v", err) + } + if row.Values != nil { + t.Fatalf("row values should be nil: %v", row.Values) + } + stats, err := client.ResultSetStats(ctx, rows) + if err != nil { + t.Fatalf("failed to get stats: %v", err) + } + if g, w := stats.GetRowCountExact(), int64(testutil.UpdateBarSetFooRowCount); g != w { + t.Fatalf("row count mismatch\n Got: %v\nWant: %v", g, w) + } + if _, err := client.CloseRows(ctx, rows); err != nil { + t.Fatalf("failed to close rows: %v", err) + } + if _, err := client.Commit(ctx, connection); err != nil { + t.Fatalf("failed to commit: %v", err) + } + + requests := server.TestSpanner.DrainRequestsFromServer() + executeRequests := testutil.RequestsOfType(requests, reflect.TypeOf(&sppb.ExecuteSqlRequest{})) + if g, w := len(executeRequests), 1; g != w { + t.Fatalf("num execute requests mismatch\n Got: %v\nWant: %v", g, w) + } + request := executeRequests[0].(*sppb.ExecuteSqlRequest) + expectedTag := "test_tag" + if i == 1 { + expectedTag = "" + } + if g, w := request.RequestOptions.TransactionTag, expectedTag; g != w { + t.Fatalf("transaction tag mismatch\n Got: %v\nWant: %v", g, w) + } } if _, err := client.ClosePool(ctx, pool); err != nil { t.Fatalf("failed to close pool: %v", err) } - - requests := server.TestSpanner.DrainRequestsFromServer() - executeRequests := testutil.RequestsOfType(requests, reflect.TypeOf(&sppb.ExecuteSqlRequest{})) - if g, w := len(executeRequests), 1; g != w { - t.Fatalf("num execute requests mismatch\n Got: %v\nWant: %v", g, w) - } - request := executeRequests[0].(*sppb.ExecuteSqlRequest) - if g, w := request.RequestOptions.TransactionTag, "test_tag"; g != w { - t.Fatalf("transaction tag mismatch\n Got: %v\nWant: %v", g, w) - } } func TestRollback(t *testing.T) { diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/Connection.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/Connection.cs index b21f36f0..b7d31503 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/Connection.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/Connection.cs @@ -123,10 +123,11 @@ public Rows Execute(ExecuteSqlRequest statement) /// connection. The contents of the returned Rows object depends on the type of SQL statement. /// /// The SQL statement that should be executed + /// The cancellation token /// A Rows object with the statement result - public Task ExecuteAsync(ExecuteSqlRequest statement) + public Task ExecuteAsync(ExecuteSqlRequest statement, CancellationToken cancellationToken = default) { - return Spanner.ExecuteAsync(this, statement); + return Spanner.ExecuteAsync(this, statement, cancellationToken); } /// @@ -151,14 +152,15 @@ public long[] ExecuteBatch(IEnumerable s /// transaction is not supported. /// /// The DML or DDL statements to execute + /// The cancellation token /// The update count per statement. The update count for a DDL statement is -1. - public Task ExecuteBatchAsync(List statements) + public Task ExecuteBatchAsync(List statements, CancellationToken cancellationToken = default) { var request = new ExecuteBatchDmlRequest { Statements = { statements } }; - return Spanner.ExecuteBatchAsync(this, request); + return Spanner.ExecuteBatchAsync(this, request, cancellationToken); } /// diff --git a/transaction_test.go b/transaction_test.go index ddd6d05a..6ac5447a 100644 --- a/transaction_test.go +++ b/transaction_test.go @@ -15,6 +15,51 @@ import ( "google.golang.org/grpc/status" ) +func TestSetTransactionTag(t *testing.T) { + t.Parallel() + + db, server, teardown := setupTestDBConnection(t) + defer teardown() + ctx := context.Background() + + c, err := db.Conn(ctx) + if err != nil { + t.Fatal(err) + } + defer silentClose(c) + if _, err := c.ExecContext(ctx, "set transaction_tag = 'foo'"); err != nil { + t.Fatal(err) + } + + for range 2 { + tx, err := c.BeginTx(ctx, &sql.TxOptions{}) + if err != nil { + t.Fatal(err) + } + if _, err = tx.ExecContext(ctx, testutil.UpdateBarSetFoo); err != nil { + t.Fatal(err) + } + if err := tx.Commit(); err != nil { + t.Fatal(err) + } + } + + requests := server.TestSpanner.DrainRequestsFromServer() + executeRequests := testutil.RequestsOfType(requests, reflect.TypeOf(&spannerpb.ExecuteSqlRequest{})) + if g, w := len(executeRequests), 2; g != w { + t.Fatalf("execute requests count mismatch\n Got: %v\nWant: %v", g, w) + } + request := executeRequests[0].(*spannerpb.ExecuteSqlRequest) + if g, w := request.RequestOptions.TransactionTag, "foo"; g != w { + t.Fatalf("transaction tag mismatch\n Got: %v\nWant: %v", g, w) + } + // The transaction_tag should not be sticky. + request = executeRequests[1].(*spannerpb.ExecuteSqlRequest) + if g, w := request.RequestOptions.TransactionTag, ""; g != w { + t.Fatalf("transaction tag mismatch\n Got: %v\nWant: %v", g, w) + } +} + func TestSetTransactionIsolationLevel(t *testing.T) { t.Parallel() @@ -69,11 +114,10 @@ func TestSetTransactionReadOnly(t *testing.T) { if request.GetTransaction() == nil || request.GetTransaction().GetBegin() == nil { t.Fatal("missing begin transaction on ExecuteSqlRequest") } - // TODO: Enable once transaction_read_only is picked up by the driver. - //readOnly := request.GetTransaction().GetBegin().GetReadOnly() - //if readOnly == nil { - // t.Fatal("missing readOnly on ExecuteSqlRequest") - //} + readOnly := request.GetTransaction().GetBegin().GetReadOnly() + if readOnly == nil { + t.Fatal("missing readOnly on ExecuteSqlRequest") + } } func TestSetTransactionDeferrable(t *testing.T) { From 846932042896503ed3909ac1c385a9b9d97501c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 27 Nov 2025 17:29:46 +0100 Subject: [PATCH 18/29] chore: add multi-statement SQL support --- .../CommandTests.cs | 5 -- .../DataReaderTests.cs | 19 -------- .../DbFactoryFixture.cs | 14 +++++- .../spanner-ado-net-tests/CommandTests.cs | 38 +++++++++------ .../spanner-ado-net/SpannerBatch.cs | 2 +- .../spanner-ado-net/SpannerCommand.cs | 8 ++-- .../spanner-ado-net/SpannerDataReader.cs | 13 ++++- .../spanner-ado-net/SpannerParameter.cs | 4 +- .../SpannerParameterCollection.cs | 4 +- .../MockSpannerServer.cs | 11 +++-- .../spannerlib-dotnet/Rows.cs | 47 +++++++++++++++++++ 11 files changed, 112 insertions(+), 53 deletions(-) diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/CommandTests.cs b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/CommandTests.cs index a2feb8ab..5bc8552b 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/CommandTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/CommandTests.cs @@ -85,11 +85,6 @@ public override void ExecuteScalar_returns_string_when_text() base.ExecuteScalar_returns_string_when_text(); } - [Fact(Skip = "Spanner does not support multiple SQL statements in one string")] - public override void ExecuteScalar_returns_first_when_batching() - { - } - [Fact(Skip = "Spanner does not support empty statements")] public override void ExecuteReader_HasRows_is_false_for_comment() { diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DataReaderTests.cs b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DataReaderTests.cs index 25f12c9b..96300bfc 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DataReaderTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DataReaderTests.cs @@ -22,25 +22,6 @@ namespace Google.Cloud.Spanner.DataProvider.SpecificationTests; public class DataReaderTests(DbFactoryFixture fixture) : DataReaderTestBase(fixture) { - [Fact(Skip = "SpannerLib does not support multiple statements in one query string")] - public override void HasRows_works_when_batching() - { - } - - [Fact(Skip = "SpannerLib does not support multiple statements in one query string")] - public override void NextResult_works() - { - } - - [Fact(Skip = "SpannerLib does not support multiple statements in one query string")] - public override void SingleResult_returns_one_result_set() - { - } - - [Fact(Skip = "SpannerLib does not support multiple statements in one query string")] - public override void SingleRow_returns_one_result_set() - { - } [Fact(Skip = "Getting stats after closing a DataReader is not supported")] public override void RecordsAffected_returns_negative_1_after_close_when_no_rows() diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DbFactoryFixture.cs b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DbFactoryFixture.cs index 6c4e39e2..18b4233c 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DbFactoryFixture.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/DbFactoryFixture.cs @@ -48,9 +48,9 @@ static DbFactoryFixture() DbType.Single, DbType.String, ]; - public string SelectNoRows => "select * from (select 1) where false"; + public string SelectNoRows => "select * from (select 1) where false;"; public System.Type NullValueExceptionType { get; } = typeof(InvalidCastException); - public string DeleteNoRows => "delete from foo where false"; + public string DeleteNoRows => "delete from foo where false;"; public DbFactoryFixture() { @@ -62,6 +62,8 @@ public void Reset() MockServerFixture.SpannerMock.Reset(); MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 1;", StatementResult.CreateSelect1ResultSet()); MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 1", StatementResult.CreateSelect1ResultSet()); + MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 2", StatementResult.CreateSelect2ResultSet()); + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(" SELECT 2", StatementResult.CreateSelect2ResultSet()); MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT NULL;", StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "c", DBNull.Value)); MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 1 AS id;", @@ -74,6 +76,14 @@ public void Reset() StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.String}, "c", "ab¢d")); MockServerFixture.SpannerMock.AddOrUpdateStatementResult(SelectNoRows, StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "c")); + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(SelectNoRows[..^1], + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "c")); + MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 42", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "c", 42)); + MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 43", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "c", 43)); + MockServerFixture.SpannerMock.AddOrUpdateStatementResult(" SELECT 43", + StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "c", 43)); MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 42 UNION SELECT 43;", StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = TypeCode.Int64}, "c", 42, 43)); MockServerFixture.SpannerMock.AddOrUpdateStatementResult("SELECT 1 UNION SELECT 2;", diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs index c8cf14f3..dc1630cc 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/CommandTests.cs @@ -203,12 +203,13 @@ public async Task TestExecuteNonQueryWithError() [TestCase(new[] { false, false }, TestName = "TwoNonQueries")] [TestCase(new[] { false, true }, TestName = "NonQueryQuery")] [TestCase(new[] { true, false }, TestName = "QueryNonQuery")] - [Ignore("Requires support for multi-statements strings in the shared library")] public async Task MultipleStatements(bool[] queries) { const string update = "UPDATE my_table SET name='yo' WHERE 1=0;"; const string select = "SELECT 1;"; Fixture.SpannerMock.AddOrUpdateStatementResult(update, StatementResult.CreateUpdateCount(0)); + Fixture.SpannerMock.AddOrUpdateStatementResult(update[..^1], StatementResult.CreateUpdateCount(0)); + Fixture.SpannerMock.AddOrUpdateStatementResult(select, StatementResult.CreateSelect1ResultSet()); await using var conn = await OpenConnectionAsync(); var sb = new StringBuilder(); @@ -226,20 +227,29 @@ public async Task MultipleStatements(bool[] queries) await cmd.PrepareAsync(); } await using var reader = await cmd.ExecuteReaderAsync(); - var numResultSets = queries.Count(q => q); + var numResultSets = queries.Length; for (var i = 0; i < numResultSets; i++) { - Assert.That(await reader.ReadAsync(), Is.True); - Assert.That(reader[0], Is.EqualTo(1)); + Assert.That(await reader.ReadAsync(), Is.EqualTo(queries[i])); + if (queries[i]) + { + Assert.That(reader[0], Is.EqualTo(1)); + } Assert.That(await reader.NextResultAsync(), Is.EqualTo(i != numResultSets - 1)); } } } [Test] - [Ignore("Requires support for multi-statements strings in the shared library")] public async Task MultipleStatementsWithParameters([Values(PrepareOrNot.NotPrepared, PrepareOrNot.Prepared)] PrepareOrNot prepare) { + Fixture.SpannerMock.AddOrUpdateStatementResult("SELECT @p1", StatementResult.CreateResultSet( + new List>([Tuple.Create(TypeCode.Int64, "p1")]), + new List([[8L]]))); + Fixture.SpannerMock.AddOrUpdateStatementResult(" SELECT @p2", StatementResult.CreateResultSet( + new List>([Tuple.Create(TypeCode.String, "p2")]), + new List([["foo"]]))); + await using var conn = await OpenConnectionAsync(); await using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT @p1; SELECT @p2"; @@ -263,7 +273,6 @@ public async Task MultipleStatementsWithParameters([Values(PrepareOrNot.NotPrepa } [Test] - [Ignore("Requires support for multi-statements strings in the shared library")] public async Task SingleRowMultipleStatements([Values(PrepareOrNot.NotPrepared, PrepareOrNot.Prepared)] PrepareOrNot prepare) { await using var conn = await OpenConnectionAsync(); @@ -489,9 +498,8 @@ public async Task ExecuteNonQuery() Assert.That(cmd.ExecuteNonQueryAsync, Is.EqualTo(1)); // Insert two rows in one batch using a SQL string that contains two statements. - // TODO: Enable when SpannerLib supports SQL strings with multiple statements. - // cmd.CommandText = $"{insertOneRow}; {insertOneRow}"; - // Assert.That(cmd.ExecuteNonQueryAsync, Is.EqualTo(2)); + cmd.CommandText = $"{insertOneRow}; {insertOneRow}"; + Assert.That(cmd.ExecuteNonQueryAsync, Is.EqualTo(2)); // Execute a large SQL string. var value = TestUtils.GenerateRandomString(10_000_000); @@ -746,23 +754,25 @@ public async Task ManyParameters([Values(PrepareOrNot.NotPrepared, PrepareOrNot. } [Test] - [Ignore("Requires multi-statement support in SpannerLib")] public async Task ManyParametersAcrossStatements() { var result = StatementResult.CreateSelect1ResultSet(); - // Create a command with 1000 statements which have 70 params each + // Create a command with 100 statements which have 7 params each + const int numStatements = 100; + const int numParamsPerStatement = 7; + await using var conn = await OpenConnectionAsync(); await using var cmd = new SpannerCommand(conn); var paramIndex = 0; var sb = new StringBuilder(); - for (var statementIndex = 0; statementIndex < 1000; statementIndex++) + for (var statementIndex = 0; statementIndex < numStatements; statementIndex++) { if (statementIndex > 0) - sb.Append("; "); + sb.Append(";"); var statement = new StringBuilder(); statement.Append("SELECT "); var startIndex = paramIndex; - var endIndex = paramIndex + 70; + var endIndex = paramIndex + numParamsPerStatement; for (; paramIndex < endIndex; paramIndex++) { var paramName = "p" + paramIndex; diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs index 3e9c8535..4659b75d 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs @@ -62,7 +62,7 @@ protected override Task ExecuteDbDataReaderAsync(CommandBehavior b var statements = new List(DbBatchCommands.Count); foreach (var command in DbBatchCommands) { - var spannerParams = ((SpannerParameterCollection)command.Parameters).CreateSpannerParams(); + var spannerParams = ((SpannerParameterCollection)command.Parameters).CreateSpannerParams(prepare: false); var queryParams = spannerParams.Item1; var paramTypes = spannerParams.Item2; var batchStatement = new ExecuteBatchDmlRequest.Types.Statement diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs index 9252c745..39004818 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs @@ -125,7 +125,7 @@ internal ExecuteSqlRequest BuildStatement(ExecuteSqlRequest.Types.QueryMode mode { GaxPreconditions.CheckState(!(HasTransaction && SingleUseReadOnlyTransactionOptions != null), "Cannot set both a transaction and single-use read-only options"); - var spannerParams = ((SpannerParameterCollection)DbParameterCollection).CreateSpannerParams(); + var spannerParams = Parameters.CreateSpannerParams(prepare: mode == ExecuteSqlRequest.Types.QueryMode.Plan); var queryParams = spannerParams.Item1; var paramTypes = spannerParams.Item2; var sql = CommandText; @@ -206,7 +206,7 @@ private Mutation BuildMutation() write.Columns.Add(name); } - values.Values.Add(spannerParameter.ConvertToProto(spannerParameter)); + values.Values.Add(spannerParameter.ConvertToProto(spannerParameter, prepare: false)); } else { @@ -282,7 +282,7 @@ public override int ExecuteNonQuery() } using var rows = Execute(); - return (int)rows.UpdateCount; + return (int)rows.GetTotalUpdateCount(); } public override async Task ExecuteNonQueryAsync(CancellationToken cancellationToken) @@ -295,7 +295,7 @@ public override async Task ExecuteNonQueryAsync(CancellationToken cancellat } await using var rows = await ExecuteAsync(cancellationToken); - return (int)rows.UpdateCount; + return (int) await rows.GetTotalUpdateCountAsync(cancellationToken); } public override object? ExecuteScalar() diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs index e7517699..f39084b4 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs @@ -37,6 +37,7 @@ public class SpannerDataReader : DbDataReader private readonly SpannerConnection _connection; private readonly CommandBehavior _commandBehavior; private bool IsSingleRow => _commandBehavior.HasFlag(CommandBehavior.SingleRow); + private bool IsSingleResult => _commandBehavior.HasFlag(CommandBehavior.SingleResult); private Rows LibRows { get; } private bool _closed; private bool _hasReadData; @@ -917,7 +918,17 @@ public override bool IsDBNull(int ordinal) public override bool NextResult() { CheckNotClosed(); - return false; + if (IsSingleRow || IsSingleResult) + { + return false; + } + _currentRow = null; + _tempRow = null; + _hasData = false; + _hasReadData = false; + _metadata = null; + _stats = null; + return LibRows.NextResultSet(); } public override IEnumerator GetEnumerator() diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerParameter.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerParameter.cs index 9b6187d8..f29508fc 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerParameter.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerParameter.cs @@ -94,9 +94,9 @@ public override void ResetDbType() _dbType = null; } - internal Value ConvertToProto(DbParameter dbParameter) + internal Value ConvertToProto(DbParameter dbParameter, bool prepare) { - GaxPreconditions.CheckState(dbParameter.Direction != ParameterDirection.Input || Value != null, $"Parameter {ParameterName} has no value"); + GaxPreconditions.CheckState(prepare || dbParameter.Direction != ParameterDirection.Input || Value != null, $"Parameter {ParameterName} has no value"); return ConvertToProto(Value); } diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerParameterCollection.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerParameterCollection.cs index 5f17a151..eb63077a 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerParameterCollection.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerParameterCollection.cs @@ -228,7 +228,7 @@ public override void AddRange(Array values) } } - internal Tuple> CreateSpannerParams() + internal Tuple> CreateSpannerParams(bool prepare) { var queryParams = new Struct(); var paramTypes = new MapField(); @@ -250,7 +250,7 @@ public override void AddRange(Array values) { name = "p" + (index + 1); } - queryParams.Fields.Add(name, spannerParameter.ConvertToProto(spannerParameter)); + queryParams.Fields.Add(name, spannerParameter.ConvertToProto(spannerParameter, prepare)); var paramType = spannerParameter.GetSpannerType(); if (paramType != null) { diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/MockSpannerServer.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/MockSpannerServer.cs index 40d9fe71..f125ce5c 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/MockSpannerServer.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/MockSpannerServer.cs @@ -58,15 +58,20 @@ public static StatementResult CreateException(Exception exception) { return new StatementResult(exception); } + + public static StatementResult CreateSelectZeroResultSet() + { + return CreateSingleColumnResultSet(new Spanner.V1.Type { Code = Spanner.V1.TypeCode.Int64 }, "COL1", 0); + } public static StatementResult CreateSelect1ResultSet() { return CreateSingleColumnResultSet(new Spanner.V1.Type { Code = Spanner.V1.TypeCode.Int64 }, "COL1", 1); } - - public static StatementResult CreateSelectZeroResultSet() + + public static StatementResult CreateSelect2ResultSet() { - return CreateSingleColumnResultSet(new Spanner.V1.Type { Code = Spanner.V1.TypeCode.Int64 }, "COL1", 0); + return CreateSingleColumnResultSet(new Spanner.V1.Type { Code = Spanner.V1.TypeCode.Int64 }, "COL1", 2); } public static StatementResult CreateSingleColumnResultSet(Spanner.V1.Type type, string col, params object[] values) diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/Rows.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/Rows.cs index 41569e82..0655a447 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/Rows.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/Rows.cs @@ -63,6 +63,8 @@ public long UpdateCount } } + private bool _hasReadAllResults; + public Rows(Connection connection, long id, bool initMetadata = true) : base(connection.Spanner, id) { SpannerConnection = connection; @@ -97,12 +99,52 @@ public Rows(Connection connection, long id, bool initMetadata = true) : base(con return await Spanner.NextAsync(this, 1, ISpannerLib.RowEncoding.Proto, cancellationToken); } + /// + /// Gets the total update count in this Rows object. This consumes all data in all the result sets. + /// This method should only be called if the caller is only interested in the update count, and not in any of the + /// rows in the result sets. + /// + /// + /// The total update count of all the result sets in this Rows object. + /// + public long GetTotalUpdateCount() + { + var result = UpdateCount; + while (NextResultSet()) + { + result += UpdateCount; + } + return result; + } + + /// + /// Gets the total update count in this Rows object. This consumes all data in all the result sets. + /// This method should only be called if the caller is only interested in the update count, and not in any of the + /// rows in the result sets. + /// + /// + /// The total update count of all the result sets in this Rows object. + /// + public async Task GetTotalUpdateCountAsync(CancellationToken cancellationToken = default) + { + var result = UpdateCount; + while (await NextResultSetAsync(cancellationToken)) + { + result += UpdateCount; + } + return result; + } + /// /// Moves the cursor to the next result set in this Rows object. /// /// True if there was another result set, and false otherwise public virtual bool NextResultSet() { + if (_hasReadAllResults) + { + return false; + } return NextResultSet(Spanner.NextResultSet(this)); } @@ -112,6 +154,10 @@ public virtual bool NextResultSet() /// True if there was another result set, and false otherwise public virtual async Task NextResultSetAsync(CancellationToken cancellationToken = default) { + if (_hasReadAllResults) + { + return false; + } return NextResultSet(await Spanner.NextResultSetAsync(this, cancellationToken)); } @@ -119,6 +165,7 @@ private bool NextResultSet(ResultSetMetadata? metadata) { if (metadata == null) { + _hasReadAllResults = true; return false; } _metadata = metadata; From 61af71a85fb0169c109d7ff38c69557f8b12760d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Mon, 1 Dec 2025 10:30:40 +0100 Subject: [PATCH 19/29] chore: update README --- drivers/spanner-ado-net/README.md | 2 +- .../spanner-ado-net-benchmarks.csproj | 2 +- drivers/spanner-ado-net/spanner-ado-net-samples/README.md | 5 +++-- drivers/spanner-ado-net/spanner-ado-net/README.md | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/drivers/spanner-ado-net/README.md b/drivers/spanner-ado-net/README.md index 2c8b029b..35b81e9e 100644 --- a/drivers/spanner-ado-net/README.md +++ b/drivers/spanner-ado-net/README.md @@ -1,6 +1,6 @@ # Spanner ADO.NET Data Provider -__ALPHA: This library is still in development. It is not year ready for production use.__ +__ALPHA: This library is still in development. It is not yet ready for production use.__ ADO.NET Data Provider for Spanner. This library implements the standard ADO.NET interfaces and classes and exposes an API that is similar to ADO.NET data providers for other relational database systems. diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/spanner-ado-net-benchmarks.csproj b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/spanner-ado-net-benchmarks.csproj index 1c8e0a3d..6e476925 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/spanner-ado-net-benchmarks.csproj +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/spanner-ado-net-benchmarks.csproj @@ -12,7 +12,7 @@ - + diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/README.md b/drivers/spanner-ado-net/spanner-ado-net-samples/README.md index b07bb9b5..336b3155 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/README.md +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/README.md @@ -1,5 +1,6 @@ # Spanner ADO.NET Data Provider Samples -Samples for ADO.NET Data Provider for Spanner. +__ALPHA: This library is still in development. It is not yet ready for production use.__ -__ALPHA: Not for production use__ +Samples for the ADO.NET Data Provider for Spanner. The [Snippets](Snippets) directory contains ready-to-run samples +for using various Spanner features with the ADO.NET provider. diff --git a/drivers/spanner-ado-net/spanner-ado-net/README.md b/drivers/spanner-ado-net/spanner-ado-net/README.md index 2c8b029b..35b81e9e 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/README.md +++ b/drivers/spanner-ado-net/spanner-ado-net/README.md @@ -1,6 +1,6 @@ # Spanner ADO.NET Data Provider -__ALPHA: This library is still in development. It is not year ready for production use.__ +__ALPHA: This library is still in development. It is not yet ready for production use.__ ADO.NET Data Provider for Spanner. This library implements the standard ADO.NET interfaces and classes and exposes an API that is similar to ADO.NET data providers for other relational database systems. From 142e2a07f40cfc96cf0fa3b362b218fb81d10413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Tue, 2 Dec 2025 10:00:15 +0100 Subject: [PATCH 20/29] chore: regen protos --- drivers/spanner-ado-net/global.json | 2 +- .../spanner-ado-net-benchmarks/deploy.txt | 155 ++ .../spanner-ado-net-benchmarks.csproj | 7 +- .../tpcc/AbstractRunner.cs | 8 + .../tpcc/BasicsRunner.cs | 242 ++ .../tpcc/Program.cs | 137 +- .../spanner-ado-net-benchmarks/tpcc/Stats.cs | 348 ++- .../tpcc/TpccRunner.cs | 21 +- ...spanner-ado-net-specification-tests.csproj | 1 + .../spanner-ado-net-tests/TransactionTests.cs | 75 +- .../spanner-ado-net-tests.csproj | 1 + .../spanner-ado-net/SpannerBatch.cs | 4 +- .../spanner-ado-net/SpannerCommand.cs | 46 +- .../spanner-ado-net/SpannerConnection.cs | 58 +- .../spanner-ado-net/SpannerDataReader.cs | 3 +- .../spanner-ado-net/SpannerPool.cs | 13 +- .../spanner-ado-net/SpannerTransaction.cs | 31 +- .../spanner-ado-net/publish.sh | 0 .../spanner-ado-net/spanner-ado-net.csproj | 14 +- spannerlib/api/connection.go | 3 + spannerlib/api/rows.go | 4 + .../google/spannerlib/v1/spannerlib.pb.go | 615 +++-- .../google/spannerlib/v1/spannerlib.proto | 46 +- .../spannerlib/v1/spannerlib_grpc.pb.go | 77 +- spannerlib/grpc-server/server.go | 181 +- spannerlib/grpc-server/server_test.go | 555 ++++- .../GrpcConnection.cs | 38 + .../GrpcLibSpanner.cs | 412 +++- .../StreamingRows.cs | 212 +- .../spannerlib-dotnet-grpc-impl.csproj | 8 +- .../spannerlib-dotnet-grpc-server.csproj | 2 +- .../spannerlib-dotnet-grpc-v1/Spannerlib.g.cs | 2100 +++++++++++++---- .../SpannerlibGrpc.cs | 18 + .../spannerlib-dotnet-grpc-v1.csproj | 2 +- .../spannerlib-dotnet-mockserver.csproj | 2 +- .../SharedLibSpanner.cs | 11 +- .../spannerlib-dotnet-native-impl.csproj | 4 +- .../spannerlib-dotnet-native.csproj | 2 +- .../AbstractMockServerTests.cs | 6 +- .../spannerlib-dotnet-tests/BasicTests.cs | 1 - .../spannerlib-dotnet-tests/RowsTests.cs | 237 +- .../spannerlib-dotnet/Connection.cs | 27 +- .../spannerlib-dotnet/ISpannerLib.cs | 18 +- .../spannerlib-dotnet/Rows.cs | 2 +- .../spannerlib-dotnet.csproj | 2 +- .../v1/ConnectionStreamRequest.java | 1040 ++++++++ .../v1/ConnectionStreamRequestOrBuilder.java | 75 + .../v1/ConnectionStreamResponse.java | 1439 ++++++++++- .../v1/ConnectionStreamResponseOrBuilder.java | 106 +- .../spannerlib/v1/ExecuteAllResponse.java | 720 ++++++ .../v1/ExecuteAllResponseOrBuilder.java | 36 + .../cloud/spannerlib/v1/ExecuteRequest.java | 185 ++ .../v1/ExecuteRequestOrBuilder.java | 15 + .../cloud/spannerlib/v1/ExecuteResponse.java | 1166 +++++++++ .../v1/ExecuteResponseOrBuilder.java | 72 + .../v1/ExecuteStreamingRequest.java | 744 ++++++ .../v1/ExecuteStreamingRequestOrBuilder.java | 42 + .../cloud/spannerlib/v1/FetchOptions.java | 500 ++++ .../spannerlib/v1/FetchOptionsOrBuilder.java | 24 + .../cloud/spannerlib/v1/NextRequest.java | 231 +- .../spannerlib/v1/NextRequestOrBuilder.java | 17 +- .../spannerlib/v1/NextResultSetResponse.java | 626 +++++ .../v1/NextResultSetResponseOrBuilder.java | 33 + .../google/cloud/spannerlib/v1/RowData.java | 386 ++- .../cloud/spannerlib/v1/RowDataOrBuilder.java | 42 +- .../cloud/spannerlib/v1/SpannerLibGrpc.java | 78 +- .../cloud/spannerlib/v1/SpannerLibProto.java | 252 +- 67 files changed, 12275 insertions(+), 1305 deletions(-) create mode 100644 drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/AbstractRunner.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/BasicsRunner.cs mode change 100644 => 100755 drivers/spanner-ado-net/spanner-ado-net/publish.sh create mode 100644 spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/GrpcConnection.cs create mode 100644 spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteAllResponse.java create mode 100644 spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteAllResponseOrBuilder.java create mode 100644 spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteResponse.java create mode 100644 spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteResponseOrBuilder.java create mode 100644 spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteStreamingRequest.java create mode 100644 spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteStreamingRequestOrBuilder.java create mode 100644 spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/FetchOptions.java create mode 100644 spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/FetchOptionsOrBuilder.java create mode 100644 spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextResultSetResponse.java create mode 100644 spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextResultSetResponseOrBuilder.java diff --git a/drivers/spanner-ado-net/global.json b/drivers/spanner-ado-net/global.json index 2ddda36c..b5b37b60 100644 --- a/drivers/spanner-ado-net/global.json +++ b/drivers/spanner-ado-net/global.json @@ -1,7 +1,7 @@ { "sdk": { "version": "8.0.0", - "rollForward": "latestMinor", + "rollForward": "latestMajor", "allowPrerelease": false } } \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt index b08e5498..d58f378e 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt @@ -1,4 +1,159 @@ +# SpannerLib, 10 clients - TPCC + +gcloud run deploy spannerlib-dotnet-benchmark-tpcc \ + --region=europe-north1 \ + --no-allow-unauthenticated --no-cpu-throttling \ + --min-instances=1 --max-instances=1 \ + --cpu=4 --memory=2Gi \ + --set-env-vars=NUM_WAREHOUSES=500,NUM_CLIENTS=10 \ + --base-image dotnet8 \ + --source . + +# SpannerLib, 10 clients - TPCC - No internal retries + +gcloud run deploy spannerlib-dotnet-benchmark-tpcc-no-internal-retries \ + --region=europe-north1 \ + --no-allow-unauthenticated --no-cpu-throttling \ + --min-instances=1 --max-instances=1 \ + --cpu=4 --memory=2Gi \ + --set-env-vars=NUM_WAREHOUSES=500,NUM_CLIENTS=10,RETRY_ABORTS_INTERNALLY=false,CLIENT_TYPE=SpannerLibNoRetries \ + --base-image dotnet8 \ + --source . + +# ClientLib, 10 clients - TPCC + +gcloud run deploy spannerlib-dotnet-benchmark-tpcc-client-lib \ + --region=europe-north1 \ + --no-allow-unauthenticated --no-cpu-throttling \ + --min-instances=1 --max-instances=1 \ + --cpu=4 --memory=2Gi \ + --set-env-vars=NUM_WAREHOUSES=500,NUM_CLIENTS=10,CLIENT_TYPE=ClientLib \ + --base-image dotnet8 \ + --source . + + + +# SpannerLib, 50 clients - PointQuery + +gcloud run deploy spannerlib-dotnet-benchmark-point-query \ + --region=europe-north1 \ + --no-allow-unauthenticated --no-cpu-throttling \ + --min-instances=1 --max-instances=1 \ + --cpu=4 --memory=2Gi \ + --set-env-vars=NUM_WAREHOUSES=100,NUM_CLIENTS=50,TRANSACTION_TYPE=PointQuery \ + --base-image dotnet8 \ + --source . + +# ClientLib, 50 clients - PointQuery + +gcloud run deploy spannerlib-dotnet-benchmark-point-query-client-lib \ + --region=europe-north1 \ + --no-allow-unauthenticated --no-cpu-throttling \ + --min-instances=1 --max-instances=1 \ + --cpu=4 --memory=2Gi \ + --set-env-vars=NUM_WAREHOUSES=100,NUM_CLIENTS=50,TRANSACTION_TYPE=PointQuery,CLIENT_TYPE=ClientLib \ + --base-image dotnet8 \ + --source . + +# SpannerLib, 50 clients - Scalar + +gcloud run deploy spannerlib-dotnet-benchmark-scalar \ + --region=europe-north1 \ + --no-allow-unauthenticated --no-cpu-throttling \ + --min-instances=1 --max-instances=1 \ + --cpu=4 --memory=2Gi \ + --set-env-vars=NUM_WAREHOUSES=100,NUM_CLIENTS=50,TRANSACTION_TYPE=Scalar \ + --base-image dotnet8 \ + --source . + +# ClientLib, 50 clients - Scalar + +gcloud run deploy spannerlib-dotnet-benchmark-scalar-client-lib \ + --region=europe-north1 \ + --no-allow-unauthenticated --no-cpu-throttling \ + --min-instances=1 --max-instances=1 \ + --cpu=4 --memory=2Gi \ + --set-env-vars=NUM_WAREHOUSES=100,NUM_CLIENTS=50,TRANSACTION_TYPE=Scalar,CLIENT_TYPE=ClientLib \ + --base-image dotnet8 \ + --source . + +# SpannerLib, 50 clients - PointDML + +gcloud run deploy spannerlib-dotnet-benchmark-point-dml \ + --region=europe-north1 \ + --no-allow-unauthenticated --no-cpu-throttling \ + --min-instances=1 --max-instances=1 \ + --cpu=4 --memory=2Gi \ + --set-env-vars=NUM_WAREHOUSES=100,NUM_CLIENTS=50,TRANSACTION_TYPE=PointDml \ + --base-image dotnet8 \ + --source . + + +# ClientLib, 50 clients - PointDML + +gcloud run deploy spannerlib-dotnet-benchmark-point-dml-client-lib \ + --region=europe-north1 \ + --no-allow-unauthenticated --no-cpu-throttling \ + --min-instances=1 --max-instances=1 \ + --cpu=4 --memory=2Gi \ + --set-env-vars=NUM_WAREHOUSES=100,NUM_CLIENTS=50,TRANSACTION_TYPE=PointDml,CLIENT_TYPE=ClientLib \ + --base-image dotnet8 \ + --source . + + +# SpannerLib, 50 clients - ReadWriteTx + +gcloud run deploy spannerlib-dotnet-benchmark-read-write-tx \ + --region=europe-north1 \ + --no-allow-unauthenticated --no-cpu-throttling \ + --min-instances=1 --max-instances=1 \ + --cpu=4 --memory=2Gi \ + --set-env-vars=NUM_WAREHOUSES=100,NUM_CLIENTS=50,TRANSACTION_TYPE=ReadWriteTx \ + --base-image dotnet8 \ + --source . + + +# ClientLib, 50 clients - ReadWriteTx + +gcloud run deploy spannerlib-dotnet-benchmark-read-write-tx-client-lib \ + --region=europe-north1 \ + --no-allow-unauthenticated --no-cpu-throttling \ + --min-instances=1 --max-instances=1 \ + --cpu=4 --memory=2Gi \ + --set-env-vars=NUM_WAREHOUSES=100,NUM_CLIENTS=50,TRANSACTION_TYPE=ReadWriteTx,CLIENT_TYPE=ClientLib \ + --base-image dotnet8 \ + --source . + + +# SpannerLib, 50 clients - LargeQuery + +gcloud run deploy spannerlib-dotnet-benchmark-large-query \ + --region=europe-north1 \ + --no-allow-unauthenticated --no-cpu-throttling \ + --min-instances=1 --max-instances=1 \ + --cpu=4 --memory=2Gi \ + --set-env-vars=NUM_WAREHOUSES=100,NUM_CLIENTS=5,TRANSACTION_TYPE=LargeQuery \ + --base-image dotnet8 \ + --source . + +# ClientLib, 50 clients - LargeQuery + +gcloud run deploy spannerlib-dotnet-benchmark-large-query-client-lib \ + --region=europe-north1 \ + --no-allow-unauthenticated --no-cpu-throttling \ + --min-instances=1 --max-instances=1 \ + --cpu=4 --memory=2Gi \ + --set-env-vars=NUM_WAREHOUSES=100,NUM_CLIENTS=5,TRANSACTION_TYPE=LargeQuery,CLIENT_TYPE=ClientLib \ + --base-image dotnet8 \ + --source . + + + + + + + gcloud run deploy spannerlib-dotnet-benchmark-tpcc \ --region=europe-north1 \ --no-allow-unauthenticated --no-cpu-throttling \ diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/spanner-ado-net-benchmarks.csproj b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/spanner-ado-net-benchmarks.csproj index 6e476925..86c91ee2 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/spanner-ado-net-benchmarks.csproj +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/spanner-ado-net-benchmarks.csproj @@ -11,9 +11,14 @@ - + + + + + + diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/AbstractRunner.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/AbstractRunner.cs new file mode 100644 index 00000000..abdbe255 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/AbstractRunner.cs @@ -0,0 +1,8 @@ +namespace Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc; + +public abstract class AbstractRunner +{ + public abstract Task RunAsync(CancellationToken cancellationToken); + + public abstract Task RunTransactionAsync(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/BasicsRunner.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/BasicsRunner.cs new file mode 100644 index 00000000..87ff7436 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/BasicsRunner.cs @@ -0,0 +1,242 @@ +using System.Data; +using System.Data.Common; +using System.Diagnostics; +using Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc.loader; + +namespace Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc; + +public class BasicsRunner : AbstractRunner +{ + private readonly Stats _stats; + private readonly DbConnection _connection; + private readonly Program.BenchmarkType _benchmarkType; + private readonly int _numWarehouses; + private readonly int _numDistrictsPerWarehouse; + private readonly int _numCustomersPerDistrict; + private readonly int _numItems; + + internal BasicsRunner( + Stats stats, + DbConnection connection, + Program.BenchmarkType benchmarkType, + int numWarehouses, + int numDistrictsPerWarehouse = 10, + int numCustomersPerDistrict = 3000, + int numItems = 100_000) + { + _stats = stats; + _connection = connection; + _benchmarkType = benchmarkType; + _numWarehouses = numWarehouses; + _numDistrictsPerWarehouse = numDistrictsPerWarehouse; + _numCustomersPerDistrict = numCustomersPerDistrict; + _numItems = numItems; + } + + public override async Task RunAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await RunTransactionAsync(cancellationToken); + var delay = Random.Shared.Next(10, 100); + await Task.Delay(delay, cancellationToken); + } + } + + public override async Task RunTransactionAsync(CancellationToken cancellationToken) + { + switch (_benchmarkType) + { + case Program.BenchmarkType.PointQuery: + await PointQueryAsync(cancellationToken); + break; + case Program.BenchmarkType.Scalar: + await ScalarAsync(cancellationToken); + break; + case Program.BenchmarkType.LargeQuery: + await LargeQueryAsync(cancellationToken); + break; + case Program.BenchmarkType.PointDml: + await PointDmlAsync(cancellationToken); + break; + case Program.BenchmarkType.ReadWriteTx: + await ReadWriteTransactionAsync(cancellationToken); + break; + default: + throw new NotSupportedException($"Transaction type {_benchmarkType} is not supported."); + } + } + + private async Task PointQueryAsync(CancellationToken cancellationToken) + { + var watch = Stopwatch.StartNew(); + await using var command = CreatePointReadCommand(); + var numRows = 0; + await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + for (var col = 0; col < reader.FieldCount; col++) + { + _ = reader.GetValue(col); + } + numRows++; + } + if (numRows != 1) + { + throw new InvalidOperationException("Unexpected number of rows returned: " + numRows); + } + watch.Stop(); + _stats.RegisterOperationLatency(Program.OperationType.PointQuery, watch.Elapsed.TotalMilliseconds); + } + + private async Task ScalarAsync(CancellationToken cancellationToken) + { + var watch = Stopwatch.StartNew(); + await using var command = CreatePointReadCommand(); + var item = await command.ExecuteScalarAsync(cancellationToken); + if (item == null) + { + throw new InvalidOperationException("No row returned"); + } + watch.Stop(); + _stats.RegisterOperationLatency(Program.OperationType.Scalar, watch.Elapsed.TotalMilliseconds); + } + + private async Task PointDmlAsync(CancellationToken cancellationToken) + { + var warehouseId = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numWarehouses)); + var districtId = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numDistrictsPerWarehouse)); + var customerId = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numCustomersPerDistrict)); + + var watch = Stopwatch.StartNew(); + await using var command = _connection.CreateCommand(); + command.CommandText = "update customer set c_data=$1 where w_id=$2 and d_id=$3 and c_id=$4"; + AddParameter(command, "p1", DataLoader.RandomString(500)); + AddParameter(command, "p2", warehouseId); + AddParameter(command, "p3", districtId); + AddParameter(command, "p4", customerId); + var updated = await command.ExecuteNonQueryAsync(cancellationToken); + if (updated != 1) + { + throw new InvalidOperationException("Unexpected affected rows: " + updated); + } + watch.Stop(); + _stats.RegisterOperationLatency(Program.OperationType.PointDml, watch.Elapsed.TotalMilliseconds); + } + + private async Task ReadWriteTransactionAsync(CancellationToken cancellationToken) + { + while (true) + { + var watch = Stopwatch.StartNew(); + try + { + await using var transaction = await _connection.BeginTransactionAsync(IsolationLevel.RepeatableRead, cancellationToken); + if (transaction is Data.SpannerTransaction spannerTransaction) + { + spannerTransaction.Tag = "client-lib"; + } + else + { + await using var cmd = _connection.CreateCommand(); + cmd.CommandText = "set local transaction_tag = 'spanner-lib'"; + await cmd.ExecuteNonQueryAsync(cancellationToken); + } + + for (var i = 0; i < 3; i++) + { + var warehouseId = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numWarehouses)); + var districtId = + DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numDistrictsPerWarehouse)); + var customerId = + DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numCustomersPerDistrict)); + + await using var selectCommand = _connection.CreateCommand(); + selectCommand.CommandText = "select * from customer where w_id=$1 and d_id=$2 and c_id=$3"; + selectCommand.Transaction = transaction; + AddParameter(selectCommand, "p1", warehouseId); + AddParameter(selectCommand, "p2", districtId); + AddParameter(selectCommand, "p3", customerId); + await using var reader = await selectCommand.ExecuteReaderAsync(cancellationToken); + var foundRows = 0; + while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + foundRows++; + } + + if (foundRows != 1) + { + throw new InvalidOperationException("Unexpected found rows: " + foundRows); + } + + await using var updateCommand = _connection.CreateCommand(); + updateCommand.CommandText = "update customer set c_data=$1 where w_id=$2 and d_id=$3 and c_id=$4"; + updateCommand.Transaction = transaction; + AddParameter(updateCommand, "p1", DataLoader.RandomString(500)); + AddParameter(updateCommand, "p2", warehouseId); + AddParameter(updateCommand, "p3", districtId); + AddParameter(updateCommand, "p4", customerId); + var updated = await updateCommand.ExecuteNonQueryAsync(cancellationToken); + if (updated != 1) + { + throw new InvalidOperationException("Unexpected affected rows: " + updated); + } + } + + await transaction.CommitAsync(cancellationToken); + watch.Stop(); + _stats.RegisterOperationLatency(Program.OperationType.ReadWriteTx, watch.Elapsed.TotalMilliseconds); + break; + } + catch (Exception exception) + { + _stats.RegisterFailedOperation(Program.OperationType.ReadWriteTx, watch.Elapsed, exception); + } + } + } + + private async Task LargeQueryAsync(CancellationToken cancellationToken) + { + await using var command = _connection.CreateCommand(); + command.CommandText = "select * from customer limit $1 offset $2"; + AddParameter(command, "p1", 100_000L); + AddParameter(command, "p2", Random.Shared.Next(1, 1001)); + await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + // Fetch the first row outside the measurement to ensure a fair comparison between clients that delay the + // query execution to the first read, and those that don't. + await reader.ReadAsync(cancellationToken).ConfigureAwait(false); + + var watch = Stopwatch.StartNew(); + while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + for (var col = 0; col < reader.FieldCount; col++) + { + _ = reader.GetValue(col); + } + } + watch.Stop(); + _stats.RegisterOperationLatency(Program.OperationType.LargeQuery, watch.Elapsed.TotalMilliseconds); + } + + private DbCommand CreatePointReadCommand() + { + var warehouseId = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numWarehouses)); + var itemId = DataLoader.ReverseBitsUnsigned((ulong)Random.Shared.Next(_numItems)); + + var command = _connection.CreateCommand(); + command.CommandText = "select * from stock where s_i_id=$1 and w_id=$2"; + AddParameter(command, "p1", itemId); + AddParameter(command, "p2", warehouseId); + + return command; + } + + private void AddParameter(DbCommand command, string name, object value) + { + var itemParameter = command.CreateParameter(); + itemParameter.ParameterName = name; + itemParameter.Value = value; + command.Parameters.Add(itemParameter); + } + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Program.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Program.cs index 62e0d7d1..7d151009 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Program.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Program.cs @@ -1,23 +1,58 @@ using System.Collections.Concurrent; using System.Data.Common; using System.Diagnostics; +using System.Runtime.InteropServices; +using Google.Api; +using Google.Api.Gax.ResourceNames; +using Google.Cloud.Monitoring.V3; using Google.Cloud.Spanner.Admin.Database.V1; +using Google.Cloud.Spanner.Common.V1; using Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc.loader; +using Google.Cloud.SpannerLib.Grpc; using Microsoft.AspNetCore.Builder; +using Enum = System.Enum; namespace Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc; public static class Program { - enum ClientType + public enum ClientType { SpannerLib, + BidiSpannerLib, + SpannerLibNoRetries, NativeSpannerLib, ClientLib, } + + public enum BenchmarkType + { + Tpcc, + PointQuery, + LargeQuery, + Scalar, + PointDml, + ReadWriteTx, + } + + public enum OperationType + { + PointQuery, + LargeQuery, + Scalar, + PointDml, + ReadWriteTx, + NewOrder, + Payment, + OrderStatus, + Delivery, + StockLevel, + } public static async Task Main(string[] args) { + Console.WriteLine($"Runtime: {RuntimeInformation.RuntimeIdentifier}"); + var cancellationTokenSource = new CancellationTokenSource(); var builder = WebApplication.CreateBuilder(args); var port = Environment.GetEnvironmentVariable("PORT") ?? "8080"; @@ -26,17 +61,75 @@ public static async Task Main(string[] args) app.MapGet("/", () => { }); var webapp = app.RunAsync(url); - var logWaitTime = int.Parse(Environment.GetEnvironmentVariable("LOG_WAIT_TIME") ?? "10"); + var exportStats = bool.Parse(Environment.GetEnvironmentVariable("EXPORT_STATS") ?? "true"); + var logWaitTime = int.Parse(Environment.GetEnvironmentVariable("LOG_WAIT_TIME") ?? "60"); var database = Environment.GetEnvironmentVariable("DATABASE") ?? "projects/appdev-soda-spanner-staging/instances/knut-test-ycsb/databases/dotnet-tpcc"; var retryAbortsInternally = bool.Parse(Environment.GetEnvironmentVariable("RETRY_ABORTS_INTERNALLY") ?? "true"); var numWarehouses = int.Parse(Environment.GetEnvironmentVariable("NUM_WAREHOUSES") ?? "10"); var numClients = int.Parse(Environment.GetEnvironmentVariable("NUM_CLIENTS") ?? "10"); var targetTps = int.Parse(Environment.GetEnvironmentVariable("TRANSACTIONS_PER_SECOND") ?? "0"); - var clientTypeName = Environment.GetEnvironmentVariable("CLIENT_TYPE") ?? "SpannerLib"; + var clientTypeName = Environment.GetEnvironmentVariable("CLIENT_TYPE") ?? "BidiSpannerLib"; + var useSharedLib = bool.Parse(Environment.GetEnvironmentVariable("SPANNER_ADO_USE_NATIVE_LIB") ?? "false"); + var directPath = bool.Parse(Environment.GetEnvironmentVariable("GOOGLE_SPANNER_ENABLE_DIRECT_ACCESS") ?? "false"); + var communicationStyle = Enum.Parse(Environment.GetEnvironmentVariable("SPANNER_ADO_COMMUNICATION_STYLE") ?? nameof(GrpcLibSpanner.CommunicationStyle.BidiStreaming)); + var transactionTypeName = Environment.GetEnvironmentVariable("TRANSACTION_TYPE") ?? "Tpcc"; if (!Enum.TryParse(clientTypeName, out ClientType clientType)) { throw new ArgumentException($"Unknown client type: {clientTypeName}"); } + if (useSharedLib && clientType != ClientType.NativeSpannerLib) + { + throw new ArgumentException("Invalid combination of clientType and useSharedLib"); + } + if (!Enum.TryParse(transactionTypeName, out BenchmarkType transactionType)) + { + throw new ArgumentException($"Unknown transaction type: {transactionTypeName}"); + } + + var databaseName = DatabaseName.Parse(database); + var projectName = ProjectName.FromProject(databaseName.ProjectId); + var metricsClient = await MetricServiceClient.CreateAsync(cancellationTokenSource.Token); + var numTransactionsDescriptor = await metricsClient.CreateMetricDescriptorAsync(new CreateMetricDescriptorRequest + { + ProjectName = projectName, + MetricDescriptor = new MetricDescriptor + { + Name = "spanner-ado-net-tpcc-tps", + DisplayName = "Spanner ADO.NET TPCC Number of Transactions", + Description = "Spanner ADO.NET TPCC Number of Transactions", + LaunchStage = LaunchStage.Alpha, + MetricKind = MetricDescriptor.Types.MetricKind.Cumulative, + ValueType = MetricDescriptor.Types.ValueType.Int64, + Type = "custom.googleapis.com/spanner-ado-net-tpcc/num_transactions", + Labels = { + new LabelDescriptor { Key = "num_clients" , Description = "Number of clients", ValueType = LabelDescriptor.Types.ValueType.Int64 }, + new LabelDescriptor { Key = "client_type" , Description = "Client type", ValueType = LabelDescriptor.Types.ValueType.String }, + } + }, + }); + // await metricsClient.DeleteMetricDescriptorAsync(new DeleteMetricDescriptorRequest + // { + // MetricDescriptorName = MetricDescriptorName.FromProjectMetricDescriptor("appdev-soda-spanner-staging", "custom.googleapis.com/spanner-ado-net-tpcc/operation_latency"), + // }); + var operationLatencyDescriptor = await metricsClient.CreateMetricDescriptorAsync(new CreateMetricDescriptorRequest + { + ProjectName = projectName, + MetricDescriptor = new MetricDescriptor + { + Name = "spanner-ado-net-operation-latency", + DisplayName = "Spanner ADO.NET Operation Latency", + Description = "Spanner ADO.NET Operation Latency", + LaunchStage = LaunchStage.Alpha, + MetricKind = MetricDescriptor.Types.MetricKind.Gauge, + ValueType = MetricDescriptor.Types.ValueType.Distribution, + Type = "custom.googleapis.com/spanner-ado-net-tpcc/operation_latency", + Labels = { + new LabelDescriptor { Key = "num_clients" , Description = "Number of clients", ValueType = LabelDescriptor.Types.ValueType.Int64 }, + new LabelDescriptor { Key = "client_type" , Description = "Client type", ValueType = LabelDescriptor.Types.ValueType.String }, + new LabelDescriptor { Key = "operation_type" , Description = "Operation type", ValueType = LabelDescriptor.Types.ValueType.String }, + } + }, + }); var connectionString = $"Data Source={database}"; if (!retryAbortsInternally) @@ -56,19 +149,22 @@ public static async Task Main(string[] args) await loader.LoadAsync(cancellationTokenSource.Token); } - Console.WriteLine("Running benchmark..."); - var stats = new Stats(); + Console.WriteLine($"Running benchmark {transactionType}..."); + Console.WriteLine($"Client type: {clientType}"); + Console.WriteLine($"Num clients: {numClients}"); + Console.WriteLine($"Exporting stats: {exportStats}"); + var stats = new Stats(exportStats, projectName, metricsClient, numTransactionsDescriptor, operationLatencyDescriptor, numClients, clientType, directPath); - if (targetTps > 0) + if (targetTps > 0 && transactionType == BenchmarkType.Tpcc) { var maxWaitTime = 2 * 1000 / targetTps; Console.WriteLine($"Clients: {numClients}"); Console.WriteLine($"Transactions per second: {targetTps}"); Console.WriteLine($"Max wait time: {maxWaitTime}"); - var runners = new BlockingCollection(); + var runners = new BlockingCollection(); for (var client = 0; client < numClients; client++) { - runners.Add(await CreateRunnerAsync(clientType, connectionString, stats, numWarehouses, cancellationTokenSource), cancellationTokenSource.Token); + runners.Add(await CreateRunnerAsync(clientType, transactionType, connectionString, stats, numWarehouses, cancellationTokenSource), cancellationTokenSource.Token); } var lastLogTime = DateTime.UtcNow; while (!cancellationTokenSource.IsCancellationRequested) @@ -103,6 +199,7 @@ public static async Task Main(string[] args) Console.WriteLine($"Num available runners: {runners.Count}"); Console.WriteLine($"Thread pool size: {ThreadPool.ThreadCount}"); stats.LogStats(); + await stats.ExportMetrics(); lastLogTime = DateTime.UtcNow; } } @@ -112,13 +209,14 @@ public static async Task Main(string[] args) var tasks = new List(); for (var client = 0; client < numClients; client++) { - var runner = await CreateRunnerAsync(clientType, connectionString, stats, numWarehouses, cancellationTokenSource); + var runner = await CreateRunnerAsync(clientType, transactionType, connectionString, stats, numWarehouses, cancellationTokenSource); tasks.Add(runner.RunAsync(cancellationTokenSource.Token)); } while (!cancellationTokenSource.Token.IsCancellationRequested) { await Task.Delay(TimeSpan.FromSeconds(logWaitTime), cancellationTokenSource.Token); stats.LogStats(); + await stats.ExportMetrics(); } await Task.WhenAll(tasks); } @@ -127,32 +225,29 @@ public static async Task Main(string[] args) await webapp; } - private static async Task CreateRunnerAsync( + private static async Task CreateRunnerAsync( ClientType clientType, + BenchmarkType benchmarkType, string connectionString, Stats stats, int numWarehouses, CancellationTokenSource cancellationTokenSource) { DbConnection connection; - if (clientType == ClientType.SpannerLib) - { - connection = new SpannerConnection(); - } - else if (clientType == ClientType.NativeSpannerLib) - { - connection = new SpannerConnection {UseNativeLibrary = true}; - } - else if (clientType == ClientType.ClientLib) + if (clientType == ClientType.ClientLib) { connection = new Google.Cloud.Spanner.Data.SpannerConnection(); } else { - throw new ArgumentException($"Unknown client type: {clientType}"); + connection = new SpannerConnection(); } connection.ConnectionString = connectionString; await connection.OpenAsync(cancellationTokenSource.Token); - return new TpccRunner(stats, connection, numWarehouses); + if (benchmarkType == BenchmarkType.Tpcc) + { + return new TpccRunner(stats, connection, numWarehouses); + } + return new BasicsRunner(stats, connection, benchmarkType, numWarehouses); } } \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Stats.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Stats.cs index 56ab5e10..db0d234e 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Stats.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Stats.cs @@ -1,11 +1,63 @@ +using System.Text; +using Google.Api; +using Google.Api.Gax.ResourceNames; +using Google.Cloud.Monitoring.V3; +using Google.Protobuf.WellKnownTypes; +using Enum = System.Enum; + namespace Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc; +internal static class BenchmarkTypeExtensions +{ + internal static string GetLabel(this Program.OperationType type) + { + return type.ToString().ToSnakeCase(); + } + + private static string ToSnakeCase(this string text) + { + if(text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if(text.Length < 2) + { + return text.ToLowerInvariant(); + } + var sb = new StringBuilder(); + sb.Append(char.ToLowerInvariant(text[0])); + for(int i = 1; i < text.Length; ++i) + { + char c = text[i]; + if(char.IsUpper(c)) + { + sb.Append('_'); + sb.Append(char.ToLowerInvariant(c)); + } + else + { + sb.Append(c); + } + } + return sb.ToString(); + } +} + internal class Stats { + private readonly bool _exportStats; + private readonly ProjectName _projectName; + private readonly MetricServiceClient _metricsClient; + private readonly MetricDescriptor _numTransactionsDescriptor; + private readonly MetricDescriptor _operationLatencyDescriptor; + private readonly Dictionary _labels; + private readonly Dictionary> _operationLabels = new(); + private readonly DateTime _startTime; private ulong _numTransactions; private ulong _numTransactionsStarted; private ulong _numTransactionsCompleted; + private ulong _numAbortedTransactions; private ulong _numFailedTransactions; private ulong _numNewOrderTransactions; private ulong _numPaymentTransactions; @@ -15,9 +67,46 @@ internal class Stats private Exception? _lastException; private ulong _totalMillis; + private readonly object _lock = new(); + private readonly Dictionary> _operationLatencies = new(); + private ulong _numOperations; + private ulong _numFailedOperations; + private double _totalOperationMillis; - internal Stats() + internal Stats( + bool exportStats, + ProjectName projectName, + MetricServiceClient metricsClient, + MetricDescriptor numTransactionsDescriptor, + MetricDescriptor operationLatencyDescriptor, + int numClients, + Program.ClientType clientType, + bool directPath) { + _exportStats = exportStats; + _projectName = projectName; + _metricsClient = metricsClient; + _numTransactionsDescriptor = numTransactionsDescriptor; + _operationLatencyDescriptor = operationLatencyDescriptor; + var clientTypeName = clientType.ToString(); + if (directPath) + { + clientTypeName += "DirectPath"; + } + _labels = new Dictionary + { + { "num_clients", numClients.ToString() }, + { "client_type", clientTypeName }, + }; + foreach (var type in Enum.GetValues()) + { + var labels = new Dictionary(_labels) + { + ["operation_type"] = type.GetLabel(), + }; + _operationLabels[type] = labels; + _operationLatencies.Add(type, new LinkedList()); + } _startTime = DateTime.UtcNow; } @@ -39,23 +128,33 @@ internal void RegisterTransaction(TpccRunner.TransactionType transactionType, Ti { case TpccRunner.TransactionType.NewOrder: Interlocked.Increment(ref _numNewOrderTransactions); + RegisterOperationLatency(Program.OperationType.NewOrder, duration.TotalMilliseconds); break; case TpccRunner.TransactionType.Payment: Interlocked.Increment(ref _numPaymentTransactions); + RegisterOperationLatency(Program.OperationType.Payment, duration.TotalMilliseconds); break; case TpccRunner.TransactionType.OrderStatus: Interlocked.Increment(ref _numOrderStatusTransactions); + RegisterOperationLatency(Program.OperationType.OrderStatus, duration.TotalMilliseconds); break; case TpccRunner.TransactionType.Delivery: Interlocked.Increment(ref _numDeliveryTransactions); + RegisterOperationLatency(Program.OperationType.Delivery, duration.TotalMilliseconds); break; case TpccRunner.TransactionType.StockLevel: Interlocked.Increment(ref _numStockLevelTransactions); + RegisterOperationLatency(Program.OperationType.StockLevel, duration.TotalMilliseconds); break; default: throw new ArgumentOutOfRangeException(nameof(transactionType), transactionType, null); } } + + internal void RegisterAbortedTransaction(TpccRunner.TransactionType transactionType, TimeSpan duration, Exception error) + { + Interlocked.Increment(ref _numAbortedTransactions); + } internal void RegisterFailedTransaction(TpccRunner.TransactionType transactionType, TimeSpan duration, Exception error) { @@ -66,6 +165,11 @@ internal void RegisterFailedTransaction(TpccRunner.TransactionType transactionTy } } + internal void RegisterFailedOperation(Program.OperationType operationType, TimeSpan duration, Exception error) + { + Interlocked.Increment(ref _numFailedOperations); + } + internal void LogStats() { lock (this) @@ -76,17 +180,40 @@ internal void LogStats() _lastException = null; } } - Console.Write(ToString()); + if (Interlocked.Read(ref _numTransactions) > 0) + { + Console.Write(ToString()); + } + if (Interlocked.Read(ref _numOperations) > 0) + { + double avg; + lock (_lock) + { + avg = _totalOperationMillis / _numOperations; + } + Console.WriteLine($" Total time: {DateTime.UtcNow - _startTime}"); + Console.WriteLine($" Avg: {avg}"); + Console.WriteLine($" Num operations: {Interlocked.Read(ref _numOperations)}"); + Console.WriteLine($"Num failed operations: {Interlocked.Read(ref _numFailedOperations)}"); + } + Console.WriteLine($"Total pause duration: {GC.GetTotalPauseDuration()}"); + Console.WriteLine($"Pause time percentage: {GC.GetGCMemoryInfo().PauseTimePercentage}"); } public override string ToString() { + if (Interlocked.Read(ref _numTransactions) == 0) + { + return "No TPCC stats"; + } return $" Total duration: {DateTime.UtcNow - _startTime}{Environment.NewLine}" + $"Transactions/sec: {Interlocked.Read(ref _numTransactions) / (DateTime.UtcNow - _startTime).TotalSeconds}{Environment.NewLine}" + $" Total: {Interlocked.Read(ref _numTransactions)}{Environment.NewLine}" + $" Avg: {Interlocked.Read(ref _totalMillis) / Interlocked.Read(ref _numTransactions)}{Environment.NewLine}" + $" Started: {Interlocked.Read(ref _numTransactionsStarted)}{Environment.NewLine}" + $" Completed: {Interlocked.Read(ref _numTransactionsCompleted)}{Environment.NewLine}" + + $" Aborted: {Interlocked.Read(ref _numAbortedTransactions)}{Environment.NewLine}" + + $" Abort rate: {Interlocked.Read(ref _numAbortedTransactions) / Interlocked.Read(ref _numTransactions)}{Environment.NewLine}" + $" Failed: {Interlocked.Read(ref _numFailedTransactions)}{Environment.NewLine}" + $" Num new order: {Interlocked.Read(ref _numNewOrderTransactions)}{Environment.NewLine}" + $" Num payment: {Interlocked.Read(ref _numPaymentTransactions)}{Environment.NewLine}" + @@ -94,4 +221,221 @@ public override string ToString() $" Num delivery: {Interlocked.Read(ref _numDeliveryTransactions)}{Environment.NewLine}" + $" Num stock level: {Interlocked.Read(ref _numStockLevelTransactions)}{Environment.NewLine}"; } + + internal async Task ExportMetrics() + { + if (Interlocked.Read(ref _numTransactions) > 0) + { + await ExportTpccMetrics(); + } + + var exportOperationLatencies = false; + lock (_operationLatencies) + { + foreach (var latencies in _operationLatencies) + { + if (latencies.Value.First != null) + { + exportOperationLatencies = true; + } + } + } + + if (exportOperationLatencies) + { + await ExportOperationLatencyMetrics(); + } + } + + + private async Task ExportTpccMetrics() + { + var request = new CreateTimeSeriesRequest + { + ProjectName = _projectName, + TimeSeries = + { + new TimeSeries + { + ValueType = _numTransactionsDescriptor.ValueType, + MetricKind = _numTransactionsDescriptor.MetricKind, + Metric = new Metric + { + Type = _numTransactionsDescriptor.Type, + Labels = { _labels }, + }, + Points = + { + new Point + { + Interval = new TimeInterval + { + StartTime = Timestamp.FromDateTime(_startTime), + EndTime = Timestamp.FromDateTime(DateTime.UtcNow), + }, + Value = new TypedValue { Int64Value = (long)_numTransactions } + } + } + } + } + }; + if (_exportStats) + { + await _metricsClient.CreateTimeSeriesAsync(request); + } + else + { + Console.WriteLine(request); + } + } + + internal void RegisterOperationLatency(Program.OperationType benchmarkType, double millis) + { + Interlocked.Increment(ref _numOperations); + lock (_lock) + { + _totalOperationMillis += millis; + _operationLatencies[benchmarkType].AddLast(millis); + } + } + + private async Task ExportOperationLatencyMetrics() + { + Dictionary> latencies = new Dictionary>(); + lock (_lock) + { + foreach (var entry in _operationLatencies) + { + if (entry.Value.First != null) + { + var points = new List(entry.Value); + latencies[entry.Key] = points; + entry.Value.Clear(); + } + } + } + var exportTime = DateTime.UtcNow; + + foreach (var entry in latencies) + { + var labels = _operationLabels[entry.Key]; + var points = entry.Value; + var request = new CreateTimeSeriesRequest + { + ProjectName = _projectName, + TimeSeries = + { + new TimeSeries + { + ValueType = _operationLatencyDescriptor.ValueType, + MetricKind = _operationLatencyDescriptor.MetricKind, + Metric = new Metric + { + Type = _operationLatencyDescriptor.Type, + Labels = { labels }, + }, + Points = + { + new Point + { + Interval = new TimeInterval + { + EndTime = Timestamp.FromDateTime(exportTime), + }, + Value = new TypedValue + { + DistributionValue = CreateDistribution(entry.Key, points), + } + } + }, + } + }, + }; + if (_exportStats) + { + await _metricsClient.CreateTimeSeriesAsync(request); + } + else + { + Console.WriteLine(request); + } + } + } + + private Distribution CreateDistribution(Program.OperationType benchmarkType, List points) + { + if (points.Count == 0) + { + return new Distribution + { + Count = 0, + Mean = 0.0d, + }; + } + double[] bounds; + + if (benchmarkType == Program.OperationType.LargeQuery) + { + bounds = + [ + 250d, 500d, 750d, 1000d, 1250d, 1500d, 2000d, 2250d, 2500d, + 2750d, 3000d, 3250d, 3500d, 4000d, 5000d, 6000d, 7000d, + 8000d, 9000d, 10000d, 11000d, 12000d, 13000d, 14000d, 15000d, + 16000d, 17000d, 18000d, 19000d, 20000d, 25000d, 30000d, 50000d + ]; + } + else + { + bounds = + [ + 1.0d, 1.1d, 1.2d, 1.3d, 1.4d, 1.5d, 1.6d, 1.7d, 1.8d, 1.9d, + 2.0d, 2.1d, 2.2d, 2.3d, 2.4d, 2.5d, 2.6d, 2.7d, 2.8d, 2.9d, + 3.0d, 3.1d, 3.2d, 3.3d, 3.4d, 3.5d, 3.6d, 3.7d, 3.8d, 3.9d, + 4.0d, 4.1d, 4.2d, 4.3d, 4.4d, 4.5d, 4.6d, 4.7d, 4.8d, 4.9d, + 5.0d, 5.1d, 5.2d, 5.3d, 5.4d, 5.5d, 5.6d, 5.7d, 5.8d, 5.9d, + 6.0d, 6.1d, 6.2d, 6.3d, 6.4d, 6.5d, 6.6d, 6.7d, 6.8d, 6.9d, + 7.0d, 7.2d, 7.4d, 7.6d, 7.8d, 8.0d, 8.2d, 8.4d, 8.6d, 8.8d, + 9.0d, 9.5d, 10.0d, 10.5d, 11.0d, 11.5d, 12.0d, 12.5d, 13.0d, + 14d, 15d, 17.5d, 20d, 25d, 30d, 35d, 40d, 50d, 100d, 200d, + 300d, 400d, 500d, 600d, 700d, 800d, 900d, 1000d, 2000d + ]; + } + + var bucketCounts = new long[bounds.Length + 1]; + foreach (var point in points) + { + if (point < bounds[0]) + { + bucketCounts[0]++; + } + else if (point >= bounds[^1]) + { + bucketCounts[^1]++; + } + else + { + for (var i = 1; i < bounds.Length; i++) + { + if (point >= bounds[i-1] && point < bounds[i]) + { + bucketCounts[i]++; + break; + } + } + } + } + + return new Distribution + { + Count = points.Count, + BucketCounts = { bucketCounts }, + BucketOptions = new Distribution.Types.BucketOptions + { + ExplicitBuckets = new Distribution.Types.BucketOptions.Types.Explicit + { + Bounds = { bounds }, + }, + } + }; + } } \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/TpccRunner.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/TpccRunner.cs index bf753df6..29fb9923 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/TpccRunner.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/TpccRunner.cs @@ -23,7 +23,7 @@ namespace Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc; -internal class TpccRunner +internal class TpccRunner : AbstractRunner { internal enum TransactionType { @@ -62,7 +62,7 @@ internal TpccRunner( _isClientLib = connection is Data.SpannerConnection; } - internal async Task RunAsync(CancellationToken cancellationToken) + public override async Task RunAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { @@ -70,7 +70,7 @@ internal async Task RunAsync(CancellationToken cancellationToken) } } - internal async Task RunTransactionAsync(CancellationToken cancellationToken) + public override async Task RunTransactionAsync(CancellationToken cancellationToken) { var watch = Stopwatch.StartNew(); var transaction = Random.Shared.Next(23); @@ -121,11 +121,17 @@ internal async Task RunTransactionAsync(CancellationToken cancellationToken) { if (exception is SpannerException { Code: Code.Aborted }) { + _stats.RegisterAbortedTransaction(transactionType, watch.Elapsed, exception); + continue; + } + if (exception is SpannerDbException { Status.Code: (int)Code.Aborted }) + { + _stats.RegisterAbortedTransaction(transactionType, watch.Elapsed, exception); continue; } - if (exception is Data.SpannerException { ErrorCode: ErrorCode.Aborted }) { + _stats.RegisterAbortedTransaction(transactionType, watch.Elapsed, exception); continue; } } @@ -658,14 +664,14 @@ private async Task BeginTransactionAsync(string tag, CancellationToken cancellat SpannerTransactionCreationOptions.ReadWrite.WithIsolationLevel(IsolationLevel.RepeatableRead), new SpannerTransactionOptions { - Tag = tag, + Tag = tag + "_client_lib", }, cancellationToken); } else if (_connection is SpannerConnection connection) { _currentTransaction = await connection.BeginTransactionAsync(IsolationLevel.RepeatableRead, cancellationToken); - await ExecuteNonQueryAsync($"set local transaction_tag = '{tag}'", cancellationToken); + await ExecuteNonQueryAsync($"set local transaction_tag = '{tag}_spanner_lib'", cancellationToken); } } @@ -857,5 +863,4 @@ private long GetOtherWarehouseId(long currentId) { } } } - -} \ No newline at end of file +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/spanner-ado-net-specification-tests.csproj b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/spanner-ado-net-specification-tests.csproj index 905694fb..919c2d84 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-specification-tests/spanner-ado-net-specification-tests.csproj +++ b/drivers/spanner-ado-net/spanner-ado-net-specification-tests/spanner-ado-net-specification-tests.csproj @@ -10,6 +10,7 @@ true Google.Cloud.Spanner.DataProvider.SpecificationTests default + LatestMajor diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/TransactionTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/TransactionTests.cs index 9299ef6e..70bcedd5 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/TransactionTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/TransactionTests.cs @@ -13,6 +13,7 @@ // limitations under the License. using System.Data; +using System.Data.Common; using System.Diagnostics.CodeAnalysis; using Google.Cloud.Spanner.V1; using Google.Cloud.SpannerLib.MockServer; @@ -485,23 +486,83 @@ public async Task EmptyDispose() } [Test] - [TestCase(IsolationLevel.RepeatableRead, TransactionOptions.Types.IsolationLevel.RepeatableRead)] - [TestCase(IsolationLevel.Serializable, TransactionOptions.Types.IsolationLevel.Serializable)] - [TestCase(IsolationLevel.Snapshot, TransactionOptions.Types.IsolationLevel.RepeatableRead)] - [TestCase(IsolationLevel.Unspecified, TransactionOptions.Types.IsolationLevel.Unspecified)] + public async Task DbConnectionIsolationLevel() + { + const string insertSql = "INSERT INTO my_table (name) VALUES ('X')"; + Fixture.SpannerMock.AddOrUpdateStatementResult(insertSql, StatementResult.CreateUpdateCount(1L)); + const string selectSql = "select value from my_table where id=@id"; + Fixture.SpannerMock.AddOrUpdateStatementResult(selectSql, StatementResult.CreateSingleColumnResultSet(new V1.Type{Code = V1.TypeCode.String}, "value", "One")); + + var cancellationToken = CancellationToken.None; + await using var conn = await OpenConnectionAsync(); + DbConnection dbConn = conn; + await using var transaction = await dbConn.BeginTransactionAsync(IsolationLevel.RepeatableRead, cancellationToken); + await using var cmd = dbConn.CreateCommand(); + cmd.CommandText = "set local transaction_tag = 'spanner-lib'"; + await cmd.ExecuteNonQueryAsync(cancellationToken); + + await using var selectCommand = dbConn.CreateCommand(); + selectCommand.CommandText = selectSql; + selectCommand.Transaction = transaction; + await using var reader = await selectCommand.ExecuteReaderAsync(cancellationToken); + var foundRows = 0; + while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + foundRows++; + } + if (foundRows != 1) + { + throw new InvalidOperationException("Unexpected found rows: " + foundRows); + } + + await using var updateCommand = dbConn.CreateCommand(); + updateCommand.CommandText = insertSql; + updateCommand.Transaction = transaction; + var updated = await updateCommand.ExecuteNonQueryAsync(cancellationToken); + if (updated != 1) + { + throw new InvalidOperationException("Unexpected affected rows: " + updated); + } + await transaction.CommitAsync(cancellationToken); + + var requests = Fixture.SpannerMock.Requests; + Console.WriteLine(requests); + var request = Fixture.SpannerMock.Requests.OfType().First(); + Assert.That(request.Transaction.Begin.IsolationLevel, Is.EqualTo(TransactionOptions.Types.IsolationLevel.RepeatableRead)); + } + + [Test] + [TestCase(IsolationLevel.RepeatableRead, TransactionOptions.Types.IsolationLevel.RepeatableRead, false)] + [TestCase(IsolationLevel.Serializable, TransactionOptions.Types.IsolationLevel.Serializable, false)] + [TestCase(IsolationLevel.Snapshot, TransactionOptions.Types.IsolationLevel.RepeatableRead, false)] + [TestCase(IsolationLevel.Unspecified, TransactionOptions.Types.IsolationLevel.Unspecified, false)] + [TestCase(IsolationLevel.RepeatableRead, TransactionOptions.Types.IsolationLevel.RepeatableRead, true)] + [TestCase(IsolationLevel.Serializable, TransactionOptions.Types.IsolationLevel.Serializable, true)] + [TestCase(IsolationLevel.Snapshot, TransactionOptions.Types.IsolationLevel.RepeatableRead, true)] + [TestCase(IsolationLevel.Unspecified, TransactionOptions.Types.IsolationLevel.Unspecified, true)] [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] - public async Task SupportedIsolationLevels(IsolationLevel level, TransactionOptions.Types.IsolationLevel expectedSpannerLevel) + public async Task SupportedIsolationLevels(IsolationLevel level, TransactionOptions.Types.IsolationLevel expectedSpannerLevel, bool async) { const string insertSql = "INSERT INTO my_table (name) VALUES ('X')"; Fixture.SpannerMock.AddOrUpdateStatementResult(insertSql, StatementResult.CreateUpdateCount(1L)); await using var conn = await OpenConnectionAsync(); - var tx = conn.BeginTransaction(level); + var tx = async ? await conn.BeginTransactionAsync(level) : conn.BeginTransaction(level); + var cmd = conn.CreateCommand("set local transaction_tag='test'"); + cmd.Transaction = tx; + await cmd.ExecuteNonQueryAsync(); await conn.ExecuteNonQueryAsync(insertSql, tx: tx); // TODO: Add support for this to the shared lib. // Assert.That(conn.ExecuteScalar("SHOW TRANSACTION ISOLATION LEVEL"), Is.EqualTo(expectedSpannerLevel.ToString())); - await tx.CommitAsync(); + if (async) + { + await tx.CommitAsync(); + } + else + { + tx.Commit(); + } var request = Fixture.SpannerMock.Requests.OfType().First(); Assert.That(request.Transaction.Begin.IsolationLevel, Is.EqualTo(expectedSpannerLevel)); diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/spanner-ado-net-tests.csproj b/drivers/spanner-ado-net/spanner-ado-net-tests/spanner-ado-net-tests.csproj index 31870103..2d7680bd 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/spanner-ado-net-tests.csproj +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/spanner-ado-net-tests.csproj @@ -10,6 +10,7 @@ true Google.Cloud.Spanner.DataProvider.Tests default + LatestMajor diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs index 4659b75d..1fa89cc2 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs @@ -133,8 +133,8 @@ public override async Task ExecuteNonQueryAsync(CancellationToken cancellat return 0; } var statements = CreateStatements(); - await SetRequestTagAsync(cancellationToken); - var results = await SpannerConnection.ExecuteBatchAsync(statements, cancellationToken); + await SetRequestTagAsync(cancellationToken).ConfigureAwait(false); + var results = await SpannerConnection.ExecuteBatchAsync(statements, cancellationToken).ConfigureAwait(false); DbBatchCommands.SetAffected(results); return (int) results.Sum(); } diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs index 39004818..69da7bc6 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs @@ -251,17 +251,17 @@ private Rows Execute(ExecuteSqlRequest.Types.QueryMode mode = ExecuteSqlRequest. private Task ExecuteAsync(CancellationToken cancellationToken) { - return ExecuteAsync(ExecuteSqlRequest.Types.QueryMode.Normal, cancellationToken); + return ExecuteAsync(ExecuteSqlRequest.Types.QueryMode.Normal, prefetchRows: 0, cancellationToken); } - private Task ExecuteAsync(ExecuteSqlRequest.Types.QueryMode mode, CancellationToken cancellationToken) + private Task ExecuteAsync(ExecuteSqlRequest.Types.QueryMode mode, int prefetchRows, CancellationToken cancellationToken) { CheckCommandStateForExecution(); if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } - return SpannerConnection.ExecuteAsync(BuildStatement(mode), cancellationToken); + return SpannerConnection.ExecuteAsync(BuildStatement(mode), prefetchRows, cancellationToken); } private void CheckCommandStateForExecution() @@ -290,12 +290,15 @@ public override async Task ExecuteNonQueryAsync(CancellationToken cancellat CheckDisposed(); if (_mutation != null) { - await ExecuteMutationAsync(cancellationToken); + await ExecuteMutationAsync(cancellationToken).ConfigureAwait(false); return 1; } - await using var rows = await ExecuteAsync(cancellationToken); - return (int) await rows.GetTotalUpdateCountAsync(cancellationToken); + var rows = await ExecuteAsync(cancellationToken).ConfigureAwait(false); + await using (rows.ConfigureAwait(false)) + { + return (int)await rows.GetTotalUpdateCountAsync(cancellationToken).ConfigureAwait(false); + } } public override object? ExecuteScalar() @@ -318,28 +321,35 @@ public override async Task ExecuteNonQueryAsync(CancellationToken cancellat { CheckDisposed(); GaxPreconditions.CheckState(_mutation == null, "Cannot execute mutations with ExecuteScalarAsync()"); - await using var rows = await ExecuteAsync(cancellationToken); - await using var reader = new SpannerDataReader(SpannerConnection, rows, CommandBehavior.Default); - if (await reader.ReadAsync(cancellationToken)) + var rows = await ExecuteAsync(cancellationToken).ConfigureAwait(false); + await using (rows.ConfigureAwait(false)) { - if (reader.FieldCount > 0) + var reader = new SpannerDataReader(SpannerConnection, rows, CommandBehavior.Default); + await using (reader.ConfigureAwait(false)) { - return reader.GetValue(0); + if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + if (reader.FieldCount > 0) + { + return reader.GetValue(0); + } + } } + return null; } - return null; } public override void Prepare() { CheckDisposed(); - Execute(ExecuteSqlRequest.Types.QueryMode.Plan); + using var rows = Execute(ExecuteSqlRequest.Types.QueryMode.Plan); } - public override Task PrepareAsync(CancellationToken cancellationToken = default) + public override async Task PrepareAsync(CancellationToken cancellationToken = default) { CheckDisposed(); - return ExecuteAsync(ExecuteSqlRequest.Types.QueryMode.Plan, cancellationToken); + var rows = await ExecuteAsync(ExecuteSqlRequest.Types.QueryMode.Plan, prefetchRows: 0, cancellationToken).ConfigureAwait(false); + await using (rows.ConfigureAwait(false)); } protected override DbParameter CreateDbParameter() @@ -388,14 +398,14 @@ protected override async Task ExecuteDbDataReaderAsync(CommandBeha var mode = behavior.HasFlag(CommandBehavior.SchemaOnly) ? ExecuteSqlRequest.Types.QueryMode.Plan : ExecuteSqlRequest.Types.QueryMode.Normal; - var rows = await ExecuteAsync(mode, cancellationToken); + var rows = await ExecuteAsync(mode, prefetchRows: 0, cancellationToken).ConfigureAwait(false); return new SpannerDataReader(SpannerConnection, rows, behavior); } catch (SpannerException exception) { if (behavior.HasFlag(CommandBehavior.CloseConnection)) { - await SpannerConnection.CloseAsync(); + await SpannerConnection.CloseAsync().ConfigureAwait(false); } throw new SpannerDbException(exception); } @@ -403,7 +413,7 @@ protected override async Task ExecuteDbDataReaderAsync(CommandBeha { if (behavior.HasFlag(CommandBehavior.CloseConnection)) { - await SpannerConnection.CloseAsync(); + await SpannerConnection.CloseAsync().ConfigureAwait(false); } throw; } diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs index bf181bc5..fee8bacb 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs @@ -17,6 +17,7 @@ using System.Data; using System.Data.Common; using System.Diagnostics.CodeAnalysis; +using System.Reflection.Metadata.Ecma335; using System.Threading; using System.Threading.Tasks; using Google.Api.Gax; @@ -31,8 +32,6 @@ namespace Google.Cloud.Spanner.DataProvider; public class SpannerConnection : DbConnection { - public bool UseSharedLibrary { get; set; } - private string _connectionString = string.Empty; private SpannerConnectionStringBuilder? _connectionStringBuilder; @@ -166,18 +165,10 @@ public SpannerConnection(SpannerConnectionStringBuilder connectionStringBuilder) { return ValueTask.FromCanceled(cancellationToken); } - - try - { - return new ValueTask(BeginTransaction(new TransactionOptions - { - IsolationLevel = SpannerTransaction.TranslateIsolationLevel(isolationLevel), - })); - } - catch (Exception e) + return BeginTransactionAsync(new TransactionOptions { - return ValueTask.FromException(e); - } + IsolationLevel = SpannerTransaction.TranslateIsolationLevel(isolationLevel), + }, cancellationToken); } public new SpannerTransaction BeginTransaction() => BeginTransaction(IsolationLevel.Unspecified); @@ -198,6 +189,14 @@ protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLeve }); } + protected override ValueTask BeginDbTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken) + { + return BeginDbTransactionAsync(new TransactionOptions + { + IsolationLevel = SpannerTransaction.TranslateIsolationLevel(isolationLevel), + }, cancellationToken); + } + /// /// Starts a new read-only transaction with default options. /// @@ -235,7 +234,30 @@ private SpannerTransaction BeginTransaction(TransactionOptions transactionOption { EnsureOpen(); GaxPreconditions.CheckState(!HasTransaction, "This connection has a transaction."); - _transaction = new SpannerTransaction(this, LibConnection, transactionOptions); + _transaction = SpannerTransaction.CreateTransaction(this, LibConnection, transactionOptions); + return _transaction; + } + + /// + /// Start a new transaction using the given TransactionOptions. + /// + /// The options to use for the new transaction + /// The cancellation token + /// The new transaction + /// If the connection has an active transaction + private async ValueTask BeginTransactionAsync(TransactionOptions transactionOptions, CancellationToken cancellationToken) + { + EnsureOpen(); + GaxPreconditions.CheckState(!HasTransaction, "This connection has a transaction."); + _transaction = await SpannerTransaction.CreateTransactionAsync(this, LibConnection, transactionOptions, cancellationToken).ConfigureAwait(false); + return _transaction; + } + + private async ValueTask BeginDbTransactionAsync(TransactionOptions transactionOptions, CancellationToken cancellationToken) + { + EnsureOpen(); + GaxPreconditions.CheckState(!HasTransaction, "This connection has a transaction."); + _transaction = await SpannerTransaction.CreateTransactionAsync(this, LibConnection, transactionOptions, cancellationToken).ConfigureAwait(false); return _transaction; } @@ -387,18 +409,18 @@ protected override DbCommand CreateDbCommand() return cmd; } - public Rows Execute(ExecuteSqlRequest statement) + public Rows Execute(ExecuteSqlRequest statement, int prefetchRows = 0) { EnsureOpen(); _transaction?.MarkUsed(); - return TranslateException(() => LibConnection.Execute(statement)); + return TranslateException(() => LibConnection.Execute(statement, prefetchRows)); } - public Task ExecuteAsync(ExecuteSqlRequest statement, CancellationToken cancellationToken = default) + public Task ExecuteAsync(ExecuteSqlRequest statement, int prefetchRows = 0, CancellationToken cancellationToken = default) { EnsureOpen(); _transaction?.MarkUsed(); - return TranslateException(LibConnection.ExecuteAsync(statement, cancellationToken)); + return TranslateException(LibConnection.ExecuteAsync(statement, prefetchRows, cancellationToken)); } public new SpannerBatch CreateBatch() => (SpannerBatch) base.CreateBatch(); diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs index f39084b4..54273ce2 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs @@ -19,7 +19,6 @@ using System.Data.Common; using System.Globalization; using System.IO; -using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -154,7 +153,7 @@ public override async Task ReadAsync(CancellationToken cancellationToken) if (!InternalRead()) { _hasReadData = true; - _currentRow = await LibRows.NextAsync(cancellationToken); + _currentRow = await LibRows.NextAsync(cancellationToken).ConfigureAwait(false); } _hasData = _hasData || _currentRow != null; return _currentRow != null; diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerPool.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerPool.cs index 6d401971..e9ec2550 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerPool.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerPool.cs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -23,13 +24,19 @@ namespace Google.Cloud.Spanner.DataProvider; internal class SpannerPool { + private static bool UseNativeLib => bool.Parse(Environment.GetEnvironmentVariable("SPANNER_ADO_USE_NATIVE_LIB") ?? "false"); + + private static int NumChannels => int.Parse(Environment.GetEnvironmentVariable("SPANNER_ADO_NUM_CHANNELS") ?? "4"); + + private static GrpcLibSpanner.CommunicationStyle CommunicationStyle => Enum.Parse(Environment.GetEnvironmentVariable("SPANNER_ADO_COMMUNICATION_STYLE") ?? nameof(GrpcLibSpanner.CommunicationStyle.BidiStreaming)); + private static ISpannerLib? _gRpcSpannerLib; private static ISpannerLib GrpcSpannerLib { get { - _gRpcSpannerLib ??= new GrpcLibSpanner(); + _gRpcSpannerLib ??= new GrpcLibSpanner(numChannels: NumChannels, communicationStyle: CommunicationStyle); return _gRpcSpannerLib; } } @@ -48,13 +55,13 @@ private static ISpannerLib NativeSpannerLib private static readonly ConcurrentDictionary Pools = new(); [MethodImpl(MethodImplOptions.Synchronized)] - internal static SpannerPool GetOrCreate(string dsn, bool useNativeLibrary = false) + internal static SpannerPool GetOrCreate(string dsn) { if (Pools.TryGetValue(dsn, out var value)) { return value; } - var pool = Pool.Create(useNativeLibrary ? NativeSpannerLib : GrpcSpannerLib, dsn); + var pool = Pool.Create(UseNativeLib ? NativeSpannerLib : GrpcSpannerLib, dsn); var spannerPool = new SpannerPool(dsn, pool); Pools[dsn] = spannerPool; return spannerPool; diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs index cc6c71b3..b4584f05 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerTransaction.cs @@ -60,13 +60,30 @@ public string? Tag private bool _disposed; - internal SpannerTransaction(SpannerConnection connection, SpannerLib.Connection libConnection, TransactionOptions options) + internal static SpannerTransaction CreateTransaction( + SpannerConnection connection, SpannerLib.Connection libConnection, TransactionOptions options) + { + // This call to BeginTransaction does not trigger an RPC. It only registers the transaction on the connection. + libConnection.BeginTransaction(options); + return new SpannerTransaction(connection, libConnection, options); + } + + internal static async ValueTask CreateTransactionAsync( + SpannerConnection connection, + SpannerLib.Connection libConnection, + TransactionOptions options, + CancellationToken cancellationToken) + { + // This call to BeginTransaction does not trigger an RPC. It only registers the transaction on the connection. + await libConnection.BeginTransactionAsync(options, cancellationToken).ConfigureAwait(false); + return new SpannerTransaction(connection, libConnection, options); + } + + private SpannerTransaction(SpannerConnection connection, SpannerLib.Connection libConnection, TransactionOptions options) { _spannerConnection = connection; IsolationLevel = TranslateIsolationLevel(options.IsolationLevel); LibConnection = libConnection; - // This call to BeginTransaction does not trigger an RPC. It only registers the transaction on the connection. - LibConnection.BeginTransaction(options); } internal static TransactionOptions.Types.IsolationLevel TranslateIsolationLevel(IsolationLevel isolationLevel) @@ -109,21 +126,19 @@ protected override void Dispose(bool disposing) if (!IsCompleted) { // Do a shoot-and-forget rollback. - RollbackAsync(CancellationToken.None); + Rollback(); } _disposed = true; - base.Dispose(disposing); } - public override ValueTask DisposeAsync() + public override async ValueTask DisposeAsync() { if (!IsCompleted) { // Do a shoot-and-forget rollback. - RollbackAsync(CancellationToken.None); + await RollbackAsync(CancellationToken.None).ConfigureAwait(false); } _disposed = true; - return base.DisposeAsync(); } private void CheckDisposed() diff --git a/drivers/spanner-ado-net/spanner-ado-net/publish.sh b/drivers/spanner-ado-net/spanner-ado-net/publish.sh old mode 100644 new mode 100755 diff --git a/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj b/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj index 8d70844c..ac614072 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj +++ b/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj @@ -9,7 +9,7 @@ Alpha.Google.Cloud.Spanner.DataProvider .NET Data Provider for Spanner Google - 1.0.0-alpha.20251003170157 + 1.0.0-alpha.20251207191847 ADO.NET Data Provider. Alpha version: Not for production use @@ -18,8 +18,20 @@ Alpha version: Not for production use https://github.com/googleapis/go-sql-spanner/drivers/spanner-ado-net + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/spannerlib/api/connection.go b/spannerlib/api/connection.go index 63035294..c624c9b7 100644 --- a/spannerlib/api/connection.go +++ b/spannerlib/api/connection.go @@ -35,6 +35,9 @@ import ( func CloseConnection(ctx context.Context, poolId, connId int64) error { pool, err := findPool(poolId) if err != nil { + if status.Code(err) == codes.NotFound { + return nil + } return err } c, ok := pool.connections.LoadAndDelete(connId) diff --git a/spannerlib/api/rows.go b/spannerlib/api/rows.go index 4e18a7ce..e9956742 100644 --- a/spannerlib/api/rows.go +++ b/spannerlib/api/rows.go @@ -100,6 +100,7 @@ func next(ctx context.Context, poolId, connId, rowsId int64, marshalResult bool) return nil, nil, err } if !marshalResult || values == nil { + rows.buffer = nil return values, nil, nil } @@ -115,6 +116,9 @@ func next(ctx context.Context, poolId, connId, rowsId int64, marshalResult bool) func CloseRows(ctx context.Context, poolId, connId, rowsId int64) error { conn, err := findConnection(poolId, connId) if err != nil { + if status.Code(err) == codes.NotFound { + return nil + } return err } r, ok := conn.results.LoadAndDelete(rowsId) diff --git a/spannerlib/grpc-server/google/spannerlib/v1/spannerlib.pb.go b/spannerlib/grpc-server/google/spannerlib/v1/spannerlib.pb.go index 9e74c5fe..727634d4 100644 --- a/spannerlib/grpc-server/google/spannerlib/v1/spannerlib.pb.go +++ b/spannerlib/grpc-server/google/spannerlib/v1/spannerlib.pb.go @@ -9,6 +9,7 @@ package spannerlibpb import ( spannerpb "cloud.google.com/go/spanner/apiv1/spannerpb" _ "google.golang.org/genproto/googleapis/api/annotations" + status "google.golang.org/genproto/googleapis/rpc/status" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" emptypb "google.golang.org/protobuf/types/known/emptypb" @@ -197,6 +198,7 @@ type ExecuteRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Connection *Connection `protobuf:"bytes,1,opt,name=connection,proto3" json:"connection,omitempty"` ExecuteSqlRequest *spannerpb.ExecuteSqlRequest `protobuf:"bytes,2,opt,name=execute_sql_request,json=executeSqlRequest,proto3" json:"execute_sql_request,omitempty"` + FetchOptions *FetchOptions `protobuf:"bytes,3,opt,name=fetch_options,json=fetchOptions,proto3" json:"fetch_options,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -245,6 +247,13 @@ func (x *ExecuteRequest) GetExecuteSqlRequest() *spannerpb.ExecuteSqlRequest { return nil } +func (x *ExecuteRequest) GetFetchOptions() *FetchOptions { + if x != nil { + return x.FetchOptions + } + return nil +} + type ExecuteBatchRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Connection *Connection `protobuf:"bytes,1,opt,name=connection,proto3" json:"connection,omitempty"` @@ -552,8 +561,7 @@ func (x *Rows) GetId() int64 { type NextRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Rows *Rows `protobuf:"bytes,1,opt,name=rows,proto3" json:"rows,omitempty"` - NumRows int64 `protobuf:"varint,2,opt,name=num_rows,json=numRows,proto3" json:"num_rows,omitempty"` - Encoding int64 `protobuf:"varint,3,opt,name=encoding,proto3" json:"encoding,omitempty"` + FetchOptions *FetchOptions `protobuf:"bytes,2,opt,name=fetch_options,json=fetchOptions,proto3" json:"fetch_options,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -595,14 +603,59 @@ func (x *NextRequest) GetRows() *Rows { return nil } -func (x *NextRequest) GetNumRows() int64 { +func (x *NextRequest) GetFetchOptions() *FetchOptions { + if x != nil { + return x.FetchOptions + } + return nil +} + +type FetchOptions struct { + state protoimpl.MessageState `protogen:"open.v1"` + NumRows int64 `protobuf:"varint,1,opt,name=num_rows,json=numRows,proto3" json:"num_rows,omitempty"` + Encoding int64 `protobuf:"varint,2,opt,name=encoding,proto3" json:"encoding,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FetchOptions) Reset() { + *x = FetchOptions{} + mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FetchOptions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FetchOptions) ProtoMessage() {} + +func (x *FetchOptions) ProtoReflect() protoreflect.Message { + mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FetchOptions.ProtoReflect.Descriptor instead. +func (*FetchOptions) Descriptor() ([]byte, []int) { + return file_google_spannerlib_v1_spannerlib_proto_rawDescGZIP(), []int{12} +} + +func (x *FetchOptions) GetNumRows() int64 { if x != nil { return x.NumRows } return 0 } -func (x *NextRequest) GetEncoding() int64 { +func (x *FetchOptions) GetEncoding() int64 { if x != nil { return x.Encoding } @@ -611,18 +664,19 @@ func (x *NextRequest) GetEncoding() int64 { type RowData struct { state protoimpl.MessageState `protogen:"open.v1"` - Rows *Rows `protobuf:"bytes,1,opt,name=rows,proto3" json:"rows,omitempty"` - Metadata *spannerpb.ResultSetMetadata `protobuf:"bytes,2,opt,name=metadata,proto3" json:"metadata,omitempty"` - Data []*structpb.ListValue `protobuf:"bytes,3,rep,name=data,proto3" json:"data,omitempty"` - Stats *spannerpb.ResultSetStats `protobuf:"bytes,4,opt,name=stats,proto3" json:"stats,omitempty"` - HasMoreResults bool `protobuf:"varint,5,opt,name=has_more_results,json=hasMoreResults,proto3" json:"has_more_results,omitempty"` + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + Rows *Rows `protobuf:"bytes,2,opt,name=rows,proto3" json:"rows,omitempty"` + Metadata *spannerpb.ResultSetMetadata `protobuf:"bytes,3,opt,name=metadata,proto3" json:"metadata,omitempty"` + Data []*structpb.ListValue `protobuf:"bytes,4,rep,name=data,proto3" json:"data,omitempty"` + Stats *spannerpb.ResultSetStats `protobuf:"bytes,5,opt,name=stats,proto3" json:"stats,omitempty"` + HasMoreResults bool `protobuf:"varint,6,opt,name=has_more_results,json=hasMoreResults,proto3" json:"has_more_results,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RowData) Reset() { *x = RowData{} - mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[12] + mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -634,7 +688,7 @@ func (x *RowData) String() string { func (*RowData) ProtoMessage() {} func (x *RowData) ProtoReflect() protoreflect.Message { - mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[12] + mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -647,7 +701,14 @@ func (x *RowData) ProtoReflect() protoreflect.Message { // Deprecated: Use RowData.ProtoReflect.Descriptor instead. func (*RowData) Descriptor() ([]byte, []int) { - return file_google_spannerlib_v1_spannerlib_proto_rawDescGZIP(), []int{12} + return file_google_spannerlib_v1_spannerlib_proto_rawDescGZIP(), []int{13} +} + +func (x *RowData) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" } func (x *RowData) GetRows() *Rows { @@ -694,7 +755,7 @@ type MetadataRequest struct { func (x *MetadataRequest) Reset() { *x = MetadataRequest{} - mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[13] + mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -706,7 +767,7 @@ func (x *MetadataRequest) String() string { func (*MetadataRequest) ProtoMessage() {} func (x *MetadataRequest) ProtoReflect() protoreflect.Message { - mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[13] + mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -719,7 +780,7 @@ func (x *MetadataRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use MetadataRequest.ProtoReflect.Descriptor instead. func (*MetadataRequest) Descriptor() ([]byte, []int) { - return file_google_spannerlib_v1_spannerlib_proto_rawDescGZIP(), []int{13} + return file_google_spannerlib_v1_spannerlib_proto_rawDescGZIP(), []int{14} } func (x *MetadataRequest) GetRows() *Rows { @@ -738,7 +799,7 @@ type ResultSetStatsRequest struct { func (x *ResultSetStatsRequest) Reset() { *x = ResultSetStatsRequest{} - mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[14] + mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -750,7 +811,7 @@ func (x *ResultSetStatsRequest) String() string { func (*ResultSetStatsRequest) ProtoMessage() {} func (x *ResultSetStatsRequest) ProtoReflect() protoreflect.Message { - mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[14] + mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -763,7 +824,7 @@ func (x *ResultSetStatsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResultSetStatsRequest.ProtoReflect.Descriptor instead. func (*ResultSetStatsRequest) Descriptor() ([]byte, []int) { - return file_google_spannerlib_v1_spannerlib_proto_rawDescGZIP(), []int{14} + return file_google_spannerlib_v1_spannerlib_proto_rawDescGZIP(), []int{15} } func (x *ResultSetStatsRequest) GetRows() *Rows { @@ -778,6 +839,11 @@ type ConnectionStreamRequest struct { // Types that are valid to be assigned to Request: // // *ConnectionStreamRequest_ExecuteRequest + // *ConnectionStreamRequest_ExecuteBatchRequest + // *ConnectionStreamRequest_BeginTransactionRequest + // *ConnectionStreamRequest_CommitRequest + // *ConnectionStreamRequest_RollbackRequest + // *ConnectionStreamRequest_WriteMutationsRequest Request isConnectionStreamRequest_Request `protobuf_oneof:"request"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -785,7 +851,7 @@ type ConnectionStreamRequest struct { func (x *ConnectionStreamRequest) Reset() { *x = ConnectionStreamRequest{} - mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[15] + mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -797,7 +863,7 @@ func (x *ConnectionStreamRequest) String() string { func (*ConnectionStreamRequest) ProtoMessage() {} func (x *ConnectionStreamRequest) ProtoReflect() protoreflect.Message { - mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[15] + mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -810,7 +876,7 @@ func (x *ConnectionStreamRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ConnectionStreamRequest.ProtoReflect.Descriptor instead. func (*ConnectionStreamRequest) Descriptor() ([]byte, []int) { - return file_google_spannerlib_v1_spannerlib_proto_rawDescGZIP(), []int{15} + return file_google_spannerlib_v1_spannerlib_proto_rawDescGZIP(), []int{16} } func (x *ConnectionStreamRequest) GetRequest() isConnectionStreamRequest_Request { @@ -829,6 +895,51 @@ func (x *ConnectionStreamRequest) GetExecuteRequest() *ExecuteRequest { return nil } +func (x *ConnectionStreamRequest) GetExecuteBatchRequest() *ExecuteBatchRequest { + if x != nil { + if x, ok := x.Request.(*ConnectionStreamRequest_ExecuteBatchRequest); ok { + return x.ExecuteBatchRequest + } + } + return nil +} + +func (x *ConnectionStreamRequest) GetBeginTransactionRequest() *BeginTransactionRequest { + if x != nil { + if x, ok := x.Request.(*ConnectionStreamRequest_BeginTransactionRequest); ok { + return x.BeginTransactionRequest + } + } + return nil +} + +func (x *ConnectionStreamRequest) GetCommitRequest() *Connection { + if x != nil { + if x, ok := x.Request.(*ConnectionStreamRequest_CommitRequest); ok { + return x.CommitRequest + } + } + return nil +} + +func (x *ConnectionStreamRequest) GetRollbackRequest() *Connection { + if x != nil { + if x, ok := x.Request.(*ConnectionStreamRequest_RollbackRequest); ok { + return x.RollbackRequest + } + } + return nil +} + +func (x *ConnectionStreamRequest) GetWriteMutationsRequest() *WriteMutationsRequest { + if x != nil { + if x, ok := x.Request.(*ConnectionStreamRequest_WriteMutationsRequest); ok { + return x.WriteMutationsRequest + } + } + return nil +} + type isConnectionStreamRequest_Request interface { isConnectionStreamRequest_Request() } @@ -837,13 +948,117 @@ type ConnectionStreamRequest_ExecuteRequest struct { ExecuteRequest *ExecuteRequest `protobuf:"bytes,1,opt,name=execute_request,json=executeRequest,proto3,oneof"` } +type ConnectionStreamRequest_ExecuteBatchRequest struct { + ExecuteBatchRequest *ExecuteBatchRequest `protobuf:"bytes,2,opt,name=execute_batch_request,json=executeBatchRequest,proto3,oneof"` +} + +type ConnectionStreamRequest_BeginTransactionRequest struct { + BeginTransactionRequest *BeginTransactionRequest `protobuf:"bytes,3,opt,name=begin_transaction_request,json=beginTransactionRequest,proto3,oneof"` +} + +type ConnectionStreamRequest_CommitRequest struct { + CommitRequest *Connection `protobuf:"bytes,4,opt,name=commit_request,json=commitRequest,proto3,oneof"` +} + +type ConnectionStreamRequest_RollbackRequest struct { + RollbackRequest *Connection `protobuf:"bytes,5,opt,name=rollback_request,json=rollbackRequest,proto3,oneof"` +} + +type ConnectionStreamRequest_WriteMutationsRequest struct { + WriteMutationsRequest *WriteMutationsRequest `protobuf:"bytes,6,opt,name=write_mutations_request,json=writeMutationsRequest,proto3,oneof"` +} + func (*ConnectionStreamRequest_ExecuteRequest) isConnectionStreamRequest_Request() {} +func (*ConnectionStreamRequest_ExecuteBatchRequest) isConnectionStreamRequest_Request() {} + +func (*ConnectionStreamRequest_BeginTransactionRequest) isConnectionStreamRequest_Request() {} + +func (*ConnectionStreamRequest_CommitRequest) isConnectionStreamRequest_Request() {} + +func (*ConnectionStreamRequest_RollbackRequest) isConnectionStreamRequest_Request() {} + +func (*ConnectionStreamRequest_WriteMutationsRequest) isConnectionStreamRequest_Request() {} + +type ExecuteResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rows *Rows `protobuf:"bytes,1,opt,name=rows,proto3" json:"rows,omitempty"` + ResultSets []*spannerpb.ResultSet `protobuf:"bytes,2,rep,name=result_sets,json=resultSets,proto3" json:"result_sets,omitempty"` + Status *status.Status `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` + HasMoreResults bool `protobuf:"varint,4,opt,name=has_more_results,json=hasMoreResults,proto3" json:"has_more_results,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecuteResponse) Reset() { + *x = ExecuteResponse{} + mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecuteResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecuteResponse) ProtoMessage() {} + +func (x *ExecuteResponse) ProtoReflect() protoreflect.Message { + mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecuteResponse.ProtoReflect.Descriptor instead. +func (*ExecuteResponse) Descriptor() ([]byte, []int) { + return file_google_spannerlib_v1_spannerlib_proto_rawDescGZIP(), []int{17} +} + +func (x *ExecuteResponse) GetRows() *Rows { + if x != nil { + return x.Rows + } + return nil +} + +func (x *ExecuteResponse) GetResultSets() []*spannerpb.ResultSet { + if x != nil { + return x.ResultSets + } + return nil +} + +func (x *ExecuteResponse) GetStatus() *status.Status { + if x != nil { + return x.Status + } + return nil +} + +func (x *ExecuteResponse) GetHasMoreResults() bool { + if x != nil { + return x.HasMoreResults + } + return false +} + type ConnectionStreamResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` + state protoimpl.MessageState `protogen:"open.v1"` + Status *status.Status `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` // Types that are valid to be assigned to Response: // - // *ConnectionStreamResponse_Row + // *ConnectionStreamResponse_ExecuteResponse + // *ConnectionStreamResponse_ExecuteBatchResponse + // *ConnectionStreamResponse_BeginTransactionResponse + // *ConnectionStreamResponse_CommitResponse + // *ConnectionStreamResponse_RollbackResponse + // *ConnectionStreamResponse_WriteMutationsResponse Response isConnectionStreamResponse_Response `protobuf_oneof:"response"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -851,7 +1066,7 @@ type ConnectionStreamResponse struct { func (x *ConnectionStreamResponse) Reset() { *x = ConnectionStreamResponse{} - mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[16] + mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -863,7 +1078,7 @@ func (x *ConnectionStreamResponse) String() string { func (*ConnectionStreamResponse) ProtoMessage() {} func (x *ConnectionStreamResponse) ProtoReflect() protoreflect.Message { - mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[16] + mi := &file_google_spannerlib_v1_spannerlib_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -876,7 +1091,14 @@ func (x *ConnectionStreamResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ConnectionStreamResponse.ProtoReflect.Descriptor instead. func (*ConnectionStreamResponse) Descriptor() ([]byte, []int) { - return file_google_spannerlib_v1_spannerlib_proto_rawDescGZIP(), []int{16} + return file_google_spannerlib_v1_spannerlib_proto_rawDescGZIP(), []int{18} +} + +func (x *ConnectionStreamResponse) GetStatus() *status.Status { + if x != nil { + return x.Status + } + return nil } func (x *ConnectionStreamResponse) GetResponse() isConnectionStreamResponse_Response { @@ -886,10 +1108,55 @@ func (x *ConnectionStreamResponse) GetResponse() isConnectionStreamResponse_Resp return nil } -func (x *ConnectionStreamResponse) GetRow() *spannerpb.PartialResultSet { +func (x *ConnectionStreamResponse) GetExecuteResponse() *ExecuteResponse { + if x != nil { + if x, ok := x.Response.(*ConnectionStreamResponse_ExecuteResponse); ok { + return x.ExecuteResponse + } + } + return nil +} + +func (x *ConnectionStreamResponse) GetExecuteBatchResponse() *spannerpb.ExecuteBatchDmlResponse { if x != nil { - if x, ok := x.Response.(*ConnectionStreamResponse_Row); ok { - return x.Row + if x, ok := x.Response.(*ConnectionStreamResponse_ExecuteBatchResponse); ok { + return x.ExecuteBatchResponse + } + } + return nil +} + +func (x *ConnectionStreamResponse) GetBeginTransactionResponse() *emptypb.Empty { + if x != nil { + if x, ok := x.Response.(*ConnectionStreamResponse_BeginTransactionResponse); ok { + return x.BeginTransactionResponse + } + } + return nil +} + +func (x *ConnectionStreamResponse) GetCommitResponse() *spannerpb.CommitResponse { + if x != nil { + if x, ok := x.Response.(*ConnectionStreamResponse_CommitResponse); ok { + return x.CommitResponse + } + } + return nil +} + +func (x *ConnectionStreamResponse) GetRollbackResponse() *emptypb.Empty { + if x != nil { + if x, ok := x.Response.(*ConnectionStreamResponse_RollbackResponse); ok { + return x.RollbackResponse + } + } + return nil +} + +func (x *ConnectionStreamResponse) GetWriteMutationsResponse() *spannerpb.CommitResponse { + if x != nil { + if x, ok := x.Response.(*ConnectionStreamResponse_WriteMutationsResponse); ok { + return x.WriteMutationsResponse } } return nil @@ -899,29 +1166,60 @@ type isConnectionStreamResponse_Response interface { isConnectionStreamResponse_Response() } -type ConnectionStreamResponse_Row struct { - Row *spannerpb.PartialResultSet `protobuf:"bytes,1,opt,name=row,proto3,oneof"` +type ConnectionStreamResponse_ExecuteResponse struct { + ExecuteResponse *ExecuteResponse `protobuf:"bytes,2,opt,name=execute_response,json=executeResponse,proto3,oneof"` +} + +type ConnectionStreamResponse_ExecuteBatchResponse struct { + ExecuteBatchResponse *spannerpb.ExecuteBatchDmlResponse `protobuf:"bytes,3,opt,name=execute_batch_response,json=executeBatchResponse,proto3,oneof"` +} + +type ConnectionStreamResponse_BeginTransactionResponse struct { + BeginTransactionResponse *emptypb.Empty `protobuf:"bytes,4,opt,name=begin_transaction_response,json=beginTransactionResponse,proto3,oneof"` +} + +type ConnectionStreamResponse_CommitResponse struct { + CommitResponse *spannerpb.CommitResponse `protobuf:"bytes,5,opt,name=commit_response,json=commitResponse,proto3,oneof"` +} + +type ConnectionStreamResponse_RollbackResponse struct { + RollbackResponse *emptypb.Empty `protobuf:"bytes,6,opt,name=rollback_response,json=rollbackResponse,proto3,oneof"` } -func (*ConnectionStreamResponse_Row) isConnectionStreamResponse_Response() {} +type ConnectionStreamResponse_WriteMutationsResponse struct { + WriteMutationsResponse *spannerpb.CommitResponse `protobuf:"bytes,7,opt,name=write_mutations_response,json=writeMutationsResponse,proto3,oneof"` +} + +func (*ConnectionStreamResponse_ExecuteResponse) isConnectionStreamResponse_Response() {} + +func (*ConnectionStreamResponse_ExecuteBatchResponse) isConnectionStreamResponse_Response() {} + +func (*ConnectionStreamResponse_BeginTransactionResponse) isConnectionStreamResponse_Response() {} + +func (*ConnectionStreamResponse_CommitResponse) isConnectionStreamResponse_Response() {} + +func (*ConnectionStreamResponse_RollbackResponse) isConnectionStreamResponse_Response() {} + +func (*ConnectionStreamResponse_WriteMutationsResponse) isConnectionStreamResponse_Response() {} var File_google_spannerlib_v1_spannerlib_proto protoreflect.FileDescriptor const file_google_spannerlib_v1_spannerlib_proto_rawDesc = "" + "\n" + - "%google/spannerlib/v1/spannerlib.proto\x12\x14google.spannerlib.v1\x1a\x1fgoogle/api/field_behavior.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\"google/spanner/v1/result_set.proto\x1a\x1fgoogle/spanner/v1/spanner.proto\x1a#google/spanner/v1/transaction.proto\"\r\n" + + "%google/spannerlib/v1/spannerlib.proto\x12\x14google.spannerlib.v1\x1a\x1fgoogle/api/field_behavior.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x17google/rpc/status.proto\x1a\"google/spanner/v1/result_set.proto\x1a\x1fgoogle/spanner/v1/spanner.proto\x1a#google/spanner/v1/transaction.proto\"\r\n" + "\vInfoRequest\"(\n" + "\fInfoResponse\x12\x18\n" + "\aversion\x18\x01 \x01(\tR\aversion\"E\n" + "\x11CreatePoolRequest\x120\n" + "\x11connection_string\x18\x01 \x01(\tB\x03\xe0A\x02R\x10connectionString\"N\n" + "\x17CreateConnectionRequest\x123\n" + - "\x04pool\x18\x01 \x01(\v2\x1a.google.spannerlib.v1.PoolB\x03\xe0A\x02R\x04pool\"\xb2\x01\n" + + "\x04pool\x18\x01 \x01(\v2\x1a.google.spannerlib.v1.PoolB\x03\xe0A\x02R\x04pool\"\xfb\x01\n" + "\x0eExecuteRequest\x12E\n" + "\n" + "connection\x18\x01 \x01(\v2 .google.spannerlib.v1.ConnectionB\x03\xe0A\x02R\n" + "connection\x12Y\n" + - "\x13execute_sql_request\x18\x02 \x01(\v2$.google.spanner.v1.ExecuteSqlRequestB\x03\xe0A\x02R\x11executeSqlRequest\"\xc7\x01\n" + + "\x13execute_sql_request\x18\x02 \x01(\v2$.google.spanner.v1.ExecuteSqlRequestB\x03\xe0A\x02R\x11executeSqlRequest\x12G\n" + + "\rfetch_options\x18\x03 \x01(\v2\".google.spannerlib.v1.FetchOptionsR\ffetchOptions\"\xc7\x01\n" + "\x13ExecuteBatchRequest\x12E\n" + "\n" + "connection\x18\x01 \x01(\v2 .google.spannerlib.v1.ConnectionB\x03\xe0A\x02R\n" + @@ -947,28 +1245,49 @@ const file_google_spannerlib_v1_spannerlib_proto_rawDesc = "" + "\n" + "connection\x18\x01 \x01(\v2 .google.spannerlib.v1.ConnectionB\x03\xe0A\x02R\n" + "connection\x12\x13\n" + - "\x02id\x18\x02 \x01(\x03B\x03\xe0A\x02R\x02id\"\x83\x01\n" + + "\x02id\x18\x02 \x01(\x03B\x03\xe0A\x02R\x02id\"\x90\x01\n" + "\vNextRequest\x123\n" + - "\x04rows\x18\x01 \x01(\v2\x1a.google.spannerlib.v1.RowsB\x03\xe0A\x02R\x04rows\x12\x1e\n" + - "\bnum_rows\x18\x02 \x01(\x03B\x03\xe0A\x02R\anumRows\x12\x1f\n" + - "\bencoding\x18\x03 \x01(\x03B\x03\xe0A\x02R\bencoding\"\x98\x02\n" + - "\aRowData\x123\n" + - "\x04rows\x18\x01 \x01(\v2\x1a.google.spannerlib.v1.RowsB\x03\xe0A\x02R\x04rows\x12@\n" + - "\bmetadata\x18\x02 \x01(\v2$.google.spanner.v1.ResultSetMetadataR\bmetadata\x123\n" + - "\x04data\x18\x03 \x03(\v2\x1a.google.protobuf.ListValueB\x03\xe0A\x02R\x04data\x127\n" + - "\x05stats\x18\x04 \x01(\v2!.google.spanner.v1.ResultSetStatsR\x05stats\x12(\n" + - "\x10has_more_results\x18\x05 \x01(\bR\x0ehasMoreResults\"F\n" + + "\x04rows\x18\x01 \x01(\v2\x1a.google.spannerlib.v1.RowsB\x03\xe0A\x02R\x04rows\x12L\n" + + "\rfetch_options\x18\x02 \x01(\v2\".google.spannerlib.v1.FetchOptionsB\x03\xe0A\x02R\ffetchOptions\"O\n" + + "\fFetchOptions\x12\x1e\n" + + "\bnum_rows\x18\x01 \x01(\x03B\x03\xe0A\x02R\anumRows\x12\x1f\n" + + "\bencoding\x18\x02 \x01(\x03B\x03\xe0A\x02R\bencoding\"\xb7\x02\n" + + "\aRowData\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x123\n" + + "\x04rows\x18\x02 \x01(\v2\x1a.google.spannerlib.v1.RowsB\x03\xe0A\x02R\x04rows\x12@\n" + + "\bmetadata\x18\x03 \x01(\v2$.google.spanner.v1.ResultSetMetadataR\bmetadata\x123\n" + + "\x04data\x18\x04 \x03(\v2\x1a.google.protobuf.ListValueB\x03\xe0A\x02R\x04data\x127\n" + + "\x05stats\x18\x05 \x01(\v2!.google.spanner.v1.ResultSetStatsR\x05stats\x12(\n" + + "\x10has_more_results\x18\x06 \x01(\bR\x0ehasMoreResults\"F\n" + "\x0fMetadataRequest\x123\n" + "\x04rows\x18\x01 \x01(\v2\x1a.google.spannerlib.v1.RowsB\x03\xe0A\x02R\x04rows\"L\n" + "\x15ResultSetStatsRequest\x123\n" + - "\x04rows\x18\x01 \x01(\v2\x1a.google.spannerlib.v1.RowsB\x03\xe0A\x02R\x04rows\"u\n" + + "\x04rows\x18\x01 \x01(\v2\x1a.google.spannerlib.v1.RowsB\x03\xe0A\x02R\x04rows\"\xc4\x04\n" + "\x17ConnectionStreamRequest\x12O\n" + - "\x0fexecute_request\x18\x01 \x01(\v2$.google.spannerlib.v1.ExecuteRequestH\x00R\x0eexecuteRequestB\t\n" + - "\arequest\"_\n" + - "\x18ConnectionStreamResponse\x127\n" + - "\x03row\x18\x01 \x01(\v2#.google.spanner.v1.PartialResultSetH\x00R\x03rowB\n" + + "\x0fexecute_request\x18\x01 \x01(\v2$.google.spannerlib.v1.ExecuteRequestH\x00R\x0eexecuteRequest\x12_\n" + + "\x15execute_batch_request\x18\x02 \x01(\v2).google.spannerlib.v1.ExecuteBatchRequestH\x00R\x13executeBatchRequest\x12k\n" + + "\x19begin_transaction_request\x18\x03 \x01(\v2-.google.spannerlib.v1.BeginTransactionRequestH\x00R\x17beginTransactionRequest\x12I\n" + + "\x0ecommit_request\x18\x04 \x01(\v2 .google.spannerlib.v1.ConnectionH\x00R\rcommitRequest\x12M\n" + + "\x10rollback_request\x18\x05 \x01(\v2 .google.spannerlib.v1.ConnectionH\x00R\x0frollbackRequest\x12e\n" + + "\x17write_mutations_request\x18\x06 \x01(\v2+.google.spannerlib.v1.WriteMutationsRequestH\x00R\x15writeMutationsRequestB\t\n" + + "\arequest\"\xdb\x01\n" + + "\x0fExecuteResponse\x123\n" + + "\x04rows\x18\x01 \x01(\v2\x1a.google.spannerlib.v1.RowsB\x03\xe0A\x02R\x04rows\x12=\n" + + "\vresult_sets\x18\x02 \x03(\v2\x1c.google.spanner.v1.ResultSetR\n" + + "resultSets\x12*\n" + + "\x06status\x18\x03 \x01(\v2\x12.google.rpc.StatusR\x06status\x12(\n" + + "\x10has_more_results\x18\x04 \x01(\bR\x0ehasMoreResults\"\xd6\x04\n" + + "\x18ConnectionStreamResponse\x12*\n" + + "\x06status\x18\x01 \x01(\v2\x12.google.rpc.StatusR\x06status\x12R\n" + + "\x10execute_response\x18\x02 \x01(\v2%.google.spannerlib.v1.ExecuteResponseH\x00R\x0fexecuteResponse\x12b\n" + + "\x16execute_batch_response\x18\x03 \x01(\v2*.google.spanner.v1.ExecuteBatchDmlResponseH\x00R\x14executeBatchResponse\x12V\n" + + "\x1abegin_transaction_response\x18\x04 \x01(\v2\x16.google.protobuf.EmptyH\x00R\x18beginTransactionResponse\x12L\n" + + "\x0fcommit_response\x18\x05 \x01(\v2!.google.spanner.v1.CommitResponseH\x00R\x0ecommitResponse\x12E\n" + + "\x11rollback_response\x18\x06 \x01(\v2\x16.google.protobuf.EmptyH\x00R\x10rollbackResponse\x12]\n" + + "\x18write_mutations_response\x18\a \x01(\v2!.google.spanner.v1.CommitResponseH\x00R\x16writeMutationsResponseB\n" + "\n" + - "\bresponse2\x97\f\n" + + "\bresponse2\xeb\f\n" + "\n" + "SpannerLib\x12O\n" + "\x04Info\x12!.google.spannerlib.v1.InfoRequest\x1a\".google.spannerlib.v1.InfoResponse\"\x00\x12S\n" + @@ -989,7 +1308,8 @@ const file_google_spannerlib_v1_spannerlib_proto_rawDesc = "" + "\x06Commit\x12 .google.spannerlib.v1.Connection\x1a!.google.spanner.v1.CommitResponse\"\x00\x12F\n" + "\bRollback\x12 .google.spannerlib.v1.Connection\x1a\x16.google.protobuf.Empty\"\x00\x12b\n" + "\x0eWriteMutations\x12+.google.spannerlib.v1.WriteMutationsRequest\x1a!.google.spanner.v1.CommitResponse\"\x00\x12w\n" + - "\x10ConnectionStream\x12-.google.spannerlib.v1.ConnectionStreamRequest\x1a..google.spannerlib.v1.ConnectionStreamResponse\"\x00(\x010\x01B\xcd\x01\n" + + "\x10ConnectionStream\x12-.google.spannerlib.v1.ConnectionStreamRequest\x1a..google.spannerlib.v1.ConnectionStreamResponse\"\x00(\x010\x01\x12R\n" + + "\x11ContinueStreaming\x12\x1a.google.spannerlib.v1.Rows\x1a\x1d.google.spannerlib.v1.RowData\"\x000\x01B\xcd\x01\n" + "\x1ecom.google.cloud.spannerlib.v1B\x0fSpannerLibProtoP\x01Z>cloud.google.com/go/spannerlib/apiv1/spannerlibpb;spannerlibpb\xaa\x02\x1aGoogle.Cloud.SpannerLib.V1\xca\x02\x1aGoogle\\Cloud\\SpannerLib\\V1\xea\x02\x1dGoogle::Cloud::SpannerLib::V1b\x06proto3" var ( @@ -1004,7 +1324,7 @@ func file_google_spannerlib_v1_spannerlib_proto_rawDescGZIP() []byte { return file_google_spannerlib_v1_spannerlib_proto_rawDescData } -var file_google_spannerlib_v1_spannerlib_proto_msgTypes = make([]protoimpl.MessageInfo, 17) +var file_google_spannerlib_v1_spannerlib_proto_msgTypes = make([]protoimpl.MessageInfo, 19) var file_google_spannerlib_v1_spannerlib_proto_goTypes = []any{ (*InfoRequest)(nil), // 0: google.spannerlib.v1.InfoRequest (*InfoResponse)(nil), // 1: google.spannerlib.v1.InfoResponse @@ -1018,85 +1338,106 @@ var file_google_spannerlib_v1_spannerlib_proto_goTypes = []any{ (*Connection)(nil), // 9: google.spannerlib.v1.Connection (*Rows)(nil), // 10: google.spannerlib.v1.Rows (*NextRequest)(nil), // 11: google.spannerlib.v1.NextRequest - (*RowData)(nil), // 12: google.spannerlib.v1.RowData - (*MetadataRequest)(nil), // 13: google.spannerlib.v1.MetadataRequest - (*ResultSetStatsRequest)(nil), // 14: google.spannerlib.v1.ResultSetStatsRequest - (*ConnectionStreamRequest)(nil), // 15: google.spannerlib.v1.ConnectionStreamRequest - (*ConnectionStreamResponse)(nil), // 16: google.spannerlib.v1.ConnectionStreamResponse - (*spannerpb.ExecuteSqlRequest)(nil), // 17: google.spanner.v1.ExecuteSqlRequest - (*spannerpb.ExecuteBatchDmlRequest)(nil), // 18: google.spanner.v1.ExecuteBatchDmlRequest - (*spannerpb.TransactionOptions)(nil), // 19: google.spanner.v1.TransactionOptions - (*spannerpb.BatchWriteRequest_MutationGroup)(nil), // 20: google.spanner.v1.BatchWriteRequest.MutationGroup - (*spannerpb.ResultSetMetadata)(nil), // 21: google.spanner.v1.ResultSetMetadata - (*structpb.ListValue)(nil), // 22: google.protobuf.ListValue - (*spannerpb.ResultSetStats)(nil), // 23: google.spanner.v1.ResultSetStats - (*spannerpb.PartialResultSet)(nil), // 24: google.spanner.v1.PartialResultSet - (*emptypb.Empty)(nil), // 25: google.protobuf.Empty - (*spannerpb.ExecuteBatchDmlResponse)(nil), // 26: google.spanner.v1.ExecuteBatchDmlResponse - (*spannerpb.CommitResponse)(nil), // 27: google.spanner.v1.CommitResponse + (*FetchOptions)(nil), // 12: google.spannerlib.v1.FetchOptions + (*RowData)(nil), // 13: google.spannerlib.v1.RowData + (*MetadataRequest)(nil), // 14: google.spannerlib.v1.MetadataRequest + (*ResultSetStatsRequest)(nil), // 15: google.spannerlib.v1.ResultSetStatsRequest + (*ConnectionStreamRequest)(nil), // 16: google.spannerlib.v1.ConnectionStreamRequest + (*ExecuteResponse)(nil), // 17: google.spannerlib.v1.ExecuteResponse + (*ConnectionStreamResponse)(nil), // 18: google.spannerlib.v1.ConnectionStreamResponse + (*spannerpb.ExecuteSqlRequest)(nil), // 19: google.spanner.v1.ExecuteSqlRequest + (*spannerpb.ExecuteBatchDmlRequest)(nil), // 20: google.spanner.v1.ExecuteBatchDmlRequest + (*spannerpb.TransactionOptions)(nil), // 21: google.spanner.v1.TransactionOptions + (*spannerpb.BatchWriteRequest_MutationGroup)(nil), // 22: google.spanner.v1.BatchWriteRequest.MutationGroup + (*spannerpb.ResultSetMetadata)(nil), // 23: google.spanner.v1.ResultSetMetadata + (*structpb.ListValue)(nil), // 24: google.protobuf.ListValue + (*spannerpb.ResultSetStats)(nil), // 25: google.spanner.v1.ResultSetStats + (*spannerpb.ResultSet)(nil), // 26: google.spanner.v1.ResultSet + (*status.Status)(nil), // 27: google.rpc.Status + (*spannerpb.ExecuteBatchDmlResponse)(nil), // 28: google.spanner.v1.ExecuteBatchDmlResponse + (*emptypb.Empty)(nil), // 29: google.protobuf.Empty + (*spannerpb.CommitResponse)(nil), // 30: google.spanner.v1.CommitResponse } var file_google_spannerlib_v1_spannerlib_proto_depIdxs = []int32{ 8, // 0: google.spannerlib.v1.CreateConnectionRequest.pool:type_name -> google.spannerlib.v1.Pool 9, // 1: google.spannerlib.v1.ExecuteRequest.connection:type_name -> google.spannerlib.v1.Connection - 17, // 2: google.spannerlib.v1.ExecuteRequest.execute_sql_request:type_name -> google.spanner.v1.ExecuteSqlRequest - 9, // 3: google.spannerlib.v1.ExecuteBatchRequest.connection:type_name -> google.spannerlib.v1.Connection - 18, // 4: google.spannerlib.v1.ExecuteBatchRequest.execute_batch_dml_request:type_name -> google.spanner.v1.ExecuteBatchDmlRequest - 9, // 5: google.spannerlib.v1.BeginTransactionRequest.connection:type_name -> google.spannerlib.v1.Connection - 19, // 6: google.spannerlib.v1.BeginTransactionRequest.transaction_options:type_name -> google.spanner.v1.TransactionOptions - 9, // 7: google.spannerlib.v1.WriteMutationsRequest.connection:type_name -> google.spannerlib.v1.Connection - 20, // 8: google.spannerlib.v1.WriteMutationsRequest.mutations:type_name -> google.spanner.v1.BatchWriteRequest.MutationGroup - 8, // 9: google.spannerlib.v1.Connection.pool:type_name -> google.spannerlib.v1.Pool - 9, // 10: google.spannerlib.v1.Rows.connection:type_name -> google.spannerlib.v1.Connection - 10, // 11: google.spannerlib.v1.NextRequest.rows:type_name -> google.spannerlib.v1.Rows - 10, // 12: google.spannerlib.v1.RowData.rows:type_name -> google.spannerlib.v1.Rows - 21, // 13: google.spannerlib.v1.RowData.metadata:type_name -> google.spanner.v1.ResultSetMetadata - 22, // 14: google.spannerlib.v1.RowData.data:type_name -> google.protobuf.ListValue - 23, // 15: google.spannerlib.v1.RowData.stats:type_name -> google.spanner.v1.ResultSetStats - 10, // 16: google.spannerlib.v1.MetadataRequest.rows:type_name -> google.spannerlib.v1.Rows - 10, // 17: google.spannerlib.v1.ResultSetStatsRequest.rows:type_name -> google.spannerlib.v1.Rows - 4, // 18: google.spannerlib.v1.ConnectionStreamRequest.execute_request:type_name -> google.spannerlib.v1.ExecuteRequest - 24, // 19: google.spannerlib.v1.ConnectionStreamResponse.row:type_name -> google.spanner.v1.PartialResultSet - 0, // 20: google.spannerlib.v1.SpannerLib.Info:input_type -> google.spannerlib.v1.InfoRequest - 2, // 21: google.spannerlib.v1.SpannerLib.CreatePool:input_type -> google.spannerlib.v1.CreatePoolRequest - 8, // 22: google.spannerlib.v1.SpannerLib.ClosePool:input_type -> google.spannerlib.v1.Pool - 3, // 23: google.spannerlib.v1.SpannerLib.CreateConnection:input_type -> google.spannerlib.v1.CreateConnectionRequest - 9, // 24: google.spannerlib.v1.SpannerLib.CloseConnection:input_type -> google.spannerlib.v1.Connection - 4, // 25: google.spannerlib.v1.SpannerLib.Execute:input_type -> google.spannerlib.v1.ExecuteRequest - 4, // 26: google.spannerlib.v1.SpannerLib.ExecuteStreaming:input_type -> google.spannerlib.v1.ExecuteRequest - 5, // 27: google.spannerlib.v1.SpannerLib.ExecuteBatch:input_type -> google.spannerlib.v1.ExecuteBatchRequest - 10, // 28: google.spannerlib.v1.SpannerLib.Metadata:input_type -> google.spannerlib.v1.Rows - 11, // 29: google.spannerlib.v1.SpannerLib.Next:input_type -> google.spannerlib.v1.NextRequest - 10, // 30: google.spannerlib.v1.SpannerLib.ResultSetStats:input_type -> google.spannerlib.v1.Rows - 10, // 31: google.spannerlib.v1.SpannerLib.NextResultSet:input_type -> google.spannerlib.v1.Rows - 10, // 32: google.spannerlib.v1.SpannerLib.CloseRows:input_type -> google.spannerlib.v1.Rows - 6, // 33: google.spannerlib.v1.SpannerLib.BeginTransaction:input_type -> google.spannerlib.v1.BeginTransactionRequest - 9, // 34: google.spannerlib.v1.SpannerLib.Commit:input_type -> google.spannerlib.v1.Connection - 9, // 35: google.spannerlib.v1.SpannerLib.Rollback:input_type -> google.spannerlib.v1.Connection - 7, // 36: google.spannerlib.v1.SpannerLib.WriteMutations:input_type -> google.spannerlib.v1.WriteMutationsRequest - 15, // 37: google.spannerlib.v1.SpannerLib.ConnectionStream:input_type -> google.spannerlib.v1.ConnectionStreamRequest - 1, // 38: google.spannerlib.v1.SpannerLib.Info:output_type -> google.spannerlib.v1.InfoResponse - 8, // 39: google.spannerlib.v1.SpannerLib.CreatePool:output_type -> google.spannerlib.v1.Pool - 25, // 40: google.spannerlib.v1.SpannerLib.ClosePool:output_type -> google.protobuf.Empty - 9, // 41: google.spannerlib.v1.SpannerLib.CreateConnection:output_type -> google.spannerlib.v1.Connection - 25, // 42: google.spannerlib.v1.SpannerLib.CloseConnection:output_type -> google.protobuf.Empty - 10, // 43: google.spannerlib.v1.SpannerLib.Execute:output_type -> google.spannerlib.v1.Rows - 12, // 44: google.spannerlib.v1.SpannerLib.ExecuteStreaming:output_type -> google.spannerlib.v1.RowData - 26, // 45: google.spannerlib.v1.SpannerLib.ExecuteBatch:output_type -> google.spanner.v1.ExecuteBatchDmlResponse - 21, // 46: google.spannerlib.v1.SpannerLib.Metadata:output_type -> google.spanner.v1.ResultSetMetadata - 22, // 47: google.spannerlib.v1.SpannerLib.Next:output_type -> google.protobuf.ListValue - 23, // 48: google.spannerlib.v1.SpannerLib.ResultSetStats:output_type -> google.spanner.v1.ResultSetStats - 21, // 49: google.spannerlib.v1.SpannerLib.NextResultSet:output_type -> google.spanner.v1.ResultSetMetadata - 25, // 50: google.spannerlib.v1.SpannerLib.CloseRows:output_type -> google.protobuf.Empty - 25, // 51: google.spannerlib.v1.SpannerLib.BeginTransaction:output_type -> google.protobuf.Empty - 27, // 52: google.spannerlib.v1.SpannerLib.Commit:output_type -> google.spanner.v1.CommitResponse - 25, // 53: google.spannerlib.v1.SpannerLib.Rollback:output_type -> google.protobuf.Empty - 27, // 54: google.spannerlib.v1.SpannerLib.WriteMutations:output_type -> google.spanner.v1.CommitResponse - 16, // 55: google.spannerlib.v1.SpannerLib.ConnectionStream:output_type -> google.spannerlib.v1.ConnectionStreamResponse - 38, // [38:56] is the sub-list for method output_type - 20, // [20:38] is the sub-list for method input_type - 20, // [20:20] is the sub-list for extension type_name - 20, // [20:20] is the sub-list for extension extendee - 0, // [0:20] is the sub-list for field type_name + 19, // 2: google.spannerlib.v1.ExecuteRequest.execute_sql_request:type_name -> google.spanner.v1.ExecuteSqlRequest + 12, // 3: google.spannerlib.v1.ExecuteRequest.fetch_options:type_name -> google.spannerlib.v1.FetchOptions + 9, // 4: google.spannerlib.v1.ExecuteBatchRequest.connection:type_name -> google.spannerlib.v1.Connection + 20, // 5: google.spannerlib.v1.ExecuteBatchRequest.execute_batch_dml_request:type_name -> google.spanner.v1.ExecuteBatchDmlRequest + 9, // 6: google.spannerlib.v1.BeginTransactionRequest.connection:type_name -> google.spannerlib.v1.Connection + 21, // 7: google.spannerlib.v1.BeginTransactionRequest.transaction_options:type_name -> google.spanner.v1.TransactionOptions + 9, // 8: google.spannerlib.v1.WriteMutationsRequest.connection:type_name -> google.spannerlib.v1.Connection + 22, // 9: google.spannerlib.v1.WriteMutationsRequest.mutations:type_name -> google.spanner.v1.BatchWriteRequest.MutationGroup + 8, // 10: google.spannerlib.v1.Connection.pool:type_name -> google.spannerlib.v1.Pool + 9, // 11: google.spannerlib.v1.Rows.connection:type_name -> google.spannerlib.v1.Connection + 10, // 12: google.spannerlib.v1.NextRequest.rows:type_name -> google.spannerlib.v1.Rows + 12, // 13: google.spannerlib.v1.NextRequest.fetch_options:type_name -> google.spannerlib.v1.FetchOptions + 10, // 14: google.spannerlib.v1.RowData.rows:type_name -> google.spannerlib.v1.Rows + 23, // 15: google.spannerlib.v1.RowData.metadata:type_name -> google.spanner.v1.ResultSetMetadata + 24, // 16: google.spannerlib.v1.RowData.data:type_name -> google.protobuf.ListValue + 25, // 17: google.spannerlib.v1.RowData.stats:type_name -> google.spanner.v1.ResultSetStats + 10, // 18: google.spannerlib.v1.MetadataRequest.rows:type_name -> google.spannerlib.v1.Rows + 10, // 19: google.spannerlib.v1.ResultSetStatsRequest.rows:type_name -> google.spannerlib.v1.Rows + 4, // 20: google.spannerlib.v1.ConnectionStreamRequest.execute_request:type_name -> google.spannerlib.v1.ExecuteRequest + 5, // 21: google.spannerlib.v1.ConnectionStreamRequest.execute_batch_request:type_name -> google.spannerlib.v1.ExecuteBatchRequest + 6, // 22: google.spannerlib.v1.ConnectionStreamRequest.begin_transaction_request:type_name -> google.spannerlib.v1.BeginTransactionRequest + 9, // 23: google.spannerlib.v1.ConnectionStreamRequest.commit_request:type_name -> google.spannerlib.v1.Connection + 9, // 24: google.spannerlib.v1.ConnectionStreamRequest.rollback_request:type_name -> google.spannerlib.v1.Connection + 7, // 25: google.spannerlib.v1.ConnectionStreamRequest.write_mutations_request:type_name -> google.spannerlib.v1.WriteMutationsRequest + 10, // 26: google.spannerlib.v1.ExecuteResponse.rows:type_name -> google.spannerlib.v1.Rows + 26, // 27: google.spannerlib.v1.ExecuteResponse.result_sets:type_name -> google.spanner.v1.ResultSet + 27, // 28: google.spannerlib.v1.ExecuteResponse.status:type_name -> google.rpc.Status + 27, // 29: google.spannerlib.v1.ConnectionStreamResponse.status:type_name -> google.rpc.Status + 17, // 30: google.spannerlib.v1.ConnectionStreamResponse.execute_response:type_name -> google.spannerlib.v1.ExecuteResponse + 28, // 31: google.spannerlib.v1.ConnectionStreamResponse.execute_batch_response:type_name -> google.spanner.v1.ExecuteBatchDmlResponse + 29, // 32: google.spannerlib.v1.ConnectionStreamResponse.begin_transaction_response:type_name -> google.protobuf.Empty + 30, // 33: google.spannerlib.v1.ConnectionStreamResponse.commit_response:type_name -> google.spanner.v1.CommitResponse + 29, // 34: google.spannerlib.v1.ConnectionStreamResponse.rollback_response:type_name -> google.protobuf.Empty + 30, // 35: google.spannerlib.v1.ConnectionStreamResponse.write_mutations_response:type_name -> google.spanner.v1.CommitResponse + 0, // 36: google.spannerlib.v1.SpannerLib.Info:input_type -> google.spannerlib.v1.InfoRequest + 2, // 37: google.spannerlib.v1.SpannerLib.CreatePool:input_type -> google.spannerlib.v1.CreatePoolRequest + 8, // 38: google.spannerlib.v1.SpannerLib.ClosePool:input_type -> google.spannerlib.v1.Pool + 3, // 39: google.spannerlib.v1.SpannerLib.CreateConnection:input_type -> google.spannerlib.v1.CreateConnectionRequest + 9, // 40: google.spannerlib.v1.SpannerLib.CloseConnection:input_type -> google.spannerlib.v1.Connection + 4, // 41: google.spannerlib.v1.SpannerLib.Execute:input_type -> google.spannerlib.v1.ExecuteRequest + 4, // 42: google.spannerlib.v1.SpannerLib.ExecuteStreaming:input_type -> google.spannerlib.v1.ExecuteRequest + 5, // 43: google.spannerlib.v1.SpannerLib.ExecuteBatch:input_type -> google.spannerlib.v1.ExecuteBatchRequest + 10, // 44: google.spannerlib.v1.SpannerLib.Metadata:input_type -> google.spannerlib.v1.Rows + 11, // 45: google.spannerlib.v1.SpannerLib.Next:input_type -> google.spannerlib.v1.NextRequest + 10, // 46: google.spannerlib.v1.SpannerLib.ResultSetStats:input_type -> google.spannerlib.v1.Rows + 10, // 47: google.spannerlib.v1.SpannerLib.NextResultSet:input_type -> google.spannerlib.v1.Rows + 10, // 48: google.spannerlib.v1.SpannerLib.CloseRows:input_type -> google.spannerlib.v1.Rows + 6, // 49: google.spannerlib.v1.SpannerLib.BeginTransaction:input_type -> google.spannerlib.v1.BeginTransactionRequest + 9, // 50: google.spannerlib.v1.SpannerLib.Commit:input_type -> google.spannerlib.v1.Connection + 9, // 51: google.spannerlib.v1.SpannerLib.Rollback:input_type -> google.spannerlib.v1.Connection + 7, // 52: google.spannerlib.v1.SpannerLib.WriteMutations:input_type -> google.spannerlib.v1.WriteMutationsRequest + 16, // 53: google.spannerlib.v1.SpannerLib.ConnectionStream:input_type -> google.spannerlib.v1.ConnectionStreamRequest + 10, // 54: google.spannerlib.v1.SpannerLib.ContinueStreaming:input_type -> google.spannerlib.v1.Rows + 1, // 55: google.spannerlib.v1.SpannerLib.Info:output_type -> google.spannerlib.v1.InfoResponse + 8, // 56: google.spannerlib.v1.SpannerLib.CreatePool:output_type -> google.spannerlib.v1.Pool + 29, // 57: google.spannerlib.v1.SpannerLib.ClosePool:output_type -> google.protobuf.Empty + 9, // 58: google.spannerlib.v1.SpannerLib.CreateConnection:output_type -> google.spannerlib.v1.Connection + 29, // 59: google.spannerlib.v1.SpannerLib.CloseConnection:output_type -> google.protobuf.Empty + 10, // 60: google.spannerlib.v1.SpannerLib.Execute:output_type -> google.spannerlib.v1.Rows + 13, // 61: google.spannerlib.v1.SpannerLib.ExecuteStreaming:output_type -> google.spannerlib.v1.RowData + 28, // 62: google.spannerlib.v1.SpannerLib.ExecuteBatch:output_type -> google.spanner.v1.ExecuteBatchDmlResponse + 23, // 63: google.spannerlib.v1.SpannerLib.Metadata:output_type -> google.spanner.v1.ResultSetMetadata + 24, // 64: google.spannerlib.v1.SpannerLib.Next:output_type -> google.protobuf.ListValue + 25, // 65: google.spannerlib.v1.SpannerLib.ResultSetStats:output_type -> google.spanner.v1.ResultSetStats + 23, // 66: google.spannerlib.v1.SpannerLib.NextResultSet:output_type -> google.spanner.v1.ResultSetMetadata + 29, // 67: google.spannerlib.v1.SpannerLib.CloseRows:output_type -> google.protobuf.Empty + 29, // 68: google.spannerlib.v1.SpannerLib.BeginTransaction:output_type -> google.protobuf.Empty + 30, // 69: google.spannerlib.v1.SpannerLib.Commit:output_type -> google.spanner.v1.CommitResponse + 29, // 70: google.spannerlib.v1.SpannerLib.Rollback:output_type -> google.protobuf.Empty + 30, // 71: google.spannerlib.v1.SpannerLib.WriteMutations:output_type -> google.spanner.v1.CommitResponse + 18, // 72: google.spannerlib.v1.SpannerLib.ConnectionStream:output_type -> google.spannerlib.v1.ConnectionStreamResponse + 13, // 73: google.spannerlib.v1.SpannerLib.ContinueStreaming:output_type -> google.spannerlib.v1.RowData + 55, // [55:74] is the sub-list for method output_type + 36, // [36:55] is the sub-list for method input_type + 36, // [36:36] is the sub-list for extension type_name + 36, // [36:36] is the sub-list for extension extendee + 0, // [0:36] is the sub-list for field type_name } func init() { file_google_spannerlib_v1_spannerlib_proto_init() } @@ -1104,11 +1445,21 @@ func file_google_spannerlib_v1_spannerlib_proto_init() { if File_google_spannerlib_v1_spannerlib_proto != nil { return } - file_google_spannerlib_v1_spannerlib_proto_msgTypes[15].OneofWrappers = []any{ + file_google_spannerlib_v1_spannerlib_proto_msgTypes[16].OneofWrappers = []any{ (*ConnectionStreamRequest_ExecuteRequest)(nil), + (*ConnectionStreamRequest_ExecuteBatchRequest)(nil), + (*ConnectionStreamRequest_BeginTransactionRequest)(nil), + (*ConnectionStreamRequest_CommitRequest)(nil), + (*ConnectionStreamRequest_RollbackRequest)(nil), + (*ConnectionStreamRequest_WriteMutationsRequest)(nil), } - file_google_spannerlib_v1_spannerlib_proto_msgTypes[16].OneofWrappers = []any{ - (*ConnectionStreamResponse_Row)(nil), + file_google_spannerlib_v1_spannerlib_proto_msgTypes[18].OneofWrappers = []any{ + (*ConnectionStreamResponse_ExecuteResponse)(nil), + (*ConnectionStreamResponse_ExecuteBatchResponse)(nil), + (*ConnectionStreamResponse_BeginTransactionResponse)(nil), + (*ConnectionStreamResponse_CommitResponse)(nil), + (*ConnectionStreamResponse_RollbackResponse)(nil), + (*ConnectionStreamResponse_WriteMutationsResponse)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -1116,7 +1467,7 @@ func file_google_spannerlib_v1_spannerlib_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_google_spannerlib_v1_spannerlib_proto_rawDesc), len(file_google_spannerlib_v1_spannerlib_proto_rawDesc)), NumEnums: 0, - NumMessages: 17, + NumMessages: 19, NumExtensions: 0, NumServices: 1, }, diff --git a/spannerlib/grpc-server/google/spannerlib/v1/spannerlib.proto b/spannerlib/grpc-server/google/spannerlib/v1/spannerlib.proto index 4cf46d2e..746c0005 100644 --- a/spannerlib/grpc-server/google/spannerlib/v1/spannerlib.proto +++ b/spannerlib/grpc-server/google/spannerlib/v1/spannerlib.proto @@ -5,6 +5,7 @@ package google.spannerlib.v1; import "google/api/field_behavior.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/struct.proto"; +import "google/rpc/status.proto"; import "google/spanner/v1/result_set.proto"; import "google/spanner/v1/spanner.proto"; import "google/spanner/v1/transaction.proto"; @@ -42,6 +43,7 @@ service SpannerLib { rpc WriteMutations(WriteMutationsRequest) returns (google.spanner.v1.CommitResponse) {} rpc ConnectionStream(stream ConnectionStreamRequest) returns (stream ConnectionStreamResponse) {} + rpc ContinueStreaming(Rows) returns (stream RowData) {} } message InfoRequest { @@ -70,6 +72,7 @@ message ExecuteRequest { google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [ (google.api.field_behavior) = REQUIRED ]; + FetchOptions fetch_options = 3; } message ExecuteBatchRequest { @@ -127,24 +130,31 @@ message NextRequest { Rows rows = 1 [ (google.api.field_behavior) = REQUIRED ]; - int64 num_rows = 2 [ + FetchOptions fetch_options = 2 [ (google.api.field_behavior) = REQUIRED ]; - int64 encoding = 3 [ +} + +message FetchOptions { + int64 num_rows = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + int64 encoding = 2 [ (google.api.field_behavior) = REQUIRED ]; } message RowData { - Rows rows = 1 [ + string request_id = 1; + Rows rows = 2 [ (google.api.field_behavior) = REQUIRED ]; - google.spanner.v1.ResultSetMetadata metadata = 2; - repeated google.protobuf.ListValue data = 3 [ + google.spanner.v1.ResultSetMetadata metadata = 3; + repeated google.protobuf.ListValue data = 4 [ (google.api.field_behavior) = REQUIRED ]; - google.spanner.v1.ResultSetStats stats = 4; - bool has_more_results = 5; + google.spanner.v1.ResultSetStats stats = 5; + bool has_more_results = 6; } message MetadataRequest { @@ -162,11 +172,31 @@ message ResultSetStatsRequest { message ConnectionStreamRequest { oneof request { ExecuteRequest execute_request = 1; + ExecuteBatchRequest execute_batch_request = 2; + BeginTransactionRequest begin_transaction_request = 3; + Connection commit_request = 4; + Connection rollback_request = 5; + WriteMutationsRequest write_mutations_request = 6; } } +message ExecuteResponse { + Rows rows = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + repeated google.spanner.v1.ResultSet result_sets = 2; + google.rpc.Status status = 3; + bool has_more_results = 4; +} + message ConnectionStreamResponse { + google.rpc.Status status = 1; oneof response { - google.spanner.v1.PartialResultSet row = 1; + ExecuteResponse execute_response = 2; + google.spanner.v1.ExecuteBatchDmlResponse execute_batch_response = 3; + google.protobuf.Empty begin_transaction_response = 4; + google.spanner.v1.CommitResponse commit_response = 5; + google.protobuf.Empty rollback_response = 6; + google.spanner.v1.CommitResponse write_mutations_response = 7; } } diff --git a/spannerlib/grpc-server/google/spannerlib/v1/spannerlib_grpc.pb.go b/spannerlib/grpc-server/google/spannerlib/v1/spannerlib_grpc.pb.go index 73ceb12f..45f62a59 100644 --- a/spannerlib/grpc-server/google/spannerlib/v1/spannerlib_grpc.pb.go +++ b/spannerlib/grpc-server/google/spannerlib/v1/spannerlib_grpc.pb.go @@ -22,24 +22,25 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - SpannerLib_Info_FullMethodName = "/google.spannerlib.v1.SpannerLib/Info" - SpannerLib_CreatePool_FullMethodName = "/google.spannerlib.v1.SpannerLib/CreatePool" - SpannerLib_ClosePool_FullMethodName = "/google.spannerlib.v1.SpannerLib/ClosePool" - SpannerLib_CreateConnection_FullMethodName = "/google.spannerlib.v1.SpannerLib/CreateConnection" - SpannerLib_CloseConnection_FullMethodName = "/google.spannerlib.v1.SpannerLib/CloseConnection" - SpannerLib_Execute_FullMethodName = "/google.spannerlib.v1.SpannerLib/Execute" - SpannerLib_ExecuteStreaming_FullMethodName = "/google.spannerlib.v1.SpannerLib/ExecuteStreaming" - SpannerLib_ExecuteBatch_FullMethodName = "/google.spannerlib.v1.SpannerLib/ExecuteBatch" - SpannerLib_Metadata_FullMethodName = "/google.spannerlib.v1.SpannerLib/Metadata" - SpannerLib_Next_FullMethodName = "/google.spannerlib.v1.SpannerLib/Next" - SpannerLib_ResultSetStats_FullMethodName = "/google.spannerlib.v1.SpannerLib/ResultSetStats" - SpannerLib_NextResultSet_FullMethodName = "/google.spannerlib.v1.SpannerLib/NextResultSet" - SpannerLib_CloseRows_FullMethodName = "/google.spannerlib.v1.SpannerLib/CloseRows" - SpannerLib_BeginTransaction_FullMethodName = "/google.spannerlib.v1.SpannerLib/BeginTransaction" - SpannerLib_Commit_FullMethodName = "/google.spannerlib.v1.SpannerLib/Commit" - SpannerLib_Rollback_FullMethodName = "/google.spannerlib.v1.SpannerLib/Rollback" - SpannerLib_WriteMutations_FullMethodName = "/google.spannerlib.v1.SpannerLib/WriteMutations" - SpannerLib_ConnectionStream_FullMethodName = "/google.spannerlib.v1.SpannerLib/ConnectionStream" + SpannerLib_Info_FullMethodName = "/google.spannerlib.v1.SpannerLib/Info" + SpannerLib_CreatePool_FullMethodName = "/google.spannerlib.v1.SpannerLib/CreatePool" + SpannerLib_ClosePool_FullMethodName = "/google.spannerlib.v1.SpannerLib/ClosePool" + SpannerLib_CreateConnection_FullMethodName = "/google.spannerlib.v1.SpannerLib/CreateConnection" + SpannerLib_CloseConnection_FullMethodName = "/google.spannerlib.v1.SpannerLib/CloseConnection" + SpannerLib_Execute_FullMethodName = "/google.spannerlib.v1.SpannerLib/Execute" + SpannerLib_ExecuteStreaming_FullMethodName = "/google.spannerlib.v1.SpannerLib/ExecuteStreaming" + SpannerLib_ExecuteBatch_FullMethodName = "/google.spannerlib.v1.SpannerLib/ExecuteBatch" + SpannerLib_Metadata_FullMethodName = "/google.spannerlib.v1.SpannerLib/Metadata" + SpannerLib_Next_FullMethodName = "/google.spannerlib.v1.SpannerLib/Next" + SpannerLib_ResultSetStats_FullMethodName = "/google.spannerlib.v1.SpannerLib/ResultSetStats" + SpannerLib_NextResultSet_FullMethodName = "/google.spannerlib.v1.SpannerLib/NextResultSet" + SpannerLib_CloseRows_FullMethodName = "/google.spannerlib.v1.SpannerLib/CloseRows" + SpannerLib_BeginTransaction_FullMethodName = "/google.spannerlib.v1.SpannerLib/BeginTransaction" + SpannerLib_Commit_FullMethodName = "/google.spannerlib.v1.SpannerLib/Commit" + SpannerLib_Rollback_FullMethodName = "/google.spannerlib.v1.SpannerLib/Rollback" + SpannerLib_WriteMutations_FullMethodName = "/google.spannerlib.v1.SpannerLib/WriteMutations" + SpannerLib_ConnectionStream_FullMethodName = "/google.spannerlib.v1.SpannerLib/ConnectionStream" + SpannerLib_ContinueStreaming_FullMethodName = "/google.spannerlib.v1.SpannerLib/ContinueStreaming" ) // SpannerLibClient is the client API for SpannerLib service. @@ -64,6 +65,7 @@ type SpannerLibClient interface { Rollback(ctx context.Context, in *Connection, opts ...grpc.CallOption) (*emptypb.Empty, error) WriteMutations(ctx context.Context, in *WriteMutationsRequest, opts ...grpc.CallOption) (*spannerpb.CommitResponse, error) ConnectionStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ConnectionStreamRequest, ConnectionStreamResponse], error) + ContinueStreaming(ctx context.Context, in *Rows, opts ...grpc.CallOption) (grpc.ServerStreamingClient[RowData], error) } type spannerLibClient struct { @@ -266,6 +268,25 @@ func (c *spannerLibClient) ConnectionStream(ctx context.Context, opts ...grpc.Ca // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type SpannerLib_ConnectionStreamClient = grpc.BidiStreamingClient[ConnectionStreamRequest, ConnectionStreamResponse] +func (c *spannerLibClient) ContinueStreaming(ctx context.Context, in *Rows, opts ...grpc.CallOption) (grpc.ServerStreamingClient[RowData], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &SpannerLib_ServiceDesc.Streams[2], SpannerLib_ContinueStreaming_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[Rows, RowData]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type SpannerLib_ContinueStreamingClient = grpc.ServerStreamingClient[RowData] + // SpannerLibServer is the server API for SpannerLib service. // All implementations must embed UnimplementedSpannerLibServer // for forward compatibility. @@ -288,6 +309,7 @@ type SpannerLibServer interface { Rollback(context.Context, *Connection) (*emptypb.Empty, error) WriteMutations(context.Context, *WriteMutationsRequest) (*spannerpb.CommitResponse, error) ConnectionStream(grpc.BidiStreamingServer[ConnectionStreamRequest, ConnectionStreamResponse]) error + ContinueStreaming(*Rows, grpc.ServerStreamingServer[RowData]) error mustEmbedUnimplementedSpannerLibServer() } @@ -352,6 +374,9 @@ func (UnimplementedSpannerLibServer) WriteMutations(context.Context, *WriteMutat func (UnimplementedSpannerLibServer) ConnectionStream(grpc.BidiStreamingServer[ConnectionStreamRequest, ConnectionStreamResponse]) error { return status.Errorf(codes.Unimplemented, "method ConnectionStream not implemented") } +func (UnimplementedSpannerLibServer) ContinueStreaming(*Rows, grpc.ServerStreamingServer[RowData]) error { + return status.Errorf(codes.Unimplemented, "method ContinueStreaming not implemented") +} func (UnimplementedSpannerLibServer) mustEmbedUnimplementedSpannerLibServer() {} func (UnimplementedSpannerLibServer) testEmbeddedByValue() {} @@ -679,6 +704,17 @@ func _SpannerLib_ConnectionStream_Handler(srv interface{}, stream grpc.ServerStr // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type SpannerLib_ConnectionStreamServer = grpc.BidiStreamingServer[ConnectionStreamRequest, ConnectionStreamResponse] +func _SpannerLib_ContinueStreaming_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(Rows) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(SpannerLibServer).ContinueStreaming(m, &grpc.GenericServerStream[Rows, RowData]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type SpannerLib_ContinueStreamingServer = grpc.ServerStreamingServer[RowData] + // SpannerLib_ServiceDesc is the grpc.ServiceDesc for SpannerLib service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -763,6 +799,11 @@ var SpannerLib_ServiceDesc = grpc.ServiceDesc{ ServerStreams: true, ClientStreams: true, }, + { + StreamName: "ContinueStreaming", + Handler: _SpannerLib_ContinueStreaming_Handler, + ServerStreams: true, + }, }, Metadata: "google/spannerlib/v1/spannerlib.proto", } diff --git a/spannerlib/grpc-server/server.go b/spannerlib/grpc-server/server.go index 957fb350..dfdcac72 100644 --- a/spannerlib/grpc-server/server.go +++ b/spannerlib/grpc-server/server.go @@ -10,7 +10,10 @@ import ( "syscall" "cloud.google.com/go/spanner/apiv1/spannerpb" + "google.golang.org/genproto/googleapis/rpc/status" "google.golang.org/grpc" + "google.golang.org/grpc/codes" + gstatus "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/structpb" "spannerlib/api" @@ -127,28 +130,34 @@ func (s *spannerLibServer) ExecuteStreaming(request *pb.ExecuteRequest, stream g if err != nil { return err } - defer func() { _ = api.CloseRows(context.Background(), request.Connection.Pool.Id, request.Connection.Id, id) }() rows := &pb.Rows{Connection: request.Connection, Id: id} - metadata, err := api.Metadata(queryContext, request.Connection.Pool.Id, request.Connection.Id, id) + return s.streamRows(queryContext, rows, stream) +} + +func (s *spannerLibServer) ContinueStreaming(rows *pb.Rows, stream grpc.ServerStreamingServer[pb.RowData]) error { + queryContext := stream.Context() + return s.streamRows(queryContext, rows, stream) +} + +func (s *spannerLibServer) streamRows(queryContext context.Context, rows *pb.Rows, stream grpc.ServerStreamingServer[pb.RowData]) error { + defer func() { _ = api.CloseRows(context.Background(), rows.Connection.Pool.Id, rows.Connection.Id, rows.Id) }() + metadata, err := api.Metadata(queryContext, rows.Connection.Pool.Id, rows.Connection.Id, rows.Id) if err != nil { return err } first := true for { - if row, err := api.Next(queryContext, request.Connection.Pool.Id, request.Connection.Id, id); err != nil { + if row, err := api.Next(queryContext, rows.Connection.Pool.Id, rows.Connection.Id, rows.Id); err != nil { return err } else { if row == nil { - stats, err := api.ResultSetStats(queryContext, request.Connection.Pool.Id, request.Connection.Id, id) + stats, err := api.ResultSetStats(queryContext, rows.Connection.Pool.Id, rows.Connection.Id, rows.Id) if err != nil { return err } - nextMetadata, err := api.NextResultSet(queryContext, request.Connection.Pool.Id, request.Connection.Id, id) - if err != nil { - return err - } - res := &pb.RowData{Rows: rows, Stats: stats, HasMoreResults: nextMetadata != nil} + nextMetadata, nextResultSetErr := api.NextResultSet(queryContext, rows.Connection.Pool.Id, rows.Connection.Id, rows.Id) + res := &pb.RowData{Rows: rows, Stats: stats, HasMoreResults: nextMetadata != nil || nextResultSetErr != nil} if first { res.Metadata = metadata first = false @@ -156,6 +165,9 @@ func (s *spannerLibServer) ExecuteStreaming(request *pb.ExecuteRequest, stream g if err := stream.Send(res); err != nil { return err } + if nextResultSetErr != nil { + return nextResultSetErr + } if res.HasMoreResults { metadata = nextMetadata first = true @@ -176,6 +188,117 @@ func (s *spannerLibServer) ExecuteStreaming(request *pb.ExecuteRequest, stream g return nil } +func (s *spannerLibServer) ConnectionStream(stream grpc.BidiStreamingServer[pb.ConnectionStreamRequest, pb.ConnectionStreamResponse]) error { + for { + var err error + var response *pb.ConnectionStreamResponse + req, err := stream.Recv() + if err != nil { + return err + } + if req.GetExecuteRequest() != nil { + ctx := stream.Context() + response, err = s.handleExecuteRequest(ctx, req.GetExecuteRequest()) + } else if req.GetExecuteBatchRequest() != nil { + ctx := stream.Context() + response, err = s.handleExecuteBatchRequest(ctx, req.GetExecuteBatchRequest()) + } else if req.GetWriteMutationsRequest() != nil { + ctx := stream.Context() + response, err = s.handleWriteMutationsRequest(ctx, req.GetWriteMutationsRequest()) + } else if req.GetBeginTransactionRequest() != nil { + ctx := stream.Context() + response, err = s.handleBeginTransactionRequest(ctx, req.GetBeginTransactionRequest()) + } else if req.GetCommitRequest() != nil { + ctx := stream.Context() + response, err = s.handleCommitRequest(ctx, req.GetCommitRequest()) + } else if req.GetRollbackRequest() != nil { + ctx := stream.Context() + response, err = s.handleRollbackRequest(ctx, req.GetRollbackRequest()) + } else { + return gstatus.Errorf(codes.Unimplemented, "unsupported request type: %v", req.Request) + } + if err != nil { + response = &pb.ConnectionStreamResponse{Status: gstatus.Convert(err).Proto()} + } + if stream.Send(response) != nil { + return err + } + } +} + +func (s *spannerLibServer) handleExecuteRequest(ctx context.Context, request *pb.ExecuteRequest) (*pb.ConnectionStreamResponse, error) { + maxFetchRows := int64(50) + if request.FetchOptions != nil && request.FetchOptions.NumRows > 0 { + maxFetchRows = request.FetchOptions.NumRows + } + + rows, err := s.Execute(ctx, request) + if err != nil { + return nil, err + } + response := &pb.ConnectionStreamResponse_ExecuteResponse{ExecuteResponse: &pb.ExecuteResponse{Rows: rows}} + defer func() { + if !response.ExecuteResponse.HasMoreResults { + _ = api.CloseRows(context.Background(), rows.Connection.Pool.Id, rows.Connection.Id, rows.Id) + } + }() + + metadata, err := api.Metadata(ctx, rows.Connection.Pool.Id, rows.Connection.Id, rows.Id) + numRows := int64(0) + for { + resultSet := &spannerpb.ResultSet{} + if err != nil { + return nil, err + } + resultSet.Metadata = metadata + response.ExecuteResponse.ResultSets = append(response.ExecuteResponse.ResultSets, resultSet) + for { + row, err := api.Next(ctx, rows.Connection.Pool.Id, rows.Connection.Id, rows.Id) + if err != nil { + if len(response.ExecuteResponse.ResultSets) == 1 { + return nil, err + } + // Remove the last result set from the response and return an error code for it instead. + response.ExecuteResponse.ResultSets = response.ExecuteResponse.ResultSets[:len(response.ExecuteResponse.ResultSets)-1] + response.ExecuteResponse.Status = gstatus.Convert(err).Proto() + return &pb.ConnectionStreamResponse{Status: &status.Status{}, Response: response}, nil + } + if row == nil { + break + } + resultSet.Rows = append(resultSet.Rows, row) + numRows++ + if numRows == maxFetchRows { + response.ExecuteResponse.HasMoreResults = true + return &pb.ConnectionStreamResponse{Status: &status.Status{}, Response: response}, nil + } + } + + stats, err := api.ResultSetStats(ctx, rows.Connection.Pool.Id, rows.Connection.Id, rows.Id) + if err != nil { + if len(response.ExecuteResponse.ResultSets) == 1 { + return nil, err + } + // Remove the last result set from the response and return an error code for it instead. + response.ExecuteResponse.ResultSets = response.ExecuteResponse.ResultSets[:len(response.ExecuteResponse.ResultSets)-1] + response.ExecuteResponse.Status = gstatus.Convert(err).Proto() + return &pb.ConnectionStreamResponse{Status: &status.Status{}, Response: response}, nil + } + resultSet.Stats = stats + + metadata, err = api.NextResultSet(ctx, rows.Connection.Pool.Id, rows.Connection.Id, rows.Id) + if err != nil { + response.ExecuteResponse.Status = gstatus.Convert(err).Proto() + return &pb.ConnectionStreamResponse{Status: &status.Status{}, Response: response}, nil + } + if metadata == nil { + break + } + } + response.ExecuteResponse.Status = &status.Status{} + return &pb.ConnectionStreamResponse{Status: &status.Status{}, Response: response}, nil +} + func (s *spannerLibServer) ExecuteBatch(ctx context.Context, request *pb.ExecuteBatchRequest) (*spannerpb.ExecuteBatchDmlResponse, error) { resp, err := api.ExecuteBatch(ctx, request.Connection.Pool.Id, request.Connection.Id, request.ExecuteBatchDmlRequest) if err != nil { @@ -184,6 +307,14 @@ func (s *spannerLibServer) ExecuteBatch(ctx context.Context, request *pb.Execute return resp, nil } +func (s *spannerLibServer) handleExecuteBatchRequest(ctx context.Context, request *pb.ExecuteBatchRequest) (*pb.ConnectionStreamResponse, error) { + res, err := s.ExecuteBatch(ctx, request) + if err != nil { + return nil, err + } + return &pb.ConnectionStreamResponse{Status: &status.Status{}, Response: &pb.ConnectionStreamResponse_ExecuteBatchResponse{ExecuteBatchResponse: res}}, nil +} + func (s *spannerLibServer) Metadata(ctx context.Context, rows *pb.Rows) (*spannerpb.ResultSetMetadata, error) { metadata, err := api.Metadata(ctx, rows.Connection.Pool.Id, rows.Connection.Id, rows.Id) if err != nil { @@ -237,6 +368,14 @@ func (s *spannerLibServer) BeginTransaction(ctx context.Context, request *pb.Beg return &emptypb.Empty{}, nil } +func (s *spannerLibServer) handleBeginTransactionRequest(ctx context.Context, request *pb.BeginTransactionRequest) (*pb.ConnectionStreamResponse, error) { + res, err := s.BeginTransaction(ctx, request) + if err != nil { + return nil, err + } + return &pb.ConnectionStreamResponse{Status: &status.Status{}, Response: &pb.ConnectionStreamResponse_BeginTransactionResponse{BeginTransactionResponse: res}}, nil +} + func (s *spannerLibServer) Commit(ctx context.Context, connection *pb.Connection) (*spannerpb.CommitResponse, error) { resp, err := api.Commit(ctx, connection.Pool.Id, connection.Id) if err != nil { @@ -245,6 +384,14 @@ func (s *spannerLibServer) Commit(ctx context.Context, connection *pb.Connection return resp, nil } +func (s *spannerLibServer) handleCommitRequest(ctx context.Context, connection *pb.Connection) (*pb.ConnectionStreamResponse, error) { + res, err := s.Commit(ctx, connection) + if err != nil { + return nil, err + } + return &pb.ConnectionStreamResponse{Status: &status.Status{}, Response: &pb.ConnectionStreamResponse_CommitResponse{CommitResponse: res}}, nil +} + func (s *spannerLibServer) Rollback(ctx context.Context, connection *pb.Connection) (*emptypb.Empty, error) { err := api.Rollback(ctx, connection.Pool.Id, connection.Id) if err != nil { @@ -253,6 +400,14 @@ func (s *spannerLibServer) Rollback(ctx context.Context, connection *pb.Connecti return &emptypb.Empty{}, nil } +func (s *spannerLibServer) handleRollbackRequest(ctx context.Context, connection *pb.Connection) (*pb.ConnectionStreamResponse, error) { + res, err := s.Rollback(ctx, connection) + if err != nil { + return nil, err + } + return &pb.ConnectionStreamResponse{Status: &status.Status{}, Response: &pb.ConnectionStreamResponse_RollbackResponse{RollbackResponse: res}}, nil +} + func (s *spannerLibServer) WriteMutations(ctx context.Context, request *pb.WriteMutationsRequest) (*spannerpb.CommitResponse, error) { resp, err := api.WriteMutations(ctx, request.Connection.Pool.Id, request.Connection.Id, request.Mutations) if err != nil { @@ -260,3 +415,11 @@ func (s *spannerLibServer) WriteMutations(ctx context.Context, request *pb.Write } return resp, nil } + +func (s *spannerLibServer) handleWriteMutationsRequest(ctx context.Context, request *pb.WriteMutationsRequest) (*pb.ConnectionStreamResponse, error) { + res, err := s.WriteMutations(ctx, request) + if err != nil { + return nil, err + } + return &pb.ConnectionStreamResponse{Status: &status.Status{}, Response: &pb.ConnectionStreamResponse_WriteMutationsResponse{WriteMutationsResponse: res}}, nil +} diff --git a/spannerlib/grpc-server/server_test.go b/spannerlib/grpc-server/server_test.go index 01a08718..acc8428c 100644 --- a/spannerlib/grpc-server/server_test.go +++ b/spannerlib/grpc-server/server_test.go @@ -123,7 +123,7 @@ func TestExecute(t *testing.T) { numRows := 0 for { - row, err := client.Next(ctx, &pb.NextRequest{Rows: rows, NumRows: 1}) + row, err := client.Next(ctx, &pb.NextRequest{Rows: rows, FetchOptions: &pb.FetchOptions{NumRows: 1}}) if err != nil { t.Fatalf("failed to fetch next row: %v", err) } @@ -260,6 +260,11 @@ func TestExecuteStreamingMultiStatement(t *testing.T) { Name: "test-operation", }, }) + invalidQuery := "select * from unknown_table" + _ = server.TestSpanner.PutStatementResult(invalidQuery, &testutil.StatementResult{ + Type: testutil.StatementResultError, + Err: status.Error(codes.NotFound, "Table not found"), + }) client, cleanup := startTestSpannerLibServer(t) defer cleanup() @@ -276,6 +281,7 @@ func TestExecuteStreamingMultiStatement(t *testing.T) { type expectedResults struct { numRows int affected int64 + err codes.Code } type test struct { name string @@ -373,6 +379,15 @@ func TestExecuteStreamingMultiStatement(t *testing.T) { {affected: testutil.UpdateBarSetFooRowCount}, }, }, + { + name: "query then error", + sql: fmt.Sprintf("%s;%s", testutil.SelectFooFromBar, invalidQuery), + numExecuteRequests: 2, + expectedResults: []expectedResults{ + {numRows: 2}, + {err: codes.NotFound}, + }, + }, } { t.Run(tt.name, func(t *testing.T) { stream, err := client.ExecuteStreaming(ctx, &pb.ExecuteRequest{ @@ -386,8 +401,15 @@ func TestExecuteStreamingMultiStatement(t *testing.T) { numRows := 0 for { row, err := stream.Recv() - if err != nil { - t.Fatalf("failed to receive row: %v", err) + if tt.expectedResults[numResultSets-1].err != codes.OK { + if g, w := status.Code(err), tt.expectedResults[numResultSets-1].err; g != w { + t.Fatalf("err code mismatch\n Got: %v\n Want: %v", g, w) + } + break + } else { + if err != nil { + t.Fatalf("failed to receive row: %v", err) + } } if len(row.Data) == 0 { if g, w := numRows, tt.expectedResults[numResultSets-1].numRows; g != w { @@ -733,6 +755,60 @@ func TestExecuteBatch(t *testing.T) { } } +func TestExecuteBatchBidi(t *testing.T) { + t.Parallel() + ctx := context.Background() + + server, teardown := setupMockSpannerServer(t) + defer teardown() + dsn := fmt.Sprintf("%s/projects/p/instances/i/databases/d?useplaintext=true", server.Address) + + client, cleanup := startTestSpannerLibServer(t) + defer cleanup() + + pool, err := client.CreatePool(ctx, &pb.CreatePoolRequest{ConnectionString: dsn}) + if err != nil { + t.Fatalf("failed to create pool: %v", err) + } + connection, err := client.CreateConnection(ctx, &pb.CreateConnectionRequest{Pool: pool}) + if err != nil { + t.Fatalf("failed to create connection: %v", err) + } + stream, err := client.ConnectionStream(ctx) + if err != nil { + t.Fatalf("failed to start stream: %v", err) + } + if err := stream.Send(&pb.ConnectionStreamRequest{ + Request: &pb.ConnectionStreamRequest_ExecuteBatchRequest{ + ExecuteBatchRequest: &pb.ExecuteBatchRequest{ + Connection: connection, + ExecuteBatchDmlRequest: &sppb.ExecuteBatchDmlRequest{ + Statements: []*sppb.ExecuteBatchDmlRequest_Statement{ + {Sql: testutil.UpdateBarSetFoo}, + {Sql: testutil.UpdateBarSetFoo}, + }, + }, + }, + }, + }); err != nil { + t.Fatalf("failed to send ExecuteBatch request: %v", err) + } + resp, err := stream.Recv() + if err != nil { + t.Fatalf("failed to execute batch: %v", err) + } + if g, w := len(resp.GetExecuteBatchResponse().ResultSets), 2; g != w { + t.Fatalf("num results mismatch\n Got: %v\nWant: %v", g, w) + } + + if err := stream.CloseSend(); err != nil { + t.Fatalf("failed to close send: %v", err) + } + if _, err := client.ClosePool(ctx, pool); err != nil { + t.Fatalf("failed to close pool: %v", err) + } +} + func TestTransaction(t *testing.T) { t.Parallel() ctx := context.Background() @@ -773,7 +849,7 @@ func TestTransaction(t *testing.T) { if err != nil { t.Fatalf("failed to execute: %v", err) } - row, err := client.Next(ctx, &pb.NextRequest{Rows: rows, NumRows: 1}) + row, err := client.Next(ctx, &pb.NextRequest{Rows: rows, FetchOptions: &pb.FetchOptions{NumRows: 1}}) if err != nil { t.Fatalf("failed to fetch next row: %v", err) } @@ -814,6 +890,108 @@ func TestTransaction(t *testing.T) { } } +func TestTransactionBidi(t *testing.T) { + t.Parallel() + ctx := context.Background() + + server, teardown := setupMockSpannerServer(t) + defer teardown() + dsn := fmt.Sprintf("%s/projects/p/instances/i/databases/d?useplaintext=true", server.Address) + + client, cleanup := startTestSpannerLibServer(t) + defer cleanup() + + pool, err := client.CreatePool(ctx, &pb.CreatePoolRequest{ConnectionString: dsn}) + if err != nil { + t.Fatalf("failed to create pool: %v", err) + } + connection, err := client.CreateConnection(ctx, &pb.CreateConnectionRequest{Pool: pool}) + if err != nil { + t.Fatalf("failed to create connection: %v", err) + } + stream, err := client.ConnectionStream(ctx) + if err != nil { + t.Fatalf("failed to open connection stream: %v", err) + } + if err := stream.Send(&pb.ConnectionStreamRequest{Request: &pb.ConnectionStreamRequest_ExecuteRequest{ + ExecuteRequest: &pb.ExecuteRequest{ + Connection: connection, + ExecuteSqlRequest: &sppb.ExecuteSqlRequest{Sql: "set transaction_tag='test_tag'"}, + }, + }}); err != nil { + t.Fatalf("failed to set transaction_tag: %v", err) + } + if _, err := stream.Recv(); err != nil { + t.Fatalf("failed to receive response: %v", err) + } + + for i := 0; i < 2; i++ { + if err := stream.Send(&pb.ConnectionStreamRequest{ + Request: &pb.ConnectionStreamRequest_BeginTransactionRequest{ + BeginTransactionRequest: &pb.BeginTransactionRequest{ + Connection: connection, + TransactionOptions: &sppb.TransactionOptions{}, + }, + }, + }); err != nil { + t.Fatalf("failed to begin transaction: %v", err) + } + if _, err := stream.Recv(); err != nil { + t.Fatalf("failed to receive response: %v", err) + } + if err := stream.Send(&pb.ConnectionStreamRequest{ + Request: &pb.ConnectionStreamRequest_ExecuteRequest{ + ExecuteRequest: &pb.ExecuteRequest{ + Connection: connection, + ExecuteSqlRequest: &sppb.ExecuteSqlRequest{Sql: testutil.UpdateBarSetFoo}, + }, + }, + }); err != nil { + t.Fatalf("failed to execute: %v", err) + } + response, err := stream.Recv() + if err != nil { + t.Fatalf("failed to execute: %v", err) + } + resultSet := response.GetExecuteResponse().ResultSets[0] + if resultSet.Rows != nil { + t.Fatalf("row values should be nil: %v", resultSet.Rows) + } + stats := response.GetExecuteResponse().ResultSets[0].Stats + if g, w := stats.GetRowCountExact(), int64(testutil.UpdateBarSetFooRowCount); g != w { + t.Fatalf("row count mismatch\n Got: %v\nWant: %v", g, w) + } + if err := stream.Send(&pb.ConnectionStreamRequest{ + Request: &pb.ConnectionStreamRequest_CommitRequest{ + CommitRequest: connection, + }, + }); err != nil { + t.Fatalf("failed to commit: %v", err) + } + if _, err := stream.Recv(); err != nil { + t.Fatalf("failed to receive response: %v", err) + } + + requests := server.TestSpanner.DrainRequestsFromServer() + executeRequests := testutil.RequestsOfType(requests, reflect.TypeOf(&sppb.ExecuteSqlRequest{})) + if g, w := len(executeRequests), 1; g != w { + t.Fatalf("num execute requests mismatch\n Got: %v\nWant: %v", g, w) + } + request := executeRequests[0].(*sppb.ExecuteSqlRequest) + expectedTag := "test_tag" + if i == 1 { + expectedTag = "" + } + if g, w := request.RequestOptions.TransactionTag, expectedTag; g != w { + t.Fatalf("transaction tag mismatch\n Got: %v\nWant: %v", g, w) + } + } + + if _, err := client.ClosePool(ctx, pool); err != nil { + t.Fatalf("failed to close pool: %v", err) + } +} + func TestRollback(t *testing.T) { t.Parallel() ctx := context.Background() @@ -915,6 +1093,375 @@ func TestWriteMutations(t *testing.T) { } } +func TestBidiStream(t *testing.T) { + t.Parallel() + ctx := context.Background() + + server, teardown := setupMockSpannerServer(t) + defer teardown() + dsn := fmt.Sprintf("%s/projects/p/instances/i/databases/d?useplaintext=true", server.Address) + + client, cleanup := startTestSpannerLibServer(t) + defer cleanup() + + pool, err := client.CreatePool(ctx, &pb.CreatePoolRequest{ConnectionString: dsn}) + if err != nil { + t.Fatalf("failed to create pool: %v", err) + } + connection, err := client.CreateConnection(ctx, &pb.CreateConnectionRequest{Pool: pool}) + if err != nil { + t.Fatalf("failed to create connection: %v", err) + } + + connStream, err := client.ConnectionStream(ctx) + if err != nil { + t.Fatalf("failed to open connection stream: %v", err) + } + for range 10 { + if err := connStream.Send(&pb.ConnectionStreamRequest{Request: &pb.ConnectionStreamRequest_ExecuteRequest{ExecuteRequest: &pb.ExecuteRequest{ + Connection: connection, + ExecuteSqlRequest: &sppb.ExecuteSqlRequest{Sql: testutil.SelectFooFromBar}, + }}}); err != nil { + t.Fatalf("failed to send execute request: %v", err) + } + numRows := 0 + response, err := connStream.Recv() + if err != nil { + t.Fatalf("failed to receive response: %v", err) + } + for _, resultSet := range response.GetExecuteResponse().ResultSets { + for i, row := range resultSet.Rows { + if g, w := len(row.Values), 1; g != w { + t.Fatalf("num values mismatch\n Got: %v\nWant: %v", g, w) + } + if g, w := row.Values[0].GetStringValue(), fmt.Sprintf("%d", i+1); g != w { + t.Fatalf("value mismatch\n Got: %v\nWant: %v", g, w) + } + numRows++ + } + } + if g, w := numRows, 2; g != w { + t.Fatalf("num rows mismatch\n Got: %v\nWant: %v", g, w) + } + } + if err := connStream.CloseSend(); err != nil { + t.Fatalf("failed to close connection stream: %v", err) + } + + if _, err := client.ClosePool(ctx, pool); err != nil { + t.Fatalf("failed to close pool: %v", err) + } +} + +func TestBidiStreamMultiStatement(t *testing.T) { + t.Parallel() + ctx := context.Background() + + server, teardown := setupMockSpannerServer(t) + defer teardown() + dsn := fmt.Sprintf("%s/projects/p/instances/i/databases/d?useplaintext=true", server.Address) + + client, cleanup := startTestSpannerLibServer(t) + defer cleanup() + + pool, err := client.CreatePool(ctx, &pb.CreatePoolRequest{ConnectionString: dsn}) + if err != nil { + t.Fatalf("failed to create pool: %v", err) + } + connection, err := client.CreateConnection(ctx, &pb.CreateConnectionRequest{Pool: pool}) + if err != nil { + t.Fatalf("failed to create connection: %v", err) + } + + connStream, err := client.ConnectionStream(ctx) + if err != nil { + t.Fatalf("failed to open connection stream: %v", err) + } + if err := connStream.Send(&pb.ConnectionStreamRequest{Request: &pb.ConnectionStreamRequest_ExecuteRequest{ExecuteRequest: &pb.ExecuteRequest{ + Connection: connection, + ExecuteSqlRequest: &sppb.ExecuteSqlRequest{Sql: fmt.Sprintf("%s;%s", testutil.SelectFooFromBar, testutil.UpdateBarSetFoo)}, + }}}); err != nil { + t.Fatalf("failed to send execute request: %v", err) + } + numRows := 0 + response, err := connStream.Recv() + if err != nil { + t.Fatalf("failed to receive response: %v", err) + } + if g, w := len(response.GetExecuteResponse().ResultSets), 2; g != w { + t.Fatalf("num result sets mismatch\n Got: %v\nWant: %v", g, w) + } + + // Get the query result. + resultSet := response.GetExecuteResponse().ResultSets[0] + for i, row := range resultSet.Rows { + if g, w := len(row.Values), 1; g != w { + t.Fatalf("num values mismatch\n Got: %v\nWant: %v", g, w) + } + if g, w := row.Values[0].GetStringValue(), fmt.Sprintf("%d", i+1); g != w { + t.Fatalf("value mismatch\n Got: %v\nWant: %v", g, w) + } + numRows++ + } + if g, w := numRows, 2; g != w { + t.Fatalf("num rows mismatch\n Got: %v\nWant: %v", g, w) + } + // Get the DML result. + dmlResult := response.GetExecuteResponse().ResultSets[1] + if g, w := len(dmlResult.Rows), 0; g != w { + t.Fatalf("num rows mismatch\n Got: %v\nWant: %v", g, w) + } + if g, w := dmlResult.Stats.GetRowCountExact(), int64(testutil.UpdateBarSetFooRowCount); g != w { + t.Fatalf("update count mismatch\n Got: %v\nWant: %v", g, w) + } + if response.GetExecuteResponse().HasMoreResults { + t.Fatal("expected no more results") + } + + if err := connStream.CloseSend(); err != nil { + t.Fatalf("failed to close connection stream: %v", err) + } + + if _, err := client.ClosePool(ctx, pool); err != nil { + t.Fatalf("failed to close pool: %v", err) + } +} + +func TestBidiStreamMultiStatementFirstFails(t *testing.T) { + t.Parallel() + ctx := context.Background() + + server, teardown := setupMockSpannerServer(t) + defer teardown() + dsn := fmt.Sprintf("%s/projects/p/instances/i/databases/d?useplaintext=true", server.Address) + + client, cleanup := startTestSpannerLibServer(t) + defer cleanup() + + pool, err := client.CreatePool(ctx, &pb.CreatePoolRequest{ConnectionString: dsn}) + if err != nil { + t.Fatalf("failed to create pool: %v", err) + } + connection, err := client.CreateConnection(ctx, &pb.CreateConnectionRequest{Pool: pool}) + if err != nil { + t.Fatalf("failed to create connection: %v", err) + } + + connStream, err := client.ConnectionStream(ctx) + if err != nil { + t.Fatalf("failed to open connection stream: %v", err) + } + if err := connStream.Send(&pb.ConnectionStreamRequest{Request: &pb.ConnectionStreamRequest_ExecuteRequest{ExecuteRequest: &pb.ExecuteRequest{ + Connection: connection, + ExecuteSqlRequest: &sppb.ExecuteSqlRequest{Sql: fmt.Sprintf("%s;%s", testutil.SelectFooFromBar, testutil.UpdateBarSetFoo)}, + }}}); err != nil { + t.Fatalf("failed to send execute request: %v", err) + } + numRows := 0 + response, err := connStream.Recv() + if err != nil { + t.Fatalf("failed to receive response: %v", err) + } + if g, w := len(response.GetExecuteResponse().ResultSets), 2; g != w { + t.Fatalf("num result sets mismatch\n Got: %v\nWant: %v", g, w) + } + + // Get the query result. + resultSet := response.GetExecuteResponse().ResultSets[0] + for i, row := range resultSet.Rows { + if g, w := len(row.Values), 1; g != w { + t.Fatalf("num values mismatch\n Got: %v\nWant: %v", g, w) + } + if g, w := row.Values[0].GetStringValue(), fmt.Sprintf("%d", i+1); g != w { + t.Fatalf("value mismatch\n Got: %v\nWant: %v", g, w) + } + numRows++ + } + if g, w := numRows, 2; g != w { + t.Fatalf("num rows mismatch\n Got: %v\nWant: %v", g, w) + } + // Get the DML result. + dmlResult := response.GetExecuteResponse().ResultSets[1] + if g, w := len(dmlResult.Rows), 0; g != w { + t.Fatalf("num rows mismatch\n Got: %v\nWant: %v", g, w) + } + if g, w := dmlResult.Stats.GetRowCountExact(), int64(testutil.UpdateBarSetFooRowCount); g != w { + t.Fatalf("update count mismatch\n Got: %v\nWant: %v", g, w) + } + if response.GetExecuteResponse().HasMoreResults { + t.Fatal("expected no more results") + } + + if err := connStream.CloseSend(); err != nil { + t.Fatalf("failed to close connection stream: %v", err) + } + + if _, err := client.ClosePool(ctx, pool); err != nil { + t.Fatalf("failed to close pool: %v", err) + } +} + +func TestBidiStreamEmptyResults(t *testing.T) { + t.Parallel() + ctx := context.Background() + + server, teardown := setupMockSpannerServer(t) + defer teardown() + dsn := fmt.Sprintf("%s/projects/p/instances/i/databases/d?useplaintext=true", server.Address) + + query := "select * from my_table where 1=0" + _ = server.TestSpanner.PutStatementResult(query, &testutil.StatementResult{ + Type: testutil.StatementResultResultSet, + ResultSet: &sppb.ResultSet{ + Metadata: &sppb.ResultSetMetadata{ + RowType: &sppb.StructType{ + Fields: []*sppb.StructType_Field{{Name: "c", Type: &sppb.Type{Code: sppb.TypeCode_INT64}}}, + }, + }, + Rows: []*structpb.ListValue{}, + }, + }) + + client, cleanup := startTestSpannerLibServer(t) + defer cleanup() + + pool, err := client.CreatePool(ctx, &pb.CreatePoolRequest{ConnectionString: dsn}) + if err != nil { + t.Fatalf("failed to create pool: %v", err) + } + connection, err := client.CreateConnection(ctx, &pb.CreateConnectionRequest{Pool: pool}) + if err != nil { + t.Fatalf("failed to create connection: %v", err) + } + + connStream, err := client.ConnectionStream(ctx) + if err != nil { + t.Fatalf("failed to open connection stream: %v", err) + } + if err := connStream.Send(&pb.ConnectionStreamRequest{Request: &pb.ConnectionStreamRequest_ExecuteRequest{ExecuteRequest: &pb.ExecuteRequest{ + Connection: connection, + ExecuteSqlRequest: &sppb.ExecuteSqlRequest{Sql: query}, + }}}); err != nil { + t.Fatalf("failed to send execute request: %v", err) + } + numRows := 0 + row, err := connStream.Recv() + if err != nil { + t.Fatalf("failed to receive response: %v", err) + } + for _, resultSet := range row.GetExecuteResponse().ResultSets { + numRows += len(resultSet.Rows) + } + if g, w := numRows, 0; g != w { + t.Fatalf("num rows mismatch\n Got: %v\nWant: %v", g, w) + } + if err := connStream.CloseSend(); err != nil { + t.Fatalf("failed to close connection stream: %v", err) + } + + if _, err := client.ClosePool(ctx, pool); err != nil { + t.Fatalf("failed to close pool: %v", err) + } +} + +func TestBidiStreamLargeResult(t *testing.T) { + t.Parallel() + ctx := context.Background() + + server, teardown := setupMockSpannerServer(t) + defer teardown() + dsn := fmt.Sprintf("%s/projects/p/instances/i/databases/d?useplaintext=true", server.Address) + + numRows := 125 + query := "select id from my_table" + _ = server.TestSpanner.PutStatementResult(query, &testutil.StatementResult{ + Type: testutil.StatementResultResultSet, + ResultSet: testutil.CreateSingleColumnInt64ResultSet(createInt64Slice(numRows), "id"), + }) + + client, cleanup := startTestSpannerLibServer(t) + defer cleanup() + + pool, err := client.CreatePool(ctx, &pb.CreatePoolRequest{ConnectionString: dsn}) + if err != nil { + t.Fatalf("failed to create pool: %v", err) + } + connection, err := client.CreateConnection(ctx, &pb.CreateConnectionRequest{Pool: pool}) + if err != nil { + t.Fatalf("failed to create connection: %v", err) + } + + connStream, err := client.ConnectionStream(ctx) + if err != nil { + t.Fatalf("failed to open connection stream: %v", err) + } + if err := connStream.Send(&pb.ConnectionStreamRequest{Request: &pb.ConnectionStreamRequest_ExecuteRequest{ExecuteRequest: &pb.ExecuteRequest{ + Connection: connection, + ExecuteSqlRequest: &sppb.ExecuteSqlRequest{Sql: query}, + }}}); err != nil { + t.Fatalf("failed to send execute request: %v", err) + } + foundRows := 0 + response, err := connStream.Recv() + if err != nil { + t.Fatalf("failed to receive response: %v", err) + } + for _, resultSet := range response.GetExecuteResponse().ResultSets { + for i, row := range resultSet.Rows { + if g, w := len(row.Values), 1; g != w { + t.Fatalf("num values mismatch\n Got: %v\nWant: %v", g, w) + } + if g, w := row.Values[0].GetStringValue(), fmt.Sprintf("%d", i+1); g != w { + t.Fatalf("value mismatch\n Got: %v\nWant: %v", g, w) + } + foundRows++ + } + } + if response.GetExecuteResponse().HasMoreResults { + stream, err := client.ContinueStreaming(ctx, response.GetExecuteResponse().Rows) + if err != nil { + t.Fatalf("failed to open stream: %v", err) + } + for { + row, err := stream.Recv() + if err != nil { + t.Fatalf("failed to receive row: %v", err) + } + if len(row.Data) == 0 { + break + } + if g, w := len(row.Data), 1; g != w { + t.Fatalf("num rows mismatch\n Got: %v\nWant: %v", g, w) + } + if g, w := len(row.Data[0].Values), 1; g != w { + t.Fatalf("num values mismatch\n Got: %v\nWant: %v", g, w) + } + if g, w := row.Data[0].Values[0].GetStringValue(), fmt.Sprintf("%d", foundRows+1); g != w { + t.Fatalf("value mismatch\n Got: %v\nWant: %v", g, w) + } + foundRows++ + } + } + if g, w := foundRows, numRows; g != w { + t.Fatalf("num rows mismatch\n Got: %v\nWant: %v", g, w) + } + if err := connStream.CloseSend(); err != nil { + t.Fatalf("failed to close connection stream: %v", err) + } + + if _, err := client.ClosePool(ctx, pool); err != nil { + t.Fatalf("failed to close pool: %v", err) + } +} + +func createInt64Slice(n int) []int64 { + res := make([]int64, n) + for i := 0; i < n; i++ { + res[i] = int64(i + 1) + } + return res +} + func startTestSpannerLibServer(t *testing.T) (client pb.SpannerLibClient, cleanup func()) { var tp string var name string diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/GrpcConnection.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/GrpcConnection.cs new file mode 100644 index 00000000..4c725a77 --- /dev/null +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/GrpcConnection.cs @@ -0,0 +1,38 @@ +// Copyright 2025 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 +// +// https://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. + +using Google.Cloud.SpannerLib.V1; +using Grpc.Core; + +namespace Google.Cloud.SpannerLib.Grpc; + +internal class GrpcConnection(V1.SpannerLib.SpannerLibClient client, Pool pool, long id) : Connection(pool, id) +{ + private AsyncDuplexStreamingCall? _stream; + + internal AsyncDuplexStreamingCall Stream + { + get + { + _stream ??= client.ConnectionStream(); + return _stream; + } + } + + protected override void CloseLibObject() + { + base.CloseLibObject(); + _stream?.Dispose(); + } +} diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/GrpcLibSpanner.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/GrpcLibSpanner.cs index 0c98130d..198bb370 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/GrpcLibSpanner.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/GrpcLibSpanner.cs @@ -17,9 +17,11 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; +using Google.Api.Gax; using Google.Cloud.Spanner.V1; using Google.Cloud.SpannerLib.V1; using Google.Protobuf.WellKnownTypes; +using Google.Rpc; using Grpc.Core; using Grpc.Net.Client; using BeginTransactionRequest = Google.Cloud.SpannerLib.V1.BeginTransactionRequest; @@ -27,7 +29,7 @@ namespace Google.Cloud.SpannerLib.Grpc; -public class GrpcLibSpanner : ISpannerLib +public sealed class GrpcLibSpanner : ISpannerLib { public static GrpcChannel ForUnixSocket(string fileName) { @@ -61,24 +63,31 @@ public static GrpcChannel ForTcpSocket(string address) MaxReceiveMessageSize = null, }); } + + public enum CommunicationStyle + { + ServerStreaming, + BidiStreaming, + } private readonly Server _server; - private readonly V1.SpannerLib.SpannerLibClient _client; - private readonly GrpcChannel _channel; private readonly V1.SpannerLib.SpannerLibClient[] _clients; private readonly GrpcChannel[] _channels; - private readonly bool _useStreamingRows; + private readonly CommunicationStyle _communicationStyle; private bool _disposed; + + private V1.SpannerLib.SpannerLibClient Client => _clients[Random.Shared.Next(_clients.Length)]; - public GrpcLibSpanner(bool useStreamingRows = true, Server.AddressType addressType = Server.AddressType.UnixDomainSocket) + public GrpcLibSpanner( + CommunicationStyle communicationStyle = CommunicationStyle.BidiStreaming, + int numChannels = 4, + Server.AddressType addressType = Server.AddressType.UnixDomainSocket) { + GaxPreconditions.CheckArgument(numChannels > 0, nameof(numChannels), "numChannels must be > 0"); _server = new Server(); var file = _server.Start(addressType: addressType); - _channel = addressType == Server.AddressType.Tcp ? ForTcpSocket(file) : ForUnixSocket(file); - _client = new V1.SpannerLib.SpannerLibClient(_channel); - _useStreamingRows = useStreamingRows; + _communicationStyle = communicationStyle; - var numChannels = 1; _channels = new GrpcChannel[numChannels]; _clients = new V1.SpannerLib.SpannerLibClient[numChannels]; for (var i = 0; i < numChannels; i++) @@ -88,13 +97,15 @@ public GrpcLibSpanner(bool useStreamingRows = true, Server.AddressType addressTy } } + ~GrpcLibSpanner() => Dispose(false); + public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (_disposed) { @@ -102,7 +113,6 @@ protected virtual void Dispose(bool disposing) } try { - _channel.Dispose(); foreach (var channel in _channels) { channel.Dispose(); @@ -127,9 +137,42 @@ T TranslateException(Func f) } } + private ConnectionStreamResponse ExecuteBidi(GrpcConnection connection, ConnectionStreamRequest request) + { + var connectionStream = connection.Stream; + connectionStream.RequestStream.WriteAsync(request).GetAwaiter().GetResult(); + if (!connectionStream.ResponseStream.MoveNext(CancellationToken.None).GetAwaiter().GetResult()) + { + throw new SpannerException(new Status { Code = (int)Code.Internal, Message = "No response received" }); + } + if (connectionStream.ResponseStream.Current.Status.Code != (int)Code.Ok) + { + throw new SpannerException(connectionStream.ResponseStream.Current.Status); + } + return connectionStream.ResponseStream.Current; + } + + private async Task ExecuteBidiAsync(GrpcConnection connection, + ConnectionStreamRequest request, CancellationToken cancellationToken) + { + var connectionStream = connection.Stream; + await connectionStream.RequestStream.WriteAsync(request, cancellationToken).ConfigureAwait(false); + if (!await connectionStream.ResponseStream.MoveNext(cancellationToken).ConfigureAwait(false)) + { + throw new SpannerException(new Status { Code = (int)Code.Internal, Message = "No response received" }); + } + + if (connectionStream.ResponseStream.Current.Status.Code != (int)Code.Ok) + { + throw new SpannerException(connectionStream.ResponseStream.Current.Status); + } + + return connectionStream.ResponseStream.Current; + } + public Pool CreatePool(string connectionString) { - return FromProto(TranslateException(() => _client.CreatePool(new CreatePoolRequest + return FromProto(TranslateException(() => Client.CreatePool(new CreatePoolRequest { ConnectionString = connectionString, }))); @@ -137,12 +180,12 @@ public Pool CreatePool(string connectionString) public void ClosePool(Pool pool) { - TranslateException(() => _client.ClosePool(ToProto(pool))); + TranslateException(() => Client.ClosePool(ToProto(pool))); } public Connection CreateConnection(Pool pool) { - return FromProto(pool, TranslateException(() => _client.CreateConnection(new CreateConnectionRequest + return FromProto(pool, TranslateException(() => Client.CreateConnection(new CreateConnectionRequest { Pool = ToProto(pool), }))); @@ -150,14 +193,14 @@ public Connection CreateConnection(Pool pool) public void CloseConnection(Connection connection) { - TranslateException(() => _client.CloseConnection(ToProto(connection))); + TranslateException(() => Client.CloseConnection(ToProto(connection))); } public async Task CloseConnectionAsync(Connection connection, CancellationToken cancellationToken = default) { try { - await _client.CloseConnectionAsync(ToProto(connection), cancellationToken: cancellationToken).ConfigureAwait(false); + await Client.CloseConnectionAsync(ToProto(connection), cancellationToken: cancellationToken).ConfigureAwait(false); } catch (RpcException exception) { @@ -167,20 +210,42 @@ public async Task CloseConnectionAsync(Connection connection, CancellationToken public CommitResponse? WriteMutations(Connection connection, BatchWriteRequest.Types.MutationGroup mutations) { - var response = TranslateException(() => _client.WriteMutations(new WriteMutationsRequest + if (_communicationStyle == CommunicationStyle.BidiStreaming && connection is GrpcConnection grpcConnection) + { + return WriteMutationsBidi(grpcConnection, mutations); + } + var response = TranslateException(() => Client.WriteMutations(new WriteMutationsRequest { Connection = ToProto(connection), Mutations = mutations, })); return response.CommitTimestamp == null ? null : response; } + + private CommitResponse? WriteMutationsBidi(GrpcConnection connection, + BatchWriteRequest.Types.MutationGroup mutations) + { + var response = ExecuteBidi(connection, new ConnectionStreamRequest + { + WriteMutationsRequest = new WriteMutationsRequest + { + Connection = ToProto(connection), + Mutations = mutations, + } + }); + return response.WriteMutationsResponse?.CommitTimestamp != null ? response.WriteMutationsResponse : null; + } public async Task WriteMutationsAsync(Connection connection, BatchWriteRequest.Types.MutationGroup mutations, CancellationToken cancellationToken = default) { + if (_communicationStyle == CommunicationStyle.BidiStreaming && connection is GrpcConnection grpcConnection) + { + return await WriteMutationsBidiAsync(grpcConnection, mutations, cancellationToken).ConfigureAwait(false); + } try { - var response = await _client.WriteMutationsAsync(new WriteMutationsRequest + var response = await Client.WriteMutationsAsync(new WriteMutationsRequest { Connection = ToProto(connection), Mutations = mutations, @@ -192,14 +257,32 @@ public async Task CloseConnectionAsync(Connection connection, CancellationToken throw SpannerException.ToSpannerException(exception); } } + + private async Task WriteMutationsBidiAsync(GrpcConnection connection, + BatchWriteRequest.Types.MutationGroup mutations, CancellationToken cancellationToken) + { + var response = await ExecuteBidiAsync(connection, new ConnectionStreamRequest + { + WriteMutationsRequest = new WriteMutationsRequest + { + Connection = ToProto(connection), + Mutations = mutations, + } + }, cancellationToken).ConfigureAwait(false); + return response.WriteMutationsResponse?.CommitTimestamp != null ? response.WriteMutationsResponse : null; + } - public Rows Execute(Connection connection, ExecuteSqlRequest statement) + public Rows Execute(Connection connection, ExecuteSqlRequest statement, int prefetchRows = 0) { - if (_useStreamingRows) + if (_communicationStyle == CommunicationStyle.ServerStreaming) { return ExecuteStreaming(connection, statement); } - return FromProto(connection, TranslateException(() => _client.Execute(new ExecuteRequest + if (_communicationStyle == CommunicationStyle.BidiStreaming && connection is GrpcConnection grpcConnection) + { + return ExecuteBidiStreaming(grpcConnection, statement, prefetchRows); + } + return FromProto(connection, TranslateException(() => Client.Execute(new ExecuteRequest { Connection = ToProto(connection), ExecuteSqlRequest = statement, @@ -214,18 +297,46 @@ private StreamingRows ExecuteStreaming(Connection connection, ExecuteSqlRequest Connection = ToProto(connection), ExecuteSqlRequest = statement, })); - return StreamingRows.Create(connection, stream); + return StreamingRows.Create(this, connection, stream); } - public async Task ExecuteAsync(Connection connection, ExecuteSqlRequest statement, CancellationToken cancellationToken = default) + private StreamingRows ExecuteBidiStreaming(GrpcConnection connection, ExecuteSqlRequest statement, int prefetchRows) + { + var response = ExecuteBidi(connection, new ConnectionStreamRequest + { + ExecuteRequest = new ExecuteRequest + { + Connection = ToProto(connection), + ExecuteSqlRequest = statement, + FetchOptions = new FetchOptions { NumRows = prefetchRows }, + } + }); + return StreamingRows.Create(this, connection, response.ExecuteResponse); + } + + internal AsyncServerStreamingCall ContinueStreaming(Connection connection, long rowsId) + { + var client = _clients[Random.Shared.Next(_clients.Length)]; + return TranslateException(() => client.ContinueStreaming(new V1.Rows + { + Connection = ToProto(connection), + Id = rowsId, + })); + } + + public async Task ExecuteAsync(Connection connection, ExecuteSqlRequest statement, int prefetchRows = 0, CancellationToken cancellationToken = default) { try { - if (_useStreamingRows) + if (_communicationStyle == CommunicationStyle.ServerStreaming) { return await ExecuteStreamingAsync(connection, statement, cancellationToken).ConfigureAwait(false); } - var rows = await _client.ExecuteAsync(new ExecuteRequest + if (_communicationStyle == CommunicationStyle.BidiStreaming && connection is GrpcConnection grpcConnection) + { + return await ExecuteBidiStreamingAsync(grpcConnection, statement, prefetchRows, cancellationToken).ConfigureAwait(false); + } + var rows = await Client.ExecuteAsync(new ExecuteRequest { Connection = ToProto(connection), ExecuteSqlRequest = statement, @@ -238,58 +349,83 @@ public async Task ExecuteAsync(Connection connection, ExecuteSqlRequest st } } - private async Task ExecuteStreamingAsync(Connection connection, ExecuteSqlRequest statement, CancellationToken cancellationToken = default) + private async Task ExecuteStreamingAsync(Connection connection, ExecuteSqlRequest statement, CancellationToken cancellationToken) { var client = _clients[Random.Shared.Next(_clients.Length)]; var stream = TranslateException(() => client.ExecuteStreaming(new ExecuteRequest { Connection = ToProto(connection), ExecuteSqlRequest = statement, - })); - return await StreamingRows.CreateAsync(connection, stream, cancellationToken).ConfigureAwait(false); + }, cancellationToken: cancellationToken)); + return await StreamingRows.CreateAsync(this, connection, stream, cancellationToken).ConfigureAwait(false); + } + + private async Task ExecuteBidiStreamingAsync(GrpcConnection connection, ExecuteSqlRequest statement, int prefetchRows, CancellationToken cancellationToken) + { + var response = await ExecuteBidiAsync(connection, new ConnectionStreamRequest + { + ExecuteRequest = new ExecuteRequest + { + Connection = ToProto(connection), + ExecuteSqlRequest = statement, + FetchOptions = new FetchOptions {NumRows = prefetchRows}, + } + }, cancellationToken).ConfigureAwait(false); + // ReSharper disable once MethodHasAsyncOverloadWithCancellation + return StreamingRows.Create(this, connection, response.ExecuteResponse); + } + + internal AsyncServerStreamingCall ContinueStreamingAsync(Connection connection, long rowsId, CancellationToken cancellationToken) + { + var client = _clients[Random.Shared.Next(_clients.Length)]; + return TranslateException(() => client.ContinueStreaming(new V1.Rows + { + Connection = ToProto(connection), + Id = rowsId, + }, cancellationToken: cancellationToken)); } public long[] ExecuteBatch(Connection connection, ExecuteBatchDmlRequest statements) { - var response = TranslateException(() => _client.ExecuteBatch(new ExecuteBatchRequest + if (_communicationStyle == CommunicationStyle.BidiStreaming && connection is GrpcConnection grpcConnection) + { + return ExecuteBatchBidi(grpcConnection, statements); + } + var response = TranslateException(() => Client.ExecuteBatch(new ExecuteBatchRequest { Connection = ToProto(connection), ExecuteBatchDmlRequest = statements, })); - var result = new long[response.ResultSets.Count]; - for (var i = 0; i < result.Length; i++) + return ToUpdateCounts(response); + } + + private long[] ExecuteBatchBidi(GrpcConnection connection, ExecuteBatchDmlRequest statements) + { + var response = ExecuteBidi(connection, new ConnectionStreamRequest { - if (response.ResultSets[i].Stats.HasRowCountExact) - { - result[i] = response.ResultSets[i].Stats.RowCountExact; - } - else if (response.ResultSets[i].Stats.HasRowCountLowerBound) - { - result[i] = response.ResultSets[i].Stats.RowCountLowerBound; - } - else + ExecuteBatchRequest = new ExecuteBatchRequest { - result[i] = -1; + Connection = ToProto(connection), + ExecuteBatchDmlRequest = statements, } - } - return result; + }); + return ToUpdateCounts(response.ExecuteBatchResponse); } public async Task ExecuteBatchAsync(Connection connection, ExecuteBatchDmlRequest statements, CancellationToken cancellationToken = default) { + if (_communicationStyle == CommunicationStyle.BidiStreaming && connection is GrpcConnection grpcConnection) + { + return await ExecuteBatchBidiAsync(grpcConnection, statements, cancellationToken).ConfigureAwait(false); + } try { - var stats = await _client.ExecuteBatchAsync(new ExecuteBatchRequest + var stats = await Client.ExecuteBatchAsync(new ExecuteBatchRequest { Connection = ToProto(connection), ExecuteBatchDmlRequest = statements, }, cancellationToken: cancellationToken).ConfigureAwait(false); - var result = new long[stats.ResultSets.Count]; - for (var i = 0; i < result.Length; i++) - { - result[i] = stats.ResultSets[i].Stats.RowCountExact; - } - return result; + return ToUpdateCounts(stats); } catch (RpcException exception) { @@ -297,16 +433,51 @@ public async Task ExecuteBatchAsync(Connection connection, ExecuteBatchD } } + private async Task ExecuteBatchBidiAsync(GrpcConnection connection, ExecuteBatchDmlRequest statements, + CancellationToken cancellationToken) + { + var response = await ExecuteBidiAsync(connection, new ConnectionStreamRequest + { + ExecuteBatchRequest = new ExecuteBatchRequest + { + Connection = ToProto(connection), + ExecuteBatchDmlRequest = statements, + } + }, cancellationToken).ConfigureAwait(false); + return ToUpdateCounts(response.ExecuteBatchResponse); + } + + private static long[] ToUpdateCounts(ExecuteBatchDmlResponse response) + { + var result = new long[response.ResultSets.Count]; + for (var i = 0; i < result.Length; i++) + { + if (response.ResultSets[i].Stats.HasRowCountExact) + { + result[i] = response.ResultSets[i].Stats.RowCountExact; + } + else if (response.ResultSets[i].Stats.HasRowCountLowerBound) + { + result[i] = response.ResultSets[i].Stats.RowCountLowerBound; + } + else + { + result[i] = -1; + } + } + return result; + } + public ResultSetMetadata? Metadata(Rows rows) { - return TranslateException(() => _client.Metadata(ToProto(rows))); + return TranslateException(() => Client.Metadata(ToProto(rows))); } public async Task MetadataAsync(Rows rows, CancellationToken cancellationToken = default) { try { - return await _client.MetadataAsync(ToProto(rows), cancellationToken: cancellationToken).ConfigureAwait(false); + return await Client.MetadataAsync(ToProto(rows), cancellationToken: cancellationToken).ConfigureAwait(false); } catch (RpcException exception) { @@ -316,14 +487,14 @@ public async Task ExecuteBatchAsync(Connection connection, ExecuteBatchD public ResultSetMetadata? NextResultSet(Rows rows) { - return TranslateException(() => _client.NextResultSet(ToProto(rows))); + return TranslateException(() => Client.NextResultSet(ToProto(rows))); } public async Task NextResultSetAsync(Rows rows, CancellationToken cancellationToken = default) { try { - return await _client.NextResultSetAsync(ToProto(rows), cancellationToken: cancellationToken).ConfigureAwait(false); + return await Client.NextResultSetAsync(ToProto(rows), cancellationToken: cancellationToken).ConfigureAwait(false); } catch (RpcException exception) { @@ -333,16 +504,15 @@ public async Task ExecuteBatchAsync(Connection connection, ExecuteBatchD public ResultSetStats? Stats(Rows rows) { - return TranslateException(() => _client.ResultSetStats(ToProto(rows))); + return TranslateException(() => Client.ResultSetStats(ToProto(rows))); } public ListValue? Next(Rows rows, int numRows, ISpannerLib.RowEncoding encoding) { - var row = TranslateException(() =>_client.Next(new NextRequest + var row = TranslateException(() =>Client.Next(new NextRequest { Rows = ToProto(rows), - NumRows = numRows, - Encoding = (long) encoding, + FetchOptions = new FetchOptions{NumRows = numRows, Encoding = (long)encoding}, })); return row.Values.Count == 0 ? null : row; } @@ -351,11 +521,10 @@ public async Task ExecuteBatchAsync(Connection connection, ExecuteBatchD { try { - return await _client.NextAsync(new NextRequest + return await Client.NextAsync(new NextRequest { Rows = ToProto(rows), - NumRows = numRows, - Encoding = (long)encoding, + FetchOptions = new FetchOptions {NumRows = numRows, Encoding = (long) encoding}, }, cancellationToken: cancellationToken).ConfigureAwait(false); } catch (RpcException exception) @@ -366,14 +535,14 @@ public async Task ExecuteBatchAsync(Connection connection, ExecuteBatchD public void CloseRows(Rows rows) { - TranslateException(() => _client.CloseRows(ToProto(rows))); + TranslateException(() => Client.CloseRows(ToProto(rows))); } public async Task CloseRowsAsync(Rows rows, CancellationToken cancellationToken = default) { try { - await _client.CloseRowsAsync(ToProto(rows), cancellationToken: cancellationToken).ConfigureAwait(false); + await Client.CloseRowsAsync(ToProto(rows), cancellationToken: cancellationToken).ConfigureAwait(false); } catch (RpcException exception) { @@ -383,24 +552,84 @@ public async Task CloseRowsAsync(Rows rows, CancellationToken cancellationToken public void BeginTransaction(Connection connection, TransactionOptions transactionOptions) { - TranslateException(() => _client.BeginTransaction(new BeginTransactionRequest + if (_communicationStyle == CommunicationStyle.BidiStreaming && connection is GrpcConnection grpcConnection) + { + BeginTransactionBidi(grpcConnection, transactionOptions); + return; + } + TranslateException(() => Client.BeginTransaction(new BeginTransactionRequest { Connection = ToProto(connection), TransactionOptions = transactionOptions, })); } + private void BeginTransactionBidi(GrpcConnection connection, TransactionOptions transactionOptions) + { + ExecuteBidi(connection, new ConnectionStreamRequest + { + BeginTransactionRequest = new BeginTransactionRequest + { + Connection = ToProto(connection), + TransactionOptions = transactionOptions, + } + }); + } + + public async Task BeginTransactionAsync(Connection connection, TransactionOptions transactionOptions, CancellationToken cancellationToken = default) + { + if (_communicationStyle == CommunicationStyle.BidiStreaming && connection is GrpcConnection grpcConnection) + { + await BeginTransactionBidiAsync(grpcConnection, transactionOptions, cancellationToken).ConfigureAwait(false); + return; + } + await TranslateException(() => Client.BeginTransactionAsync(new BeginTransactionRequest + { + Connection = ToProto(connection), + TransactionOptions = transactionOptions, + })).ConfigureAwait(false); + } + + private async Task BeginTransactionBidiAsync(GrpcConnection connection, TransactionOptions transactionOptions, CancellationToken cancellationToken) + { + await ExecuteBidiAsync(connection, new ConnectionStreamRequest + { + BeginTransactionRequest = new BeginTransactionRequest + { + Connection = ToProto(connection), + TransactionOptions = transactionOptions, + } + }, cancellationToken).ConfigureAwait(false); + } + public CommitResponse? Commit(Connection connection) { - var response = TranslateException(() => _client.Commit(ToProto(connection))); + if (_communicationStyle == CommunicationStyle.BidiStreaming && connection is GrpcConnection grpcConnection) + { + return CommitBidi(grpcConnection); + } + var response = TranslateException(() => Client.Commit(ToProto(connection))); return response.CommitTimestamp == null ? null : response; } + private CommitResponse? CommitBidi(GrpcConnection connection) + { + var response = ExecuteBidi(connection, new ConnectionStreamRequest + { + CommitRequest = ToProto(connection), + }); + return response.CommitResponse.CommitTimestamp == null ? null : response.CommitResponse; + } + public async Task CommitAsync(Connection connection, CancellationToken cancellationToken = default) { + if (_communicationStyle == CommunicationStyle.BidiStreaming && connection is GrpcConnection grpcConnection) + { + return await CommitBidiAsync(grpcConnection, cancellationToken).ConfigureAwait(false); + } try { - var response = await _client.CommitAsync(ToProto(connection), cancellationToken: cancellationToken).ConfigureAwait(false); + var response = await Client.CommitAsync(ToProto(connection), cancellationToken: cancellationToken).ConfigureAwait(false); return response.CommitTimestamp == null ? null : response; } catch (RpcException exception) @@ -409,16 +638,43 @@ public void BeginTransaction(Connection connection, TransactionOptions transacti } } + private async Task CommitBidiAsync(GrpcConnection connection, CancellationToken cancellationToken) + { + var response = await ExecuteBidiAsync(connection, new ConnectionStreamRequest + { + CommitRequest = ToProto(connection), + }, cancellationToken).ConfigureAwait(false); + return response.CommitResponse.CommitTimestamp == null ? null : response.CommitResponse; + } + public void Rollback(Connection connection) { - TranslateException(() => _client.Rollback(ToProto(connection))); + if (_communicationStyle == CommunicationStyle.BidiStreaming && connection is GrpcConnection grpcConnection) + { + RollbackBidi(grpcConnection); + return; + } + TranslateException(() => Client.Rollback(ToProto(connection))); + } + + private void RollbackBidi(GrpcConnection connection) + { + ExecuteBidi(connection, new ConnectionStreamRequest + { + RollbackRequest = ToProto(connection), + }); } public async Task RollbackAsync(Connection connection, CancellationToken cancellationToken = default) { + if (_communicationStyle == CommunicationStyle.BidiStreaming && connection is GrpcConnection grpcConnection) + { + await RollbackBidiAsync(grpcConnection, cancellationToken).ConfigureAwait(false); + return; + } try { - await _client.RollbackAsync(ToProto(connection), cancellationToken: cancellationToken).ConfigureAwait(false); + await Client.RollbackAsync(ToProto(connection), cancellationToken: cancellationToken).ConfigureAwait(false); } catch (RpcException exception) { @@ -426,15 +682,23 @@ public async Task RollbackAsync(Connection connection, CancellationToken cancell } } - Pool FromProto(V1.Pool pool) => new(this, pool.Id); + private async Task RollbackBidiAsync(GrpcConnection connection, CancellationToken cancellationToken) + { + await ExecuteBidiAsync(connection, new ConnectionStreamRequest + { + RollbackRequest = ToProto(connection), + }, cancellationToken).ConfigureAwait(false); + } + + private Pool FromProto(V1.Pool pool) => new(this, pool.Id); - V1.Pool ToProto(Pool pool) => new() { Id = pool.Id }; + private V1.Pool ToProto(Pool pool) => new() { Id = pool.Id }; - Connection FromProto(Pool pool, V1.Connection proto) => new(pool, proto.Id); + private GrpcConnection FromProto(Pool pool, V1.Connection proto) => new(_clients[Random.Shared.Next(_clients.Length)], pool, proto.Id); - V1.Connection ToProto(Connection connection) => new() { Id = connection.Id, Pool = ToProto(connection.Pool), }; + private V1.Connection ToProto(Connection connection) => new() { Id = connection.Id, Pool = ToProto(connection.Pool), }; - Rows FromProto(Connection connection, V1.Rows proto) => new(connection, proto.Id); + private Rows FromProto(Connection connection, V1.Rows proto) => new(connection, proto.Id); - V1.Rows ToProto(Rows rows) => new() { Id = rows.Id, Connection = ToProto(rows.SpannerConnection), }; + private V1.Rows ToProto(Rows rows) => new() { Id = rows.Id, Connection = ToProto(rows.SpannerConnection), }; } \ No newline at end of file diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/StreamingRows.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/StreamingRows.cs index 27a11271..b7a31d67 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/StreamingRows.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/StreamingRows.cs @@ -1,9 +1,25 @@ +// Copyright 2025 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 +// +// https://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. + +using System; using System.Threading; using System.Threading.Tasks; using Google.Api.Gax; using Google.Cloud.Spanner.V1; using Google.Cloud.SpannerLib.V1; using Google.Protobuf.WellKnownTypes; +using Google.Rpc; using Grpc.Core; using Status = Google.Rpc.Status; @@ -11,44 +27,105 @@ namespace Google.Cloud.SpannerLib.Grpc; public class StreamingRows : Rows { - private readonly AsyncServerStreamingCall _stream; + private readonly GrpcLibSpanner _spanner; + private readonly ExecuteResponse? _executeResponse; + private AsyncServerStreamingCall? _stream; private ListValue? _pendingRow; private ResultSetMetadata? _metadata; private ResultSetStats? _stats; private bool _done; private bool _pendingNextResultSetCall; - protected override ResultSetStats? Stats => _stats; + private bool HasOnlyInMemResults => !_executeResponse?.HasMoreResults ?? false; + private bool HasMoreInMemRows => + _executeResponse != null + && _currentResultSetIndex < _executeResponse.ResultSets.Count + && (_currentResultSetIndex < _executeResponse.ResultSets.Count-1 || _currentRowIndex < _executeResponse.ResultSets[_currentResultSetIndex].Rows.Count-1); + private bool IsPositionedAtInMemResultSet => + _executeResponse != null + && _currentResultSetIndex < _executeResponse.ResultSets.Count; + private bool IsPositionedAtInMemResultSetWithAllData => + IsPositionedAtInMemResultSet + && (_currentResultSetIndex < _executeResponse!.ResultSets.Count - 1 || !_executeResponse.HasMoreResults); + private ResultSet CurrentInMemResultSet => _executeResponse!.ResultSets[_currentResultSetIndex]; + + private int _currentResultSetIndex; + private int _currentRowIndex = -1; + + private AsyncServerStreamingCall Stream => _stream!; + + protected override ResultSetStats? Stats => IsPositionedAtInMemResultSetWithAllData ? CurrentInMemResultSet.Stats : _stats; + + public override ResultSetMetadata? Metadata => IsPositionedAtInMemResultSet ? CurrentInMemResultSet.Metadata : _metadata; - public override ResultSetMetadata? Metadata => _metadata; + internal static StreamingRows Create(GrpcLibSpanner spanner, Connection connection, AsyncServerStreamingCall stream) + { + var rows = new StreamingRows(spanner, connection, stream); + rows._pendingRow = rows.Next(); + return rows; + } - public static StreamingRows Create(Connection connection, AsyncServerStreamingCall stream) + internal static StreamingRows Create(GrpcLibSpanner spanner, Connection connection, ExecuteResponse response) { - var rows = new StreamingRows(connection, stream); + var rows = new StreamingRows(spanner, connection, response); rows._pendingRow = rows.Next(); return rows; } - public static async Task CreateAsync(Connection connection, AsyncServerStreamingCall stream, CancellationToken cancellationToken = default) + internal static async Task CreateAsync(GrpcLibSpanner spanner, Connection connection, AsyncServerStreamingCall stream, CancellationToken cancellationToken) + { + var rows = new StreamingRows(spanner, connection, stream); + rows._pendingRow = await rows.NextAsync(cancellationToken).ConfigureAwait(false); + return rows; + } + + internal static async Task CreateAsync(GrpcLibSpanner spanner, Connection connection, ExecuteResponse response, CancellationToken cancellationToken) { - var rows = new StreamingRows(connection, stream); + var rows = new StreamingRows(spanner, connection, response); rows._pendingRow = await rows.NextAsync(cancellationToken).ConfigureAwait(false); return rows; } - private StreamingRows(Connection connection, AsyncServerStreamingCall stream) : base(connection, 0, initMetadata: false) + private StreamingRows(GrpcLibSpanner spanner, Connection connection, AsyncServerStreamingCall stream) : base(connection, 0, initMetadata: false) { + _spanner = spanner; _stream = stream; + _executeResponse = null; + } + + private StreamingRows(GrpcLibSpanner spanner, Connection connection, ExecuteResponse response) : base(connection, response.Rows.Id, initMetadata: false) + { + _spanner = spanner; + _stream = null; + _executeResponse = response; } protected override void Dispose(bool disposing) + { + Cleanup(); + if (_stream == null && (_executeResponse?.HasMoreResults ?? true)) + { + base.Dispose(disposing); + } + } + + protected override ValueTask DisposeAsyncCore() + { + Cleanup(); + if (_stream == null && (_executeResponse?.HasMoreResults ?? true)) + { + return base.DisposeAsyncCore(); + } + return ValueTask.CompletedTask; + } + + private void Cleanup() { if (!_done) { MarkDone(); } - _stream.Dispose(); - base.Dispose(disposing); + _stream?.Dispose(); } private void MarkDone() @@ -56,27 +133,43 @@ private void MarkDone() _done = true; } - public override ListValue? Next() + private bool TryNextCached(out ListValue? result) { - // TODO: Combine sync and async methods - if (_pendingNextResultSetCall) + if (_pendingNextResultSetCall || _done) { - return null; + result = null; + return true; } if (_pendingRow != null) { - var row = _pendingRow; + result = _pendingRow; _pendingRow = null; - return row; + return true; + } + if (HasOnlyInMemResults || HasMoreInMemRows) + { + result = NextInMem(); + return true; } + result = null; + return false; + } + + public override ListValue? Next() + { + if (TryNextCached(out var result)) + { + return result; + } + _stream ??= _spanner.ContinueStreaming(SpannerConnection, Id); try { - var hasNext = Task.Run(() => _stream.ResponseStream.MoveNext()).ResultWithUnwrappedExceptions(); + var hasNext = Task.Run(() => Stream.ResponseStream.MoveNext()).ResultWithUnwrappedExceptions(); if (!hasNext) { MarkDone(); return null; } - var rowData = _stream.ResponseStream.Current; + var rowData = Stream.ResponseStream.Current; if (rowData.Metadata != null) { _metadata = rowData.Metadata; @@ -107,24 +200,20 @@ private void MarkDone() public override async Task NextAsync(CancellationToken cancellationToken = default) { - if (_pendingNextResultSetCall) + if (TryNextCached(out var result)) { - return null; - } - if (_pendingRow != null) { - var row = _pendingRow; - _pendingRow = null; - return row; + return result; } + _stream ??= _spanner.ContinueStreamingAsync(SpannerConnection, Id, cancellationToken); try { - var hasNext = await _stream.ResponseStream.MoveNext(cancellationToken).ConfigureAwait(false); + var hasNext = await Stream.ResponseStream.MoveNext(cancellationToken).ConfigureAwait(false); if (!hasNext) { MarkDone(); return null; } - var rowData = _stream.ResponseStream.Current; + var rowData = Stream.ResponseStream.Current; if (rowData.Metadata != null) { _metadata = rowData.Metadata; @@ -153,6 +242,37 @@ private void MarkDone() } } + /// + /// Returns the next row based on the cached in-memory results. + /// This method assumes that the cursor is positioned at an in-memory result. + /// + private ListValue? NextInMem() + { + GaxPreconditions.CheckNotNull(_executeResponse, nameof(_executeResponse)); + if (_currentResultSetIndex == _executeResponse!.ResultSets.Count) + { + return null; + } + _currentRowIndex = Math.Min(_currentRowIndex + 1, CurrentInMemResultSet.Rows.Count); + return _currentRowIndex == CurrentInMemResultSet.Rows.Count ? null : CurrentInMemResultSet.Rows[_currentRowIndex]; + } + + private bool TryNextResultSetInMem(out bool result) + { + if (HasOnlyInMemResults) + { + result = NextResultSetInMem(); + return true; + } + if (_executeResponse != null && _currentResultSetIndex < _executeResponse.ResultSets.Count-1) + { + result = NextResultSetInMem(); + return true; + } + result = false; + return false; + } + /// /// Moves the cursor to the next result set in this Rows object. /// @@ -163,10 +283,18 @@ public override bool NextResultSet() { return false; } + if (TryNextResultSetInMem(out var result)) + { + return result; + } + _stream ??= _spanner.ContinueStreaming(SpannerConnection, Id); + // Read data until we reach the next result set. ReadUntilEnd(); - return HasNextResultSet(); + var hasNextResultSet = HasNextResultSet(); + _pendingRow = Next(); + return hasNextResultSet; } /// @@ -179,16 +307,42 @@ public override async Task NextResultSetAsync(CancellationToken cancellati { return false; } + if (TryNextResultSetInMem(out var result)) + { + return result; + } + _stream ??= _spanner.ContinueStreaming(SpannerConnection, Id); + // Read data until we reach the next result set. await ReadUntilEndAsync(cancellationToken).ConfigureAwait(false); - return HasNextResultSet(); + var hasNextResultSet = HasNextResultSet(); + _pendingRow = await NextAsync(cancellationToken).ConfigureAwait(false); + return hasNextResultSet; + } + + private bool NextResultSetInMem() + { + GaxPreconditions.CheckNotNull(_executeResponse, nameof(_executeResponse)); + if (_currentResultSetIndex == _executeResponse!.ResultSets.Count - 1) + { + if (_executeResponse.Status != null && _executeResponse.Status.Code != (int)Code.Ok) + { + throw new SpannerException(_executeResponse.Status); + } + return false; + } + _currentResultSetIndex++; + _currentRowIndex = -1; + return true; } private bool HasNextResultSet() { if (_pendingNextResultSetCall) { + _stats = null; + _metadata = null; _pendingNextResultSetCall = false; return true; } diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/spannerlib-dotnet-grpc-impl.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/spannerlib-dotnet-grpc-impl.csproj index 8c8277f5..67fec147 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/spannerlib-dotnet-grpc-impl.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/spannerlib-dotnet-grpc-impl.csproj @@ -6,13 +6,17 @@ enable default Alpha.Google.Cloud.SpannerLib.GrpcImpl - 1.0.0-alpha.20251027150914 + 1.0.0-alpha.20251207191144 Google - + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-server/spannerlib-dotnet-grpc-server.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-server/spannerlib-dotnet-grpc-server.csproj index 66b1b1b2..53475b7a 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-server/spannerlib-dotnet-grpc-server.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-server/spannerlib-dotnet-grpc-server.csproj @@ -8,7 +8,7 @@ Alpha.Google.Cloud.SpannerLib.GrpcServer SpannerLib Grpc Server Google - 1.0.0-alpha.20251027150914 + 1.0.0-alpha.20251207191144 diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-v1/Spannerlib.g.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-v1/Spannerlib.g.cs index c83094e1..11e6a291 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-v1/Spannerlib.g.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-v1/Spannerlib.g.cs @@ -27,106 +27,134 @@ static SpannerlibReflection() { "CiVnb29nbGUvc3Bhbm5lcmxpYi92MS9zcGFubmVybGliLnByb3RvEhRnb29n", "bGUuc3Bhbm5lcmxpYi52MRofZ29vZ2xlL2FwaS9maWVsZF9iZWhhdmlvci5w", "cm90bxobZ29vZ2xlL3Byb3RvYnVmL2VtcHR5LnByb3RvGhxnb29nbGUvcHJv", - "dG9idWYvc3RydWN0LnByb3RvGiJnb29nbGUvc3Bhbm5lci92MS9yZXN1bHRf", - "c2V0LnByb3RvGh9nb29nbGUvc3Bhbm5lci92MS9zcGFubmVyLnByb3RvGiNn", - "b29nbGUvc3Bhbm5lci92MS90cmFuc2FjdGlvbi5wcm90byINCgtJbmZvUmVx", - "dWVzdCIfCgxJbmZvUmVzcG9uc2USDwoHdmVyc2lvbhgBIAEoCSIzChFDcmVh", - "dGVQb29sUmVxdWVzdBIeChFjb25uZWN0aW9uX3N0cmluZxgBIAEoCUID4EEC", - "IkgKF0NyZWF0ZUNvbm5lY3Rpb25SZXF1ZXN0Ei0KBHBvb2wYASABKAsyGi5n", - "b29nbGUuc3Bhbm5lcmxpYi52MS5Qb29sQgPgQQIikwEKDkV4ZWN1dGVSZXF1", - "ZXN0EjkKCmNvbm5lY3Rpb24YASABKAsyIC5nb29nbGUuc3Bhbm5lcmxpYi52", - "MS5Db25uZWN0aW9uQgPgQQISRgoTZXhlY3V0ZV9zcWxfcmVxdWVzdBgCIAEo", - "CzIkLmdvb2dsZS5zcGFubmVyLnYxLkV4ZWN1dGVTcWxSZXF1ZXN0QgPgQQIi", - "owEKE0V4ZWN1dGVCYXRjaFJlcXVlc3QSOQoKY29ubmVjdGlvbhgBIAEoCzIg", - "Lmdvb2dsZS5zcGFubmVybGliLnYxLkNvbm5lY3Rpb25CA+BBAhJRChlleGVj", - "dXRlX2JhdGNoX2RtbF9yZXF1ZXN0GAIgASgLMikuZ29vZ2xlLnNwYW5uZXIu", - "djEuRXhlY3V0ZUJhdGNoRG1sUmVxdWVzdEID4EECIp0BChdCZWdpblRyYW5z", - "YWN0aW9uUmVxdWVzdBI5Cgpjb25uZWN0aW9uGAEgASgLMiAuZ29vZ2xlLnNw", - "YW5uZXJsaWIudjEuQ29ubmVjdGlvbkID4EECEkcKE3RyYW5zYWN0aW9uX29w", - "dGlvbnMYAiABKAsyJS5nb29nbGUuc3Bhbm5lci52MS5UcmFuc2FjdGlvbk9w", - "dGlvbnNCA+BBAiKeAQoVV3JpdGVNdXRhdGlvbnNSZXF1ZXN0EjkKCmNvbm5l", - "Y3Rpb24YASABKAsyIC5nb29nbGUuc3Bhbm5lcmxpYi52MS5Db25uZWN0aW9u", - "QgPgQQISSgoJbXV0YXRpb25zGAIgASgLMjIuZ29vZ2xlLnNwYW5uZXIudjEu", - "QmF0Y2hXcml0ZVJlcXVlc3QuTXV0YXRpb25Hcm91cEID4EECIhcKBFBvb2wS", - "DwoCaWQYASABKANCA+BBAiJMCgpDb25uZWN0aW9uEi0KBHBvb2wYASABKAsy", - "Gi5nb29nbGUuc3Bhbm5lcmxpYi52MS5Qb29sQgPgQQISDwoCaWQYAiABKANC", - "A+BBAiJSCgRSb3dzEjkKCmNvbm5lY3Rpb24YASABKAsyIC5nb29nbGUuc3Bh", - "bm5lcmxpYi52MS5Db25uZWN0aW9uQgPgQQISDwoCaWQYAiABKANCA+BBAiJq", - "CgtOZXh0UmVxdWVzdBItCgRyb3dzGAEgASgLMhouZ29vZ2xlLnNwYW5uZXJs", - "aWIudjEuUm93c0ID4EECEhUKCG51bV9yb3dzGAIgASgDQgPgQQISFQoIZW5j", - "b2RpbmcYAyABKANCA+BBAiLrAQoHUm93RGF0YRItCgRyb3dzGAEgASgLMhou", - "Z29vZ2xlLnNwYW5uZXJsaWIudjEuUm93c0ID4EECEjYKCG1ldGFkYXRhGAIg", - "ASgLMiQuZ29vZ2xlLnNwYW5uZXIudjEuUmVzdWx0U2V0TWV0YWRhdGESLQoE", - "ZGF0YRgDIAMoCzIaLmdvb2dsZS5wcm90b2J1Zi5MaXN0VmFsdWVCA+BBAhIw", - "CgVzdGF0cxgEIAEoCzIhLmdvb2dsZS5zcGFubmVyLnYxLlJlc3VsdFNldFN0", - "YXRzEhgKEGhhc19tb3JlX3Jlc3VsdHMYBSABKAgiQAoPTWV0YWRhdGFSZXF1", - "ZXN0Ei0KBHJvd3MYASABKAsyGi5nb29nbGUuc3Bhbm5lcmxpYi52MS5Sb3dz", - "QgPgQQIiRgoVUmVzdWx0U2V0U3RhdHNSZXF1ZXN0Ei0KBHJvd3MYASABKAsy", - "Gi5nb29nbGUuc3Bhbm5lcmxpYi52MS5Sb3dzQgPgQQIiZQoXQ29ubmVjdGlv", - "blN0cmVhbVJlcXVlc3QSPwoPZXhlY3V0ZV9yZXF1ZXN0GAEgASgLMiQuZ29v", - "Z2xlLnNwYW5uZXJsaWIudjEuRXhlY3V0ZVJlcXVlc3RIAEIJCgdyZXF1ZXN0", - "IloKGENvbm5lY3Rpb25TdHJlYW1SZXNwb25zZRIyCgNyb3cYASABKAsyIy5n", - "b29nbGUuc3Bhbm5lci52MS5QYXJ0aWFsUmVzdWx0U2V0SABCCgoIcmVzcG9u", - "c2UylwwKClNwYW5uZXJMaWISTwoESW5mbxIhLmdvb2dsZS5zcGFubmVybGli", - "LnYxLkluZm9SZXF1ZXN0GiIuZ29vZ2xlLnNwYW5uZXJsaWIudjEuSW5mb1Jl", - "c3BvbnNlIgASUwoKQ3JlYXRlUG9vbBInLmdvb2dsZS5zcGFubmVybGliLnYx", - "LkNyZWF0ZVBvb2xSZXF1ZXN0GhouZ29vZ2xlLnNwYW5uZXJsaWIudjEuUG9v", - "bCIAEkEKCUNsb3NlUG9vbBIaLmdvb2dsZS5zcGFubmVybGliLnYxLlBvb2wa", - "Fi5nb29nbGUucHJvdG9idWYuRW1wdHkiABJlChBDcmVhdGVDb25uZWN0aW9u", - "Ei0uZ29vZ2xlLnNwYW5uZXJsaWIudjEuQ3JlYXRlQ29ubmVjdGlvblJlcXVl", - "c3QaIC5nb29nbGUuc3Bhbm5lcmxpYi52MS5Db25uZWN0aW9uIgASTQoPQ2xv", - "c2VDb25uZWN0aW9uEiAuZ29vZ2xlLnNwYW5uZXJsaWIudjEuQ29ubmVjdGlv", - "bhoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIAEk0KB0V4ZWN1dGUSJC5nb29n", - "bGUuc3Bhbm5lcmxpYi52MS5FeGVjdXRlUmVxdWVzdBoaLmdvb2dsZS5zcGFu", - "bmVybGliLnYxLlJvd3MiABJbChBFeGVjdXRlU3RyZWFtaW5nEiQuZ29vZ2xl", - "LnNwYW5uZXJsaWIudjEuRXhlY3V0ZVJlcXVlc3QaHS5nb29nbGUuc3Bhbm5l", - "cmxpYi52MS5Sb3dEYXRhIgAwARJnCgxFeGVjdXRlQmF0Y2gSKS5nb29nbGUu", - "c3Bhbm5lcmxpYi52MS5FeGVjdXRlQmF0Y2hSZXF1ZXN0GiouZ29vZ2xlLnNw", - "YW5uZXIudjEuRXhlY3V0ZUJhdGNoRG1sUmVzcG9uc2UiABJOCghNZXRhZGF0", - "YRIaLmdvb2dsZS5zcGFubmVybGliLnYxLlJvd3MaJC5nb29nbGUuc3Bhbm5l", - "ci52MS5SZXN1bHRTZXRNZXRhZGF0YSIAEkcKBE5leHQSIS5nb29nbGUuc3Bh", - "bm5lcmxpYi52MS5OZXh0UmVxdWVzdBoaLmdvb2dsZS5wcm90b2J1Zi5MaXN0", - "VmFsdWUiABJRCg5SZXN1bHRTZXRTdGF0cxIaLmdvb2dsZS5zcGFubmVybGli", - "LnYxLlJvd3MaIS5nb29nbGUuc3Bhbm5lci52MS5SZXN1bHRTZXRTdGF0cyIA", - "ElMKDU5leHRSZXN1bHRTZXQSGi5nb29nbGUuc3Bhbm5lcmxpYi52MS5Sb3dz", - "GiQuZ29vZ2xlLnNwYW5uZXIudjEuUmVzdWx0U2V0TWV0YWRhdGEiABJBCglD", - "bG9zZVJvd3MSGi5nb29nbGUuc3Bhbm5lcmxpYi52MS5Sb3dzGhYuZ29vZ2xl", - "LnByb3RvYnVmLkVtcHR5IgASWwoQQmVnaW5UcmFuc2FjdGlvbhItLmdvb2ds", - "ZS5zcGFubmVybGliLnYxLkJlZ2luVHJhbnNhY3Rpb25SZXF1ZXN0GhYuZ29v", - "Z2xlLnByb3RvYnVmLkVtcHR5IgASTwoGQ29tbWl0EiAuZ29vZ2xlLnNwYW5u", - "ZXJsaWIudjEuQ29ubmVjdGlvbhohLmdvb2dsZS5zcGFubmVyLnYxLkNvbW1p", - "dFJlc3BvbnNlIgASRgoIUm9sbGJhY2sSIC5nb29nbGUuc3Bhbm5lcmxpYi52", - "MS5Db25uZWN0aW9uGhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IgASYgoOV3Jp", - "dGVNdXRhdGlvbnMSKy5nb29nbGUuc3Bhbm5lcmxpYi52MS5Xcml0ZU11dGF0", - "aW9uc1JlcXVlc3QaIS5nb29nbGUuc3Bhbm5lci52MS5Db21taXRSZXNwb25z", - "ZSIAEncKEENvbm5lY3Rpb25TdHJlYW0SLS5nb29nbGUuc3Bhbm5lcmxpYi52", - "MS5Db25uZWN0aW9uU3RyZWFtUmVxdWVzdBouLmdvb2dsZS5zcGFubmVybGli", - "LnYxLkNvbm5lY3Rpb25TdHJlYW1SZXNwb25zZSIAKAEwAULNAQoeY29tLmdv", - "b2dsZS5jbG91ZC5zcGFubmVybGliLnYxQg9TcGFubmVyTGliUHJvdG9QAVo+", - "Y2xvdWQuZ29vZ2xlLmNvbS9nby9zcGFubmVybGliL2FwaXYxL3NwYW5uZXJs", - "aWJwYjtzcGFubmVybGlicGKqAhpHb29nbGUuQ2xvdWQuU3Bhbm5lckxpYi5W", - "McoCGkdvb2dsZVxDbG91ZFxTcGFubmVyTGliXFYx6gIdR29vZ2xlOjpDbG91", - "ZDo6U3Bhbm5lckxpYjo6VjFiBnByb3RvMw==")); + "dG9idWYvc3RydWN0LnByb3RvGhdnb29nbGUvcnBjL3N0YXR1cy5wcm90bxoi", + "Z29vZ2xlL3NwYW5uZXIvdjEvcmVzdWx0X3NldC5wcm90bxofZ29vZ2xlL3Nw", + "YW5uZXIvdjEvc3Bhbm5lci5wcm90bxojZ29vZ2xlL3NwYW5uZXIvdjEvdHJh", + "bnNhY3Rpb24ucHJvdG8iDQoLSW5mb1JlcXVlc3QiHwoMSW5mb1Jlc3BvbnNl", + "Eg8KB3ZlcnNpb24YASABKAkiMwoRQ3JlYXRlUG9vbFJlcXVlc3QSHgoRY29u", + "bmVjdGlvbl9zdHJpbmcYASABKAlCA+BBAiJIChdDcmVhdGVDb25uZWN0aW9u", + "UmVxdWVzdBItCgRwb29sGAEgASgLMhouZ29vZ2xlLnNwYW5uZXJsaWIudjEu", + "UG9vbEID4EECIs4BCg5FeGVjdXRlUmVxdWVzdBI5Cgpjb25uZWN0aW9uGAEg", + "ASgLMiAuZ29vZ2xlLnNwYW5uZXJsaWIudjEuQ29ubmVjdGlvbkID4EECEkYK", + "E2V4ZWN1dGVfc3FsX3JlcXVlc3QYAiABKAsyJC5nb29nbGUuc3Bhbm5lci52", + "MS5FeGVjdXRlU3FsUmVxdWVzdEID4EECEjkKDWZldGNoX29wdGlvbnMYAyAB", + "KAsyIi5nb29nbGUuc3Bhbm5lcmxpYi52MS5GZXRjaE9wdGlvbnMiowEKE0V4", + "ZWN1dGVCYXRjaFJlcXVlc3QSOQoKY29ubmVjdGlvbhgBIAEoCzIgLmdvb2ds", + "ZS5zcGFubmVybGliLnYxLkNvbm5lY3Rpb25CA+BBAhJRChlleGVjdXRlX2Jh", + "dGNoX2RtbF9yZXF1ZXN0GAIgASgLMikuZ29vZ2xlLnNwYW5uZXIudjEuRXhl", + "Y3V0ZUJhdGNoRG1sUmVxdWVzdEID4EECIp0BChdCZWdpblRyYW5zYWN0aW9u", + "UmVxdWVzdBI5Cgpjb25uZWN0aW9uGAEgASgLMiAuZ29vZ2xlLnNwYW5uZXJs", + "aWIudjEuQ29ubmVjdGlvbkID4EECEkcKE3RyYW5zYWN0aW9uX29wdGlvbnMY", + "AiABKAsyJS5nb29nbGUuc3Bhbm5lci52MS5UcmFuc2FjdGlvbk9wdGlvbnNC", + "A+BBAiKeAQoVV3JpdGVNdXRhdGlvbnNSZXF1ZXN0EjkKCmNvbm5lY3Rpb24Y", + "ASABKAsyIC5nb29nbGUuc3Bhbm5lcmxpYi52MS5Db25uZWN0aW9uQgPgQQIS", + "SgoJbXV0YXRpb25zGAIgASgLMjIuZ29vZ2xlLnNwYW5uZXIudjEuQmF0Y2hX", + "cml0ZVJlcXVlc3QuTXV0YXRpb25Hcm91cEID4EECIhcKBFBvb2wSDwoCaWQY", + "ASABKANCA+BBAiJMCgpDb25uZWN0aW9uEi0KBHBvb2wYASABKAsyGi5nb29n", + "bGUuc3Bhbm5lcmxpYi52MS5Qb29sQgPgQQISDwoCaWQYAiABKANCA+BBAiJS", + "CgRSb3dzEjkKCmNvbm5lY3Rpb24YASABKAsyIC5nb29nbGUuc3Bhbm5lcmxp", + "Yi52MS5Db25uZWN0aW9uQgPgQQISDwoCaWQYAiABKANCA+BBAiJ8CgtOZXh0", + "UmVxdWVzdBItCgRyb3dzGAEgASgLMhouZ29vZ2xlLnNwYW5uZXJsaWIudjEu", + "Um93c0ID4EECEj4KDWZldGNoX29wdGlvbnMYAiABKAsyIi5nb29nbGUuc3Bh", + "bm5lcmxpYi52MS5GZXRjaE9wdGlvbnNCA+BBAiI8CgxGZXRjaE9wdGlvbnMS", + "FQoIbnVtX3Jvd3MYASABKANCA+BBAhIVCghlbmNvZGluZxgCIAEoA0ID4EEC", + "Iv8BCgdSb3dEYXRhEhIKCnJlcXVlc3RfaWQYASABKAkSLQoEcm93cxgCIAEo", + "CzIaLmdvb2dsZS5zcGFubmVybGliLnYxLlJvd3NCA+BBAhI2CghtZXRhZGF0", + "YRgDIAEoCzIkLmdvb2dsZS5zcGFubmVyLnYxLlJlc3VsdFNldE1ldGFkYXRh", + "Ei0KBGRhdGEYBCADKAsyGi5nb29nbGUucHJvdG9idWYuTGlzdFZhbHVlQgPg", + "QQISMAoFc3RhdHMYBSABKAsyIS5nb29nbGUuc3Bhbm5lci52MS5SZXN1bHRT", + "ZXRTdGF0cxIYChBoYXNfbW9yZV9yZXN1bHRzGAYgASgIIkAKD01ldGFkYXRh", + "UmVxdWVzdBItCgRyb3dzGAEgASgLMhouZ29vZ2xlLnNwYW5uZXJsaWIudjEu", + "Um93c0ID4EECIkYKFVJlc3VsdFNldFN0YXRzUmVxdWVzdBItCgRyb3dzGAEg", + "ASgLMhouZ29vZ2xlLnNwYW5uZXJsaWIudjEuUm93c0ID4EECIs8DChdDb25u", + "ZWN0aW9uU3RyZWFtUmVxdWVzdBI/Cg9leGVjdXRlX3JlcXVlc3QYASABKAsy", + "JC5nb29nbGUuc3Bhbm5lcmxpYi52MS5FeGVjdXRlUmVxdWVzdEgAEkoKFWV4", + "ZWN1dGVfYmF0Y2hfcmVxdWVzdBgCIAEoCzIpLmdvb2dsZS5zcGFubmVybGli", + "LnYxLkV4ZWN1dGVCYXRjaFJlcXVlc3RIABJSChliZWdpbl90cmFuc2FjdGlv", + "bl9yZXF1ZXN0GAMgASgLMi0uZ29vZ2xlLnNwYW5uZXJsaWIudjEuQmVnaW5U", + "cmFuc2FjdGlvblJlcXVlc3RIABI6Cg5jb21taXRfcmVxdWVzdBgEIAEoCzIg", + "Lmdvb2dsZS5zcGFubmVybGliLnYxLkNvbm5lY3Rpb25IABI8ChByb2xsYmFj", + "a19yZXF1ZXN0GAUgASgLMiAuZ29vZ2xlLnNwYW5uZXJsaWIudjEuQ29ubmVj", + "dGlvbkgAEk4KF3dyaXRlX211dGF0aW9uc19yZXF1ZXN0GAYgASgLMisuZ29v", + "Z2xlLnNwYW5uZXJsaWIudjEuV3JpdGVNdXRhdGlvbnNSZXF1ZXN0SABCCQoH", + "cmVxdWVzdCKxAQoPRXhlY3V0ZVJlc3BvbnNlEi0KBHJvd3MYASABKAsyGi5n", + "b29nbGUuc3Bhbm5lcmxpYi52MS5Sb3dzQgPgQQISMQoLcmVzdWx0X3NldHMY", + "AiADKAsyHC5nb29nbGUuc3Bhbm5lci52MS5SZXN1bHRTZXQSIgoGc3RhdHVz", + "GAMgASgLMhIuZ29vZ2xlLnJwYy5TdGF0dXMSGAoQaGFzX21vcmVfcmVzdWx0", + "cxgEIAEoCCLTAwoYQ29ubmVjdGlvblN0cmVhbVJlc3BvbnNlEiIKBnN0YXR1", + "cxgBIAEoCzISLmdvb2dsZS5ycGMuU3RhdHVzEkEKEGV4ZWN1dGVfcmVzcG9u", + "c2UYAiABKAsyJS5nb29nbGUuc3Bhbm5lcmxpYi52MS5FeGVjdXRlUmVzcG9u", + "c2VIABJMChZleGVjdXRlX2JhdGNoX3Jlc3BvbnNlGAMgASgLMiouZ29vZ2xl", + "LnNwYW5uZXIudjEuRXhlY3V0ZUJhdGNoRG1sUmVzcG9uc2VIABI8ChpiZWdp", + "bl90cmFuc2FjdGlvbl9yZXNwb25zZRgEIAEoCzIWLmdvb2dsZS5wcm90b2J1", + "Zi5FbXB0eUgAEjwKD2NvbW1pdF9yZXNwb25zZRgFIAEoCzIhLmdvb2dsZS5z", + "cGFubmVyLnYxLkNvbW1pdFJlc3BvbnNlSAASMwoRcm9sbGJhY2tfcmVzcG9u", + "c2UYBiABKAsyFi5nb29nbGUucHJvdG9idWYuRW1wdHlIABJFChh3cml0ZV9t", + "dXRhdGlvbnNfcmVzcG9uc2UYByABKAsyIS5nb29nbGUuc3Bhbm5lci52MS5D", + "b21taXRSZXNwb25zZUgAQgoKCHJlc3BvbnNlMusMCgpTcGFubmVyTGliEk8K", + "BEluZm8SIS5nb29nbGUuc3Bhbm5lcmxpYi52MS5JbmZvUmVxdWVzdBoiLmdv", + "b2dsZS5zcGFubmVybGliLnYxLkluZm9SZXNwb25zZSIAElMKCkNyZWF0ZVBv", + "b2wSJy5nb29nbGUuc3Bhbm5lcmxpYi52MS5DcmVhdGVQb29sUmVxdWVzdBoa", + "Lmdvb2dsZS5zcGFubmVybGliLnYxLlBvb2wiABJBCglDbG9zZVBvb2wSGi5n", + "b29nbGUuc3Bhbm5lcmxpYi52MS5Qb29sGhYuZ29vZ2xlLnByb3RvYnVmLkVt", + "cHR5IgASZQoQQ3JlYXRlQ29ubmVjdGlvbhItLmdvb2dsZS5zcGFubmVybGli", + "LnYxLkNyZWF0ZUNvbm5lY3Rpb25SZXF1ZXN0GiAuZ29vZ2xlLnNwYW5uZXJs", + "aWIudjEuQ29ubmVjdGlvbiIAEk0KD0Nsb3NlQ29ubmVjdGlvbhIgLmdvb2ds", + "ZS5zcGFubmVybGliLnYxLkNvbm5lY3Rpb24aFi5nb29nbGUucHJvdG9idWYu", + "RW1wdHkiABJNCgdFeGVjdXRlEiQuZ29vZ2xlLnNwYW5uZXJsaWIudjEuRXhl", + "Y3V0ZVJlcXVlc3QaGi5nb29nbGUuc3Bhbm5lcmxpYi52MS5Sb3dzIgASWwoQ", + "RXhlY3V0ZVN0cmVhbWluZxIkLmdvb2dsZS5zcGFubmVybGliLnYxLkV4ZWN1", + "dGVSZXF1ZXN0Gh0uZ29vZ2xlLnNwYW5uZXJsaWIudjEuUm93RGF0YSIAMAES", + "ZwoMRXhlY3V0ZUJhdGNoEikuZ29vZ2xlLnNwYW5uZXJsaWIudjEuRXhlY3V0", + "ZUJhdGNoUmVxdWVzdBoqLmdvb2dsZS5zcGFubmVyLnYxLkV4ZWN1dGVCYXRj", + "aERtbFJlc3BvbnNlIgASTgoITWV0YWRhdGESGi5nb29nbGUuc3Bhbm5lcmxp", + "Yi52MS5Sb3dzGiQuZ29vZ2xlLnNwYW5uZXIudjEuUmVzdWx0U2V0TWV0YWRh", + "dGEiABJHCgROZXh0EiEuZ29vZ2xlLnNwYW5uZXJsaWIudjEuTmV4dFJlcXVl", + "c3QaGi5nb29nbGUucHJvdG9idWYuTGlzdFZhbHVlIgASUQoOUmVzdWx0U2V0", + "U3RhdHMSGi5nb29nbGUuc3Bhbm5lcmxpYi52MS5Sb3dzGiEuZ29vZ2xlLnNw", + "YW5uZXIudjEuUmVzdWx0U2V0U3RhdHMiABJTCg1OZXh0UmVzdWx0U2V0Ehou", + "Z29vZ2xlLnNwYW5uZXJsaWIudjEuUm93cxokLmdvb2dsZS5zcGFubmVyLnYx", + "LlJlc3VsdFNldE1ldGFkYXRhIgASQQoJQ2xvc2VSb3dzEhouZ29vZ2xlLnNw", + "YW5uZXJsaWIudjEuUm93cxoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIAElsK", + "EEJlZ2luVHJhbnNhY3Rpb24SLS5nb29nbGUuc3Bhbm5lcmxpYi52MS5CZWdp", + "blRyYW5zYWN0aW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIA", + "Ek8KBkNvbW1pdBIgLmdvb2dsZS5zcGFubmVybGliLnYxLkNvbm5lY3Rpb24a", + "IS5nb29nbGUuc3Bhbm5lci52MS5Db21taXRSZXNwb25zZSIAEkYKCFJvbGxi", + "YWNrEiAuZ29vZ2xlLnNwYW5uZXJsaWIudjEuQ29ubmVjdGlvbhoWLmdvb2ds", + "ZS5wcm90b2J1Zi5FbXB0eSIAEmIKDldyaXRlTXV0YXRpb25zEisuZ29vZ2xl", + "LnNwYW5uZXJsaWIudjEuV3JpdGVNdXRhdGlvbnNSZXF1ZXN0GiEuZ29vZ2xl", + "LnNwYW5uZXIudjEuQ29tbWl0UmVzcG9uc2UiABJ3ChBDb25uZWN0aW9uU3Ry", + "ZWFtEi0uZ29vZ2xlLnNwYW5uZXJsaWIudjEuQ29ubmVjdGlvblN0cmVhbVJl", + "cXVlc3QaLi5nb29nbGUuc3Bhbm5lcmxpYi52MS5Db25uZWN0aW9uU3RyZWFt", + "UmVzcG9uc2UiACgBMAESUgoRQ29udGludWVTdHJlYW1pbmcSGi5nb29nbGUu", + "c3Bhbm5lcmxpYi52MS5Sb3dzGh0uZ29vZ2xlLnNwYW5uZXJsaWIudjEuUm93", + "RGF0YSIAMAFCzQEKHmNvbS5nb29nbGUuY2xvdWQuc3Bhbm5lcmxpYi52MUIP", + "U3Bhbm5lckxpYlByb3RvUAFaPmNsb3VkLmdvb2dsZS5jb20vZ28vc3Bhbm5l", + "cmxpYi9hcGl2MS9zcGFubmVybGlicGI7c3Bhbm5lcmxpYnBiqgIaR29vZ2xl", + "LkNsb3VkLlNwYW5uZXJMaWIuVjHKAhpHb29nbGVcQ2xvdWRcU3Bhbm5lckxp", + "YlxWMeoCHUdvb2dsZTo6Q2xvdWQ6OlNwYW5uZXJMaWI6OlYxYgZwcm90bzM=")); descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, - new pbr::FileDescriptor[] { global::Google.Api.FieldBehaviorReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.EmptyReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.StructReflection.Descriptor, global::Google.Cloud.Spanner.V1.ResultSetReflection.Descriptor, global::Google.Cloud.Spanner.V1.SpannerReflection.Descriptor, global::Google.Cloud.Spanner.V1.TransactionReflection.Descriptor, }, + new pbr::FileDescriptor[] { global::Google.Api.FieldBehaviorReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.EmptyReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.StructReflection.Descriptor, global::Google.Rpc.StatusReflection.Descriptor, global::Google.Cloud.Spanner.V1.ResultSetReflection.Descriptor, global::Google.Cloud.Spanner.V1.SpannerReflection.Descriptor, global::Google.Cloud.Spanner.V1.TransactionReflection.Descriptor, }, new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] { new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.InfoRequest), global::Google.Cloud.SpannerLib.V1.InfoRequest.Parser, null, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.InfoResponse), global::Google.Cloud.SpannerLib.V1.InfoResponse.Parser, new[]{ "Version" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.CreatePoolRequest), global::Google.Cloud.SpannerLib.V1.CreatePoolRequest.Parser, new[]{ "ConnectionString" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.CreateConnectionRequest), global::Google.Cloud.SpannerLib.V1.CreateConnectionRequest.Parser, new[]{ "Pool" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.ExecuteRequest), global::Google.Cloud.SpannerLib.V1.ExecuteRequest.Parser, new[]{ "Connection", "ExecuteSqlRequest" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.ExecuteRequest), global::Google.Cloud.SpannerLib.V1.ExecuteRequest.Parser, new[]{ "Connection", "ExecuteSqlRequest", "FetchOptions" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.ExecuteBatchRequest), global::Google.Cloud.SpannerLib.V1.ExecuteBatchRequest.Parser, new[]{ "Connection", "ExecuteBatchDmlRequest" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.BeginTransactionRequest), global::Google.Cloud.SpannerLib.V1.BeginTransactionRequest.Parser, new[]{ "Connection", "TransactionOptions" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.WriteMutationsRequest), global::Google.Cloud.SpannerLib.V1.WriteMutationsRequest.Parser, new[]{ "Connection", "Mutations" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.Pool), global::Google.Cloud.SpannerLib.V1.Pool.Parser, new[]{ "Id" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.Connection), global::Google.Cloud.SpannerLib.V1.Connection.Parser, new[]{ "Pool", "Id" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.Rows), global::Google.Cloud.SpannerLib.V1.Rows.Parser, new[]{ "Connection", "Id" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.NextRequest), global::Google.Cloud.SpannerLib.V1.NextRequest.Parser, new[]{ "Rows", "NumRows", "Encoding" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.RowData), global::Google.Cloud.SpannerLib.V1.RowData.Parser, new[]{ "Rows", "Metadata", "Data", "Stats", "HasMoreResults" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.NextRequest), global::Google.Cloud.SpannerLib.V1.NextRequest.Parser, new[]{ "Rows", "FetchOptions" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.FetchOptions), global::Google.Cloud.SpannerLib.V1.FetchOptions.Parser, new[]{ "NumRows", "Encoding" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.RowData), global::Google.Cloud.SpannerLib.V1.RowData.Parser, new[]{ "RequestId", "Rows", "Metadata", "Data", "Stats", "HasMoreResults" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.MetadataRequest), global::Google.Cloud.SpannerLib.V1.MetadataRequest.Parser, new[]{ "Rows" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.ResultSetStatsRequest), global::Google.Cloud.SpannerLib.V1.ResultSetStatsRequest.Parser, new[]{ "Rows" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.ConnectionStreamRequest), global::Google.Cloud.SpannerLib.V1.ConnectionStreamRequest.Parser, new[]{ "ExecuteRequest" }, new[]{ "Request" }, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.ConnectionStreamResponse), global::Google.Cloud.SpannerLib.V1.ConnectionStreamResponse.Parser, new[]{ "Row" }, new[]{ "Response" }, null, null, null) + new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.ConnectionStreamRequest), global::Google.Cloud.SpannerLib.V1.ConnectionStreamRequest.Parser, new[]{ "ExecuteRequest", "ExecuteBatchRequest", "BeginTransactionRequest", "CommitRequest", "RollbackRequest", "WriteMutationsRequest" }, new[]{ "Request" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.ExecuteResponse), global::Google.Cloud.SpannerLib.V1.ExecuteResponse.Parser, new[]{ "Rows", "ResultSets", "Status", "HasMoreResults" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Google.Cloud.SpannerLib.V1.ConnectionStreamResponse), global::Google.Cloud.SpannerLib.V1.ConnectionStreamResponse.Parser, new[]{ "Status", "ExecuteResponse", "ExecuteBatchResponse", "BeginTransactionResponse", "CommitResponse", "RollbackResponse", "WriteMutationsResponse" }, new[]{ "Response" }, null, null, null) })); } #endregion @@ -934,6 +962,7 @@ public ExecuteRequest() { public ExecuteRequest(ExecuteRequest other) : this() { connection_ = other.connection_ != null ? other.connection_.Clone() : null; executeSqlRequest_ = other.executeSqlRequest_ != null ? other.executeSqlRequest_.Clone() : null; + fetchOptions_ = other.fetchOptions_ != null ? other.fetchOptions_.Clone() : null; _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); } @@ -967,6 +996,18 @@ public ExecuteRequest Clone() { } } + /// Field number for the "fetch_options" field. + public const int FetchOptionsFieldNumber = 3; + private global::Google.Cloud.SpannerLib.V1.FetchOptions fetchOptions_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Cloud.SpannerLib.V1.FetchOptions FetchOptions { + get { return fetchOptions_; } + set { + fetchOptions_ = value; + } + } + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override bool Equals(object other) { @@ -984,6 +1025,7 @@ public bool Equals(ExecuteRequest other) { } if (!object.Equals(Connection, other.Connection)) return false; if (!object.Equals(ExecuteSqlRequest, other.ExecuteSqlRequest)) return false; + if (!object.Equals(FetchOptions, other.FetchOptions)) return false; return Equals(_unknownFields, other._unknownFields); } @@ -993,6 +1035,7 @@ public override int GetHashCode() { int hash = 1; if (connection_ != null) hash ^= Connection.GetHashCode(); if (executeSqlRequest_ != null) hash ^= ExecuteSqlRequest.GetHashCode(); + if (fetchOptions_ != null) hash ^= FetchOptions.GetHashCode(); if (_unknownFields != null) { hash ^= _unknownFields.GetHashCode(); } @@ -1019,6 +1062,10 @@ public void WriteTo(pb::CodedOutputStream output) { output.WriteRawTag(18); output.WriteMessage(ExecuteSqlRequest); } + if (fetchOptions_ != null) { + output.WriteRawTag(26); + output.WriteMessage(FetchOptions); + } if (_unknownFields != null) { _unknownFields.WriteTo(output); } @@ -1037,6 +1084,10 @@ public void WriteTo(pb::CodedOutputStream output) { output.WriteRawTag(18); output.WriteMessage(ExecuteSqlRequest); } + if (fetchOptions_ != null) { + output.WriteRawTag(26); + output.WriteMessage(FetchOptions); + } if (_unknownFields != null) { _unknownFields.WriteTo(ref output); } @@ -1053,6 +1104,9 @@ public int CalculateSize() { if (executeSqlRequest_ != null) { size += 1 + pb::CodedOutputStream.ComputeMessageSize(ExecuteSqlRequest); } + if (fetchOptions_ != null) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(FetchOptions); + } if (_unknownFields != null) { size += _unknownFields.CalculateSize(); } @@ -1077,6 +1131,12 @@ public void MergeFrom(ExecuteRequest other) { } ExecuteSqlRequest.MergeFrom(other.ExecuteSqlRequest); } + if (other.fetchOptions_ != null) { + if (fetchOptions_ == null) { + FetchOptions = new global::Google.Cloud.SpannerLib.V1.FetchOptions(); + } + FetchOptions.MergeFrom(other.FetchOptions); + } _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); } @@ -1110,6 +1170,13 @@ public void MergeFrom(pb::CodedInputStream input) { input.ReadMessage(ExecuteSqlRequest); break; } + case 26: { + if (fetchOptions_ == null) { + FetchOptions = new global::Google.Cloud.SpannerLib.V1.FetchOptions(); + } + input.ReadMessage(FetchOptions); + break; + } } } #endif @@ -1143,6 +1210,13 @@ public void MergeFrom(pb::CodedInputStream input) { input.ReadMessage(ExecuteSqlRequest); break; } + case 26: { + if (fetchOptions_ == null) { + FetchOptions = new global::Google.Cloud.SpannerLib.V1.FetchOptions(); + } + input.ReadMessage(FetchOptions); + break; + } } } } @@ -2631,8 +2705,7 @@ public NextRequest() { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public NextRequest(NextRequest other) : this() { rows_ = other.rows_ != null ? other.rows_.Clone() : null; - numRows_ = other.numRows_; - encoding_ = other.encoding_; + fetchOptions_ = other.fetchOptions_ != null ? other.fetchOptions_.Clone() : null; _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); } @@ -2654,27 +2727,15 @@ public NextRequest Clone() { } } - /// Field number for the "num_rows" field. - public const int NumRowsFieldNumber = 2; - private long numRows_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public long NumRows { - get { return numRows_; } - set { - numRows_ = value; - } - } - - /// Field number for the "encoding" field. - public const int EncodingFieldNumber = 3; - private long encoding_; + /// Field number for the "fetch_options" field. + public const int FetchOptionsFieldNumber = 2; + private global::Google.Cloud.SpannerLib.V1.FetchOptions fetchOptions_; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public long Encoding { - get { return encoding_; } + public global::Google.Cloud.SpannerLib.V1.FetchOptions FetchOptions { + get { return fetchOptions_; } set { - encoding_ = value; + fetchOptions_ = value; } } @@ -2694,8 +2755,7 @@ public bool Equals(NextRequest other) { return true; } if (!object.Equals(Rows, other.Rows)) return false; - if (NumRows != other.NumRows) return false; - if (Encoding != other.Encoding) return false; + if (!object.Equals(FetchOptions, other.FetchOptions)) return false; return Equals(_unknownFields, other._unknownFields); } @@ -2704,8 +2764,7 @@ public bool Equals(NextRequest other) { public override int GetHashCode() { int hash = 1; if (rows_ != null) hash ^= Rows.GetHashCode(); - if (NumRows != 0L) hash ^= NumRows.GetHashCode(); - if (Encoding != 0L) hash ^= Encoding.GetHashCode(); + if (fetchOptions_ != null) hash ^= FetchOptions.GetHashCode(); if (_unknownFields != null) { hash ^= _unknownFields.GetHashCode(); } @@ -2728,13 +2787,9 @@ public void WriteTo(pb::CodedOutputStream output) { output.WriteRawTag(10); output.WriteMessage(Rows); } - if (NumRows != 0L) { - output.WriteRawTag(16); - output.WriteInt64(NumRows); - } - if (Encoding != 0L) { - output.WriteRawTag(24); - output.WriteInt64(Encoding); + if (fetchOptions_ != null) { + output.WriteRawTag(18); + output.WriteMessage(FetchOptions); } if (_unknownFields != null) { _unknownFields.WriteTo(output); @@ -2750,13 +2805,9 @@ public void WriteTo(pb::CodedOutputStream output) { output.WriteRawTag(10); output.WriteMessage(Rows); } - if (NumRows != 0L) { - output.WriteRawTag(16); - output.WriteInt64(NumRows); - } - if (Encoding != 0L) { - output.WriteRawTag(24); - output.WriteInt64(Encoding); + if (fetchOptions_ != null) { + output.WriteRawTag(18); + output.WriteMessage(FetchOptions); } if (_unknownFields != null) { _unknownFields.WriteTo(ref output); @@ -2771,11 +2822,8 @@ public int CalculateSize() { if (rows_ != null) { size += 1 + pb::CodedOutputStream.ComputeMessageSize(Rows); } - if (NumRows != 0L) { - size += 1 + pb::CodedOutputStream.ComputeInt64Size(NumRows); - } - if (Encoding != 0L) { - size += 1 + pb::CodedOutputStream.ComputeInt64Size(Encoding); + if (fetchOptions_ != null) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(FetchOptions); } if (_unknownFields != null) { size += _unknownFields.CalculateSize(); @@ -2795,11 +2843,11 @@ public void MergeFrom(NextRequest other) { } Rows.MergeFrom(other.Rows); } - if (other.NumRows != 0L) { - NumRows = other.NumRows; - } - if (other.Encoding != 0L) { - Encoding = other.Encoding; + if (other.fetchOptions_ != null) { + if (fetchOptions_ == null) { + FetchOptions = new global::Google.Cloud.SpannerLib.V1.FetchOptions(); + } + FetchOptions.MergeFrom(other.FetchOptions); } _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); } @@ -2827,12 +2875,11 @@ public void MergeFrom(pb::CodedInputStream input) { input.ReadMessage(Rows); break; } - case 16: { - NumRows = input.ReadInt64(); - break; - } - case 24: { - Encoding = input.ReadInt64(); + case 18: { + if (fetchOptions_ == null) { + FetchOptions = new global::Google.Cloud.SpannerLib.V1.FetchOptions(); + } + input.ReadMessage(FetchOptions); break; } } @@ -2861,12 +2908,11 @@ public void MergeFrom(pb::CodedInputStream input) { input.ReadMessage(Rows); break; } - case 16: { - NumRows = input.ReadInt64(); - break; - } - case 24: { - Encoding = input.ReadInt64(); + case 18: { + if (fetchOptions_ == null) { + FetchOptions = new global::Google.Cloud.SpannerLib.V1.FetchOptions(); + } + input.ReadMessage(FetchOptions); break; } } @@ -2877,16 +2923,16 @@ public void MergeFrom(pb::CodedInputStream input) { } [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class RowData : pb::IMessage + public sealed partial class FetchOptions : pb::IMessage #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE , pb::IBufferMessage #endif { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new RowData()); + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new FetchOptions()); private pb::UnknownFieldSet _unknownFields; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser Parser { get { return _parser; } } + public static pb::MessageParser Parser { get { return _parser; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] @@ -2902,7 +2948,7 @@ public sealed partial class RowData : pb::IMessage [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public RowData() { + public FetchOptions() { OnConstruction(); } @@ -2910,100 +2956,59 @@ public RowData() { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public RowData(RowData other) : this() { - rows_ = other.rows_ != null ? other.rows_.Clone() : null; - metadata_ = other.metadata_ != null ? other.metadata_.Clone() : null; - data_ = other.data_.Clone(); - stats_ = other.stats_ != null ? other.stats_.Clone() : null; - hasMoreResults_ = other.hasMoreResults_; + public FetchOptions(FetchOptions other) : this() { + numRows_ = other.numRows_; + encoding_ = other.encoding_; _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public RowData Clone() { - return new RowData(this); - } - - /// Field number for the "rows" field. - public const int RowsFieldNumber = 1; - private global::Google.Cloud.SpannerLib.V1.Rows rows_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public global::Google.Cloud.SpannerLib.V1.Rows Rows { - get { return rows_; } - set { - rows_ = value; - } - } - - /// Field number for the "metadata" field. - public const int MetadataFieldNumber = 2; - private global::Google.Cloud.Spanner.V1.ResultSetMetadata metadata_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public global::Google.Cloud.Spanner.V1.ResultSetMetadata Metadata { - get { return metadata_; } - set { - metadata_ = value; - } - } - - /// Field number for the "data" field. - public const int DataFieldNumber = 3; - private static readonly pb::FieldCodec _repeated_data_codec - = pb::FieldCodec.ForMessage(26, global::Google.Protobuf.WellKnownTypes.ListValue.Parser); - private readonly pbc::RepeatedField data_ = new pbc::RepeatedField(); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public pbc::RepeatedField Data { - get { return data_; } + public FetchOptions Clone() { + return new FetchOptions(this); } - /// Field number for the "stats" field. - public const int StatsFieldNumber = 4; - private global::Google.Cloud.Spanner.V1.ResultSetStats stats_; + /// Field number for the "num_rows" field. + public const int NumRowsFieldNumber = 1; + private long numRows_; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public global::Google.Cloud.Spanner.V1.ResultSetStats Stats { - get { return stats_; } + public long NumRows { + get { return numRows_; } set { - stats_ = value; + numRows_ = value; } } - /// Field number for the "has_more_results" field. - public const int HasMoreResultsFieldNumber = 5; - private bool hasMoreResults_; + /// Field number for the "encoding" field. + public const int EncodingFieldNumber = 2; + private long encoding_; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool HasMoreResults { - get { return hasMoreResults_; } + public long Encoding { + get { return encoding_; } set { - hasMoreResults_ = value; + encoding_ = value; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override bool Equals(object other) { - return Equals(other as RowData); + return Equals(other as FetchOptions); } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(RowData other) { + public bool Equals(FetchOptions other) { if (ReferenceEquals(other, null)) { return false; } if (ReferenceEquals(other, this)) { return true; } - if (!object.Equals(Rows, other.Rows)) return false; - if (!object.Equals(Metadata, other.Metadata)) return false; - if(!data_.Equals(other.data_)) return false; - if (!object.Equals(Stats, other.Stats)) return false; - if (HasMoreResults != other.HasMoreResults) return false; + if (NumRows != other.NumRows) return false; + if (Encoding != other.Encoding) return false; return Equals(_unknownFields, other._unknownFields); } @@ -3011,11 +3016,8 @@ public bool Equals(RowData other) { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override int GetHashCode() { int hash = 1; - if (rows_ != null) hash ^= Rows.GetHashCode(); - if (metadata_ != null) hash ^= Metadata.GetHashCode(); - hash ^= data_.GetHashCode(); - if (stats_ != null) hash ^= Stats.GetHashCode(); - if (HasMoreResults != false) hash ^= HasMoreResults.GetHashCode(); + if (NumRows != 0L) hash ^= NumRows.GetHashCode(); + if (Encoding != 0L) hash ^= Encoding.GetHashCode(); if (_unknownFields != null) { hash ^= _unknownFields.GetHashCode(); } @@ -3034,22 +3036,13 @@ public void WriteTo(pb::CodedOutputStream output) { #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE output.WriteRawMessage(this); #else - if (rows_ != null) { - output.WriteRawTag(10); - output.WriteMessage(Rows); - } - if (metadata_ != null) { - output.WriteRawTag(18); - output.WriteMessage(Metadata); - } - data_.WriteTo(output, _repeated_data_codec); - if (stats_ != null) { - output.WriteRawTag(34); - output.WriteMessage(Stats); + if (NumRows != 0L) { + output.WriteRawTag(8); + output.WriteInt64(NumRows); } - if (HasMoreResults != false) { - output.WriteRawTag(40); - output.WriteBool(HasMoreResults); + if (Encoding != 0L) { + output.WriteRawTag(16); + output.WriteInt64(Encoding); } if (_unknownFields != null) { _unknownFields.WriteTo(output); @@ -3061,22 +3054,13 @@ public void WriteTo(pb::CodedOutputStream output) { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (rows_ != null) { - output.WriteRawTag(10); - output.WriteMessage(Rows); - } - if (metadata_ != null) { - output.WriteRawTag(18); - output.WriteMessage(Metadata); - } - data_.WriteTo(ref output, _repeated_data_codec); - if (stats_ != null) { - output.WriteRawTag(34); - output.WriteMessage(Stats); + if (NumRows != 0L) { + output.WriteRawTag(8); + output.WriteInt64(NumRows); } - if (HasMoreResults != false) { - output.WriteRawTag(40); - output.WriteBool(HasMoreResults); + if (Encoding != 0L) { + output.WriteRawTag(16); + output.WriteInt64(Encoding); } if (_unknownFields != null) { _unknownFields.WriteTo(ref output); @@ -3088,18 +3072,11 @@ public void WriteTo(pb::CodedOutputStream output) { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public int CalculateSize() { int size = 0; - if (rows_ != null) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(Rows); - } - if (metadata_ != null) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(Metadata); - } - size += data_.CalculateSize(_repeated_data_codec); - if (stats_ != null) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(Stats); + if (NumRows != 0L) { + size += 1 + pb::CodedOutputStream.ComputeInt64Size(NumRows); } - if (HasMoreResults != false) { - size += 1 + 1; + if (Encoding != 0L) { + size += 1 + pb::CodedOutputStream.ComputeInt64Size(Encoding); } if (_unknownFields != null) { size += _unknownFields.CalculateSize(); @@ -3109,31 +3086,15 @@ public int CalculateSize() { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(RowData other) { + public void MergeFrom(FetchOptions other) { if (other == null) { return; } - if (other.rows_ != null) { - if (rows_ == null) { - Rows = new global::Google.Cloud.SpannerLib.V1.Rows(); - } - Rows.MergeFrom(other.Rows); - } - if (other.metadata_ != null) { - if (metadata_ == null) { - Metadata = new global::Google.Cloud.Spanner.V1.ResultSetMetadata(); - } - Metadata.MergeFrom(other.Metadata); - } - data_.Add(other.data_); - if (other.stats_ != null) { - if (stats_ == null) { - Stats = new global::Google.Cloud.Spanner.V1.ResultSetStats(); - } - Stats.MergeFrom(other.Stats); + if (other.NumRows != 0L) { + NumRows = other.NumRows; } - if (other.HasMoreResults != false) { - HasMoreResults = other.HasMoreResults; + if (other.Encoding != 0L) { + Encoding = other.Encoding; } _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); } @@ -3154,33 +3115,12 @@ public void MergeFrom(pb::CodedInputStream input) { default: _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); break; - case 10: { - if (rows_ == null) { - Rows = new global::Google.Cloud.SpannerLib.V1.Rows(); - } - input.ReadMessage(Rows); + case 8: { + NumRows = input.ReadInt64(); break; } - case 18: { - if (metadata_ == null) { - Metadata = new global::Google.Cloud.Spanner.V1.ResultSetMetadata(); - } - input.ReadMessage(Metadata); - break; - } - case 26: { - data_.AddEntriesFrom(input, _repeated_data_codec); - break; - } - case 34: { - if (stats_ == null) { - Stats = new global::Google.Cloud.Spanner.V1.ResultSetStats(); - } - input.ReadMessage(Stats); - break; - } - case 40: { - HasMoreResults = input.ReadBool(); + case 16: { + Encoding = input.ReadInt64(); break; } } @@ -3202,33 +3142,12 @@ public void MergeFrom(pb::CodedInputStream input) { default: _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); break; - case 10: { - if (rows_ == null) { - Rows = new global::Google.Cloud.SpannerLib.V1.Rows(); - } - input.ReadMessage(Rows); - break; - } - case 18: { - if (metadata_ == null) { - Metadata = new global::Google.Cloud.Spanner.V1.ResultSetMetadata(); - } - input.ReadMessage(Metadata); - break; - } - case 26: { - data_.AddEntriesFrom(ref input, _repeated_data_codec); - break; - } - case 34: { - if (stats_ == null) { - Stats = new global::Google.Cloud.Spanner.V1.ResultSetStats(); - } - input.ReadMessage(Stats); + case 8: { + NumRows = input.ReadInt64(); break; } - case 40: { - HasMoreResults = input.ReadBool(); + case 16: { + Encoding = input.ReadInt64(); break; } } @@ -3239,16 +3158,16 @@ public void MergeFrom(pb::CodedInputStream input) { } [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class MetadataRequest : pb::IMessage + public sealed partial class RowData : pb::IMessage #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE , pb::IBufferMessage #endif { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new MetadataRequest()); + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new RowData()); private pb::UnknownFieldSet _unknownFields; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser Parser { get { return _parser; } } + public static pb::MessageParser Parser { get { return _parser; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] @@ -3264,7 +3183,7 @@ public sealed partial class MetadataRequest : pb::IMessage [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public MetadataRequest() { + public RowData() { OnConstruction(); } @@ -3272,19 +3191,36 @@ public MetadataRequest() { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public MetadataRequest(MetadataRequest other) : this() { + public RowData(RowData other) : this() { + requestId_ = other.requestId_; rows_ = other.rows_ != null ? other.rows_.Clone() : null; + metadata_ = other.metadata_ != null ? other.metadata_.Clone() : null; + data_ = other.data_.Clone(); + stats_ = other.stats_ != null ? other.stats_.Clone() : null; + hasMoreResults_ = other.hasMoreResults_; _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public MetadataRequest Clone() { - return new MetadataRequest(this); + public RowData Clone() { + return new RowData(this); + } + + /// Field number for the "request_id" field. + public const int RequestIdFieldNumber = 1; + private string requestId_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string RequestId { + get { return requestId_; } + set { + requestId_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } } /// Field number for the "rows" field. - public const int RowsFieldNumber = 1; + public const int RowsFieldNumber = 2; private global::Google.Cloud.SpannerLib.V1.Rows rows_; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] @@ -3295,22 +3231,74 @@ public MetadataRequest Clone() { } } + /// Field number for the "metadata" field. + public const int MetadataFieldNumber = 3; + private global::Google.Cloud.Spanner.V1.ResultSetMetadata metadata_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Cloud.Spanner.V1.ResultSetMetadata Metadata { + get { return metadata_; } + set { + metadata_ = value; + } + } + + /// Field number for the "data" field. + public const int DataFieldNumber = 4; + private static readonly pb::FieldCodec _repeated_data_codec + = pb::FieldCodec.ForMessage(34, global::Google.Protobuf.WellKnownTypes.ListValue.Parser); + private readonly pbc::RepeatedField data_ = new pbc::RepeatedField(); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public pbc::RepeatedField Data { + get { return data_; } + } + + /// Field number for the "stats" field. + public const int StatsFieldNumber = 5; + private global::Google.Cloud.Spanner.V1.ResultSetStats stats_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Cloud.Spanner.V1.ResultSetStats Stats { + get { return stats_; } + set { + stats_ = value; + } + } + + /// Field number for the "has_more_results" field. + public const int HasMoreResultsFieldNumber = 6; + private bool hasMoreResults_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool HasMoreResults { + get { return hasMoreResults_; } + set { + hasMoreResults_ = value; + } + } + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override bool Equals(object other) { - return Equals(other as MetadataRequest); + return Equals(other as RowData); } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(MetadataRequest other) { + public bool Equals(RowData other) { if (ReferenceEquals(other, null)) { return false; } if (ReferenceEquals(other, this)) { return true; } + if (RequestId != other.RequestId) return false; if (!object.Equals(Rows, other.Rows)) return false; + if (!object.Equals(Metadata, other.Metadata)) return false; + if(!data_.Equals(other.data_)) return false; + if (!object.Equals(Stats, other.Stats)) return false; + if (HasMoreResults != other.HasMoreResults) return false; return Equals(_unknownFields, other._unknownFields); } @@ -3318,7 +3306,12 @@ public bool Equals(MetadataRequest other) { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override int GetHashCode() { int hash = 1; + if (RequestId.Length != 0) hash ^= RequestId.GetHashCode(); if (rows_ != null) hash ^= Rows.GetHashCode(); + if (metadata_ != null) hash ^= Metadata.GetHashCode(); + hash ^= data_.GetHashCode(); + if (stats_ != null) hash ^= Stats.GetHashCode(); + if (HasMoreResults != false) hash ^= HasMoreResults.GetHashCode(); if (_unknownFields != null) { hash ^= _unknownFields.GetHashCode(); } @@ -3337,10 +3330,27 @@ public void WriteTo(pb::CodedOutputStream output) { #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE output.WriteRawMessage(this); #else - if (rows_ != null) { + if (RequestId.Length != 0) { output.WriteRawTag(10); + output.WriteString(RequestId); + } + if (rows_ != null) { + output.WriteRawTag(18); output.WriteMessage(Rows); } + if (metadata_ != null) { + output.WriteRawTag(26); + output.WriteMessage(Metadata); + } + data_.WriteTo(output, _repeated_data_codec); + if (stats_ != null) { + output.WriteRawTag(42); + output.WriteMessage(Stats); + } + if (HasMoreResults != false) { + output.WriteRawTag(48); + output.WriteBool(HasMoreResults); + } if (_unknownFields != null) { _unknownFields.WriteTo(output); } @@ -3351,10 +3361,27 @@ public void WriteTo(pb::CodedOutputStream output) { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (rows_ != null) { + if (RequestId.Length != 0) { output.WriteRawTag(10); + output.WriteString(RequestId); + } + if (rows_ != null) { + output.WriteRawTag(18); output.WriteMessage(Rows); } + if (metadata_ != null) { + output.WriteRawTag(26); + output.WriteMessage(Metadata); + } + data_.WriteTo(ref output, _repeated_data_codec); + if (stats_ != null) { + output.WriteRawTag(42); + output.WriteMessage(Stats); + } + if (HasMoreResults != false) { + output.WriteRawTag(48); + output.WriteBool(HasMoreResults); + } if (_unknownFields != null) { _unknownFields.WriteTo(ref output); } @@ -3365,9 +3392,22 @@ public void WriteTo(pb::CodedOutputStream output) { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public int CalculateSize() { int size = 0; + if (RequestId.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(RequestId); + } if (rows_ != null) { size += 1 + pb::CodedOutputStream.ComputeMessageSize(Rows); } + if (metadata_ != null) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(Metadata); + } + size += data_.CalculateSize(_repeated_data_codec); + if (stats_ != null) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(Stats); + } + if (HasMoreResults != false) { + size += 1 + 1; + } if (_unknownFields != null) { size += _unknownFields.CalculateSize(); } @@ -3376,16 +3416,35 @@ public int CalculateSize() { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(MetadataRequest other) { + public void MergeFrom(RowData other) { if (other == null) { return; } + if (other.RequestId.Length != 0) { + RequestId = other.RequestId; + } if (other.rows_ != null) { if (rows_ == null) { Rows = new global::Google.Cloud.SpannerLib.V1.Rows(); } Rows.MergeFrom(other.Rows); } + if (other.metadata_ != null) { + if (metadata_ == null) { + Metadata = new global::Google.Cloud.Spanner.V1.ResultSetMetadata(); + } + Metadata.MergeFrom(other.Metadata); + } + data_.Add(other.data_); + if (other.stats_ != null) { + if (stats_ == null) { + Stats = new global::Google.Cloud.Spanner.V1.ResultSetStats(); + } + Stats.MergeFrom(other.Stats); + } + if (other.HasMoreResults != false) { + HasMoreResults = other.HasMoreResults; + } _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); } @@ -3406,12 +3465,38 @@ public void MergeFrom(pb::CodedInputStream input) { _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); break; case 10: { + RequestId = input.ReadString(); + break; + } + case 18: { if (rows_ == null) { Rows = new global::Google.Cloud.SpannerLib.V1.Rows(); } input.ReadMessage(Rows); break; } + case 26: { + if (metadata_ == null) { + Metadata = new global::Google.Cloud.Spanner.V1.ResultSetMetadata(); + } + input.ReadMessage(Metadata); + break; + } + case 34: { + data_.AddEntriesFrom(input, _repeated_data_codec); + break; + } + case 42: { + if (stats_ == null) { + Stats = new global::Google.Cloud.Spanner.V1.ResultSetStats(); + } + input.ReadMessage(Stats); + break; + } + case 48: { + HasMoreResults = input.ReadBool(); + break; + } } } #endif @@ -3432,12 +3517,38 @@ public void MergeFrom(pb::CodedInputStream input) { _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); break; case 10: { + RequestId = input.ReadString(); + break; + } + case 18: { if (rows_ == null) { Rows = new global::Google.Cloud.SpannerLib.V1.Rows(); } input.ReadMessage(Rows); break; } + case 26: { + if (metadata_ == null) { + Metadata = new global::Google.Cloud.Spanner.V1.ResultSetMetadata(); + } + input.ReadMessage(Metadata); + break; + } + case 34: { + data_.AddEntriesFrom(ref input, _repeated_data_codec); + break; + } + case 42: { + if (stats_ == null) { + Stats = new global::Google.Cloud.Spanner.V1.ResultSetStats(); + } + input.ReadMessage(Stats); + break; + } + case 48: { + HasMoreResults = input.ReadBool(); + break; + } } } } @@ -3446,16 +3557,16 @@ public void MergeFrom(pb::CodedInputStream input) { } [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class ResultSetStatsRequest : pb::IMessage + public sealed partial class MetadataRequest : pb::IMessage #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE , pb::IBufferMessage #endif { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new ResultSetStatsRequest()); + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new MetadataRequest()); private pb::UnknownFieldSet _unknownFields; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser Parser { get { return _parser; } } + public static pb::MessageParser Parser { get { return _parser; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] @@ -3471,7 +3582,7 @@ public sealed partial class ResultSetStatsRequest : pb::IMessageField number for the "rows" field. @@ -3505,12 +3616,12 @@ public ResultSetStatsRequest Clone() { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override bool Equals(object other) { - return Equals(other as ResultSetStatsRequest); + return Equals(other as MetadataRequest); } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(ResultSetStatsRequest other) { + public bool Equals(MetadataRequest other) { if (ReferenceEquals(other, null)) { return false; } @@ -3583,7 +3694,7 @@ public int CalculateSize() { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(ResultSetStatsRequest other) { + public void MergeFrom(MetadataRequest other) { if (other == null) { return; } @@ -3653,16 +3764,16 @@ public void MergeFrom(pb::CodedInputStream input) { } [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class ConnectionStreamRequest : pb::IMessage + public sealed partial class ResultSetStatsRequest : pb::IMessage #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE , pb::IBufferMessage #endif { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new ConnectionStreamRequest()); + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new ResultSetStatsRequest()); private pb::UnknownFieldSet _unknownFields; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser Parser { get { return _parser; } } + public static pb::MessageParser Parser { get { return _parser; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] @@ -3678,7 +3789,7 @@ public sealed partial class ConnectionStreamRequest : pb::IMessageField number for the "rows" field. + public const int RowsFieldNumber = 1; + private global::Google.Cloud.SpannerLib.V1.Rows rows_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Cloud.SpannerLib.V1.Rows Rows { + get { return rows_; } + set { + rows_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as ResultSetStatsRequest); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(ResultSetStatsRequest other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (!object.Equals(Rows, other.Rows)) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (rows_ != null) hash ^= Rows.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (rows_ != null) { + output.WriteRawTag(10); + output.WriteMessage(Rows); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (rows_ != null) { + output.WriteRawTag(10); + output.WriteMessage(Rows); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (rows_ != null) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(Rows); + } + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(ResultSetStatsRequest other) { + if (other == null) { + return; + } + if (other.rows_ != null) { + if (rows_ == null) { + Rows = new global::Google.Cloud.SpannerLib.V1.Rows(); + } + Rows.MergeFrom(other.Rows); + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 10: { + if (rows_ == null) { + Rows = new global::Google.Cloud.SpannerLib.V1.Rows(); + } + input.ReadMessage(Rows); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 10: { + if (rows_ == null) { + Rows = new global::Google.Cloud.SpannerLib.V1.Rows(); + } + input.ReadMessage(Rows); + break; + } + } + } + } + #endif + + } + + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class ConnectionStreamRequest : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new ConnectionStreamRequest()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::Google.Cloud.SpannerLib.V1.SpannerlibReflection.Descriptor.MessageTypes[16]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public ConnectionStreamRequest() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public ConnectionStreamRequest(ConnectionStreamRequest other) : this() { + switch (other.RequestCase) { + case RequestOneofCase.ExecuteRequest: ExecuteRequest = other.ExecuteRequest.Clone(); break; + case RequestOneofCase.ExecuteBatchRequest: + ExecuteBatchRequest = other.ExecuteBatchRequest.Clone(); + break; + case RequestOneofCase.BeginTransactionRequest: + BeginTransactionRequest = other.BeginTransactionRequest.Clone(); + break; + case RequestOneofCase.CommitRequest: + CommitRequest = other.CommitRequest.Clone(); + break; + case RequestOneofCase.RollbackRequest: + RollbackRequest = other.RollbackRequest.Clone(); + break; + case RequestOneofCase.WriteMutationsRequest: + WriteMutationsRequest = other.WriteMutationsRequest.Clone(); + break; + } + + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public ConnectionStreamRequest Clone() { + return new ConnectionStreamRequest(this); + } + + /// Field number for the "execute_request" field. + public const int ExecuteRequestFieldNumber = 1; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Cloud.SpannerLib.V1.ExecuteRequest ExecuteRequest { + get { return requestCase_ == RequestOneofCase.ExecuteRequest ? (global::Google.Cloud.SpannerLib.V1.ExecuteRequest) request_ : null; } + set { + request_ = value; + requestCase_ = value == null ? RequestOneofCase.None : RequestOneofCase.ExecuteRequest; + } + } + + /// Field number for the "execute_batch_request" field. + public const int ExecuteBatchRequestFieldNumber = 2; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Cloud.SpannerLib.V1.ExecuteBatchRequest ExecuteBatchRequest { + get { return requestCase_ == RequestOneofCase.ExecuteBatchRequest ? (global::Google.Cloud.SpannerLib.V1.ExecuteBatchRequest) request_ : null; } + set { + request_ = value; + requestCase_ = value == null ? RequestOneofCase.None : RequestOneofCase.ExecuteBatchRequest; + } + } + + /// Field number for the "begin_transaction_request" field. + public const int BeginTransactionRequestFieldNumber = 3; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Cloud.SpannerLib.V1.BeginTransactionRequest BeginTransactionRequest { + get { return requestCase_ == RequestOneofCase.BeginTransactionRequest ? (global::Google.Cloud.SpannerLib.V1.BeginTransactionRequest) request_ : null; } + set { + request_ = value; + requestCase_ = value == null ? RequestOneofCase.None : RequestOneofCase.BeginTransactionRequest; + } + } + + /// Field number for the "commit_request" field. + public const int CommitRequestFieldNumber = 4; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Cloud.SpannerLib.V1.Connection CommitRequest { + get { return requestCase_ == RequestOneofCase.CommitRequest ? (global::Google.Cloud.SpannerLib.V1.Connection) request_ : null; } + set { + request_ = value; + requestCase_ = value == null ? RequestOneofCase.None : RequestOneofCase.CommitRequest; + } + } + + /// Field number for the "rollback_request" field. + public const int RollbackRequestFieldNumber = 5; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Cloud.SpannerLib.V1.Connection RollbackRequest { + get { return requestCase_ == RequestOneofCase.RollbackRequest ? (global::Google.Cloud.SpannerLib.V1.Connection) request_ : null; } + set { + request_ = value; + requestCase_ = value == null ? RequestOneofCase.None : RequestOneofCase.RollbackRequest; + } + } + + /// Field number for the "write_mutations_request" field. + public const int WriteMutationsRequestFieldNumber = 6; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Cloud.SpannerLib.V1.WriteMutationsRequest WriteMutationsRequest { + get { return requestCase_ == RequestOneofCase.WriteMutationsRequest ? (global::Google.Cloud.SpannerLib.V1.WriteMutationsRequest) request_ : null; } + set { + request_ = value; + requestCase_ = value == null ? RequestOneofCase.None : RequestOneofCase.WriteMutationsRequest; + } + } + + private object request_; + /// Enum of possible cases for the "request" oneof. + public enum RequestOneofCase { + None = 0, + ExecuteRequest = 1, + ExecuteBatchRequest = 2, + BeginTransactionRequest = 3, + CommitRequest = 4, + RollbackRequest = 5, + WriteMutationsRequest = 6, + } + private RequestOneofCase requestCase_ = RequestOneofCase.None; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public RequestOneofCase RequestCase { + get { return requestCase_; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void ClearRequest() { + requestCase_ = RequestOneofCase.None; + request_ = null; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as ConnectionStreamRequest); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(ConnectionStreamRequest other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (!object.Equals(ExecuteRequest, other.ExecuteRequest)) return false; + if (!object.Equals(ExecuteBatchRequest, other.ExecuteBatchRequest)) return false; + if (!object.Equals(BeginTransactionRequest, other.BeginTransactionRequest)) return false; + if (!object.Equals(CommitRequest, other.CommitRequest)) return false; + if (!object.Equals(RollbackRequest, other.RollbackRequest)) return false; + if (!object.Equals(WriteMutationsRequest, other.WriteMutationsRequest)) return false; + if (RequestCase != other.RequestCase) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (requestCase_ == RequestOneofCase.ExecuteRequest) hash ^= ExecuteRequest.GetHashCode(); + if (requestCase_ == RequestOneofCase.ExecuteBatchRequest) hash ^= ExecuteBatchRequest.GetHashCode(); + if (requestCase_ == RequestOneofCase.BeginTransactionRequest) hash ^= BeginTransactionRequest.GetHashCode(); + if (requestCase_ == RequestOneofCase.CommitRequest) hash ^= CommitRequest.GetHashCode(); + if (requestCase_ == RequestOneofCase.RollbackRequest) hash ^= RollbackRequest.GetHashCode(); + if (requestCase_ == RequestOneofCase.WriteMutationsRequest) hash ^= WriteMutationsRequest.GetHashCode(); + hash ^= (int) requestCase_; + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (requestCase_ == RequestOneofCase.ExecuteRequest) { + output.WriteRawTag(10); + output.WriteMessage(ExecuteRequest); + } + if (requestCase_ == RequestOneofCase.ExecuteBatchRequest) { + output.WriteRawTag(18); + output.WriteMessage(ExecuteBatchRequest); + } + if (requestCase_ == RequestOneofCase.BeginTransactionRequest) { + output.WriteRawTag(26); + output.WriteMessage(BeginTransactionRequest); + } + if (requestCase_ == RequestOneofCase.CommitRequest) { + output.WriteRawTag(34); + output.WriteMessage(CommitRequest); + } + if (requestCase_ == RequestOneofCase.RollbackRequest) { + output.WriteRawTag(42); + output.WriteMessage(RollbackRequest); + } + if (requestCase_ == RequestOneofCase.WriteMutationsRequest) { + output.WriteRawTag(50); + output.WriteMessage(WriteMutationsRequest); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (requestCase_ == RequestOneofCase.ExecuteRequest) { + output.WriteRawTag(10); + output.WriteMessage(ExecuteRequest); + } + if (requestCase_ == RequestOneofCase.ExecuteBatchRequest) { + output.WriteRawTag(18); + output.WriteMessage(ExecuteBatchRequest); + } + if (requestCase_ == RequestOneofCase.BeginTransactionRequest) { + output.WriteRawTag(26); + output.WriteMessage(BeginTransactionRequest); + } + if (requestCase_ == RequestOneofCase.CommitRequest) { + output.WriteRawTag(34); + output.WriteMessage(CommitRequest); + } + if (requestCase_ == RequestOneofCase.RollbackRequest) { + output.WriteRawTag(42); + output.WriteMessage(RollbackRequest); + } + if (requestCase_ == RequestOneofCase.WriteMutationsRequest) { + output.WriteRawTag(50); + output.WriteMessage(WriteMutationsRequest); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (requestCase_ == RequestOneofCase.ExecuteRequest) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(ExecuteRequest); + } + if (requestCase_ == RequestOneofCase.ExecuteBatchRequest) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(ExecuteBatchRequest); + } + if (requestCase_ == RequestOneofCase.BeginTransactionRequest) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(BeginTransactionRequest); + } + if (requestCase_ == RequestOneofCase.CommitRequest) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(CommitRequest); + } + if (requestCase_ == RequestOneofCase.RollbackRequest) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(RollbackRequest); + } + if (requestCase_ == RequestOneofCase.WriteMutationsRequest) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(WriteMutationsRequest); + } + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(ConnectionStreamRequest other) { + if (other == null) { + return; + } + switch (other.RequestCase) { + case RequestOneofCase.ExecuteRequest: + if (ExecuteRequest == null) { + ExecuteRequest = new global::Google.Cloud.SpannerLib.V1.ExecuteRequest(); + } + ExecuteRequest.MergeFrom(other.ExecuteRequest); + break; + case RequestOneofCase.ExecuteBatchRequest: + if (ExecuteBatchRequest == null) { + ExecuteBatchRequest = new global::Google.Cloud.SpannerLib.V1.ExecuteBatchRequest(); + } + ExecuteBatchRequest.MergeFrom(other.ExecuteBatchRequest); + break; + case RequestOneofCase.BeginTransactionRequest: + if (BeginTransactionRequest == null) { + BeginTransactionRequest = new global::Google.Cloud.SpannerLib.V1.BeginTransactionRequest(); + } + BeginTransactionRequest.MergeFrom(other.BeginTransactionRequest); + break; + case RequestOneofCase.CommitRequest: + if (CommitRequest == null) { + CommitRequest = new global::Google.Cloud.SpannerLib.V1.Connection(); + } + CommitRequest.MergeFrom(other.CommitRequest); + break; + case RequestOneofCase.RollbackRequest: + if (RollbackRequest == null) { + RollbackRequest = new global::Google.Cloud.SpannerLib.V1.Connection(); + } + RollbackRequest.MergeFrom(other.RollbackRequest); + break; + case RequestOneofCase.WriteMutationsRequest: + if (WriteMutationsRequest == null) { + WriteMutationsRequest = new global::Google.Cloud.SpannerLib.V1.WriteMutationsRequest(); + } + WriteMutationsRequest.MergeFrom(other.WriteMutationsRequest); + break; + } + + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 10: { + global::Google.Cloud.SpannerLib.V1.ExecuteRequest subBuilder = new global::Google.Cloud.SpannerLib.V1.ExecuteRequest(); + if (requestCase_ == RequestOneofCase.ExecuteRequest) { + subBuilder.MergeFrom(ExecuteRequest); + } + input.ReadMessage(subBuilder); + ExecuteRequest = subBuilder; + break; + } + case 18: { + global::Google.Cloud.SpannerLib.V1.ExecuteBatchRequest subBuilder = new global::Google.Cloud.SpannerLib.V1.ExecuteBatchRequest(); + if (requestCase_ == RequestOneofCase.ExecuteBatchRequest) { + subBuilder.MergeFrom(ExecuteBatchRequest); + } + input.ReadMessage(subBuilder); + ExecuteBatchRequest = subBuilder; + break; + } + case 26: { + global::Google.Cloud.SpannerLib.V1.BeginTransactionRequest subBuilder = new global::Google.Cloud.SpannerLib.V1.BeginTransactionRequest(); + if (requestCase_ == RequestOneofCase.BeginTransactionRequest) { + subBuilder.MergeFrom(BeginTransactionRequest); + } + input.ReadMessage(subBuilder); + BeginTransactionRequest = subBuilder; + break; + } + case 34: { + global::Google.Cloud.SpannerLib.V1.Connection subBuilder = new global::Google.Cloud.SpannerLib.V1.Connection(); + if (requestCase_ == RequestOneofCase.CommitRequest) { + subBuilder.MergeFrom(CommitRequest); + } + input.ReadMessage(subBuilder); + CommitRequest = subBuilder; + break; + } + case 42: { + global::Google.Cloud.SpannerLib.V1.Connection subBuilder = new global::Google.Cloud.SpannerLib.V1.Connection(); + if (requestCase_ == RequestOneofCase.RollbackRequest) { + subBuilder.MergeFrom(RollbackRequest); + } + input.ReadMessage(subBuilder); + RollbackRequest = subBuilder; + break; + } + case 50: { + global::Google.Cloud.SpannerLib.V1.WriteMutationsRequest subBuilder = new global::Google.Cloud.SpannerLib.V1.WriteMutationsRequest(); + if (requestCase_ == RequestOneofCase.WriteMutationsRequest) { + subBuilder.MergeFrom(WriteMutationsRequest); + } + input.ReadMessage(subBuilder); + WriteMutationsRequest = subBuilder; + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 10: { + global::Google.Cloud.SpannerLib.V1.ExecuteRequest subBuilder = new global::Google.Cloud.SpannerLib.V1.ExecuteRequest(); + if (requestCase_ == RequestOneofCase.ExecuteRequest) { + subBuilder.MergeFrom(ExecuteRequest); + } + input.ReadMessage(subBuilder); + ExecuteRequest = subBuilder; + break; + } + case 18: { + global::Google.Cloud.SpannerLib.V1.ExecuteBatchRequest subBuilder = new global::Google.Cloud.SpannerLib.V1.ExecuteBatchRequest(); + if (requestCase_ == RequestOneofCase.ExecuteBatchRequest) { + subBuilder.MergeFrom(ExecuteBatchRequest); + } + input.ReadMessage(subBuilder); + ExecuteBatchRequest = subBuilder; + break; + } + case 26: { + global::Google.Cloud.SpannerLib.V1.BeginTransactionRequest subBuilder = new global::Google.Cloud.SpannerLib.V1.BeginTransactionRequest(); + if (requestCase_ == RequestOneofCase.BeginTransactionRequest) { + subBuilder.MergeFrom(BeginTransactionRequest); + } + input.ReadMessage(subBuilder); + BeginTransactionRequest = subBuilder; + break; + } + case 34: { + global::Google.Cloud.SpannerLib.V1.Connection subBuilder = new global::Google.Cloud.SpannerLib.V1.Connection(); + if (requestCase_ == RequestOneofCase.CommitRequest) { + subBuilder.MergeFrom(CommitRequest); + } + input.ReadMessage(subBuilder); + CommitRequest = subBuilder; + break; + } + case 42: { + global::Google.Cloud.SpannerLib.V1.Connection subBuilder = new global::Google.Cloud.SpannerLib.V1.Connection(); + if (requestCase_ == RequestOneofCase.RollbackRequest) { + subBuilder.MergeFrom(RollbackRequest); + } + input.ReadMessage(subBuilder); + RollbackRequest = subBuilder; + break; + } + case 50: { + global::Google.Cloud.SpannerLib.V1.WriteMutationsRequest subBuilder = new global::Google.Cloud.SpannerLib.V1.WriteMutationsRequest(); + if (requestCase_ == RequestOneofCase.WriteMutationsRequest) { + subBuilder.MergeFrom(WriteMutationsRequest); + } + input.ReadMessage(subBuilder); + WriteMutationsRequest = subBuilder; + break; + } + } + } + } + #endif + + } + + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class ExecuteResponse : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new ExecuteResponse()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::Google.Cloud.SpannerLib.V1.SpannerlibReflection.Descriptor.MessageTypes[17]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public ExecuteResponse() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public ExecuteResponse(ExecuteResponse other) : this() { + rows_ = other.rows_ != null ? other.rows_.Clone() : null; + resultSets_ = other.resultSets_.Clone(); + status_ = other.status_ != null ? other.status_.Clone() : null; + hasMoreResults_ = other.hasMoreResults_; + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public ExecuteResponse Clone() { + return new ExecuteResponse(this); + } + + /// Field number for the "rows" field. + public const int RowsFieldNumber = 1; + private global::Google.Cloud.SpannerLib.V1.Rows rows_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Cloud.SpannerLib.V1.Rows Rows { + get { return rows_; } + set { + rows_ = value; } - - _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); } + /// Field number for the "result_sets" field. + public const int ResultSetsFieldNumber = 2; + private static readonly pb::FieldCodec _repeated_resultSets_codec + = pb::FieldCodec.ForMessage(18, global::Google.Cloud.Spanner.V1.ResultSet.Parser); + private readonly pbc::RepeatedField resultSets_ = new pbc::RepeatedField(); [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public ConnectionStreamRequest Clone() { - return new ConnectionStreamRequest(this); + public pbc::RepeatedField ResultSets { + get { return resultSets_; } } - /// Field number for the "execute_request" field. - public const int ExecuteRequestFieldNumber = 1; + /// Field number for the "status" field. + public const int StatusFieldNumber = 3; + private global::Google.Rpc.Status status_; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public global::Google.Cloud.SpannerLib.V1.ExecuteRequest ExecuteRequest { - get { return requestCase_ == RequestOneofCase.ExecuteRequest ? (global::Google.Cloud.SpannerLib.V1.ExecuteRequest) request_ : null; } + public global::Google.Rpc.Status Status { + get { return status_; } set { - request_ = value; - requestCase_ = value == null ? RequestOneofCase.None : RequestOneofCase.ExecuteRequest; + status_ = value; } } - private object request_; - /// Enum of possible cases for the "request" oneof. - public enum RequestOneofCase { - None = 0, - ExecuteRequest = 1, - } - private RequestOneofCase requestCase_ = RequestOneofCase.None; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public RequestOneofCase RequestCase { - get { return requestCase_; } - } - + /// Field number for the "has_more_results" field. + public const int HasMoreResultsFieldNumber = 4; + private bool hasMoreResults_; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void ClearRequest() { - requestCase_ = RequestOneofCase.None; - request_ = null; + public bool HasMoreResults { + get { return hasMoreResults_; } + set { + hasMoreResults_ = value; + } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override bool Equals(object other) { - return Equals(other as ConnectionStreamRequest); + return Equals(other as ExecuteResponse); } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(ConnectionStreamRequest other) { + public bool Equals(ExecuteResponse other) { if (ReferenceEquals(other, null)) { return false; } if (ReferenceEquals(other, this)) { return true; } - if (!object.Equals(ExecuteRequest, other.ExecuteRequest)) return false; - if (RequestCase != other.RequestCase) return false; + if (!object.Equals(Rows, other.Rows)) return false; + if(!resultSets_.Equals(other.resultSets_)) return false; + if (!object.Equals(Status, other.Status)) return false; + if (HasMoreResults != other.HasMoreResults) return false; return Equals(_unknownFields, other._unknownFields); } @@ -3758,8 +4597,10 @@ public bool Equals(ConnectionStreamRequest other) { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override int GetHashCode() { int hash = 1; - if (requestCase_ == RequestOneofCase.ExecuteRequest) hash ^= ExecuteRequest.GetHashCode(); - hash ^= (int) requestCase_; + if (rows_ != null) hash ^= Rows.GetHashCode(); + hash ^= resultSets_.GetHashCode(); + if (status_ != null) hash ^= Status.GetHashCode(); + if (HasMoreResults != false) hash ^= HasMoreResults.GetHashCode(); if (_unknownFields != null) { hash ^= _unknownFields.GetHashCode(); } @@ -3778,9 +4619,18 @@ public void WriteTo(pb::CodedOutputStream output) { #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE output.WriteRawMessage(this); #else - if (requestCase_ == RequestOneofCase.ExecuteRequest) { + if (rows_ != null) { output.WriteRawTag(10); - output.WriteMessage(ExecuteRequest); + output.WriteMessage(Rows); + } + resultSets_.WriteTo(output, _repeated_resultSets_codec); + if (status_ != null) { + output.WriteRawTag(26); + output.WriteMessage(Status); + } + if (HasMoreResults != false) { + output.WriteRawTag(32); + output.WriteBool(HasMoreResults); } if (_unknownFields != null) { _unknownFields.WriteTo(output); @@ -3792,9 +4642,18 @@ public void WriteTo(pb::CodedOutputStream output) { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (requestCase_ == RequestOneofCase.ExecuteRequest) { + if (rows_ != null) { output.WriteRawTag(10); - output.WriteMessage(ExecuteRequest); + output.WriteMessage(Rows); + } + resultSets_.WriteTo(ref output, _repeated_resultSets_codec); + if (status_ != null) { + output.WriteRawTag(26); + output.WriteMessage(Status); + } + if (HasMoreResults != false) { + output.WriteRawTag(32); + output.WriteBool(HasMoreResults); } if (_unknownFields != null) { _unknownFields.WriteTo(ref output); @@ -3806,8 +4665,15 @@ public void WriteTo(pb::CodedOutputStream output) { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public int CalculateSize() { int size = 0; - if (requestCase_ == RequestOneofCase.ExecuteRequest) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(ExecuteRequest); + if (rows_ != null) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(Rows); + } + size += resultSets_.CalculateSize(_repeated_resultSets_codec); + if (status_ != null) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(Status); + } + if (HasMoreResults != false) { + size += 1 + 1; } if (_unknownFields != null) { size += _unknownFields.CalculateSize(); @@ -3817,19 +4683,26 @@ public int CalculateSize() { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(ConnectionStreamRequest other) { + public void MergeFrom(ExecuteResponse other) { if (other == null) { return; } - switch (other.RequestCase) { - case RequestOneofCase.ExecuteRequest: - if (ExecuteRequest == null) { - ExecuteRequest = new global::Google.Cloud.SpannerLib.V1.ExecuteRequest(); - } - ExecuteRequest.MergeFrom(other.ExecuteRequest); - break; + if (other.rows_ != null) { + if (rows_ == null) { + Rows = new global::Google.Cloud.SpannerLib.V1.Rows(); + } + Rows.MergeFrom(other.Rows); + } + resultSets_.Add(other.resultSets_); + if (other.status_ != null) { + if (status_ == null) { + Status = new global::Google.Rpc.Status(); + } + Status.MergeFrom(other.Status); + } + if (other.HasMoreResults != false) { + HasMoreResults = other.HasMoreResults; } - _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); } @@ -3850,12 +4723,25 @@ public void MergeFrom(pb::CodedInputStream input) { _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); break; case 10: { - global::Google.Cloud.SpannerLib.V1.ExecuteRequest subBuilder = new global::Google.Cloud.SpannerLib.V1.ExecuteRequest(); - if (requestCase_ == RequestOneofCase.ExecuteRequest) { - subBuilder.MergeFrom(ExecuteRequest); + if (rows_ == null) { + Rows = new global::Google.Cloud.SpannerLib.V1.Rows(); } - input.ReadMessage(subBuilder); - ExecuteRequest = subBuilder; + input.ReadMessage(Rows); + break; + } + case 18: { + resultSets_.AddEntriesFrom(input, _repeated_resultSets_codec); + break; + } + case 26: { + if (status_ == null) { + Status = new global::Google.Rpc.Status(); + } + input.ReadMessage(Status); + break; + } + case 32: { + HasMoreResults = input.ReadBool(); break; } } @@ -3878,12 +4764,25 @@ public void MergeFrom(pb::CodedInputStream input) { _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); break; case 10: { - global::Google.Cloud.SpannerLib.V1.ExecuteRequest subBuilder = new global::Google.Cloud.SpannerLib.V1.ExecuteRequest(); - if (requestCase_ == RequestOneofCase.ExecuteRequest) { - subBuilder.MergeFrom(ExecuteRequest); + if (rows_ == null) { + Rows = new global::Google.Cloud.SpannerLib.V1.Rows(); } - input.ReadMessage(subBuilder); - ExecuteRequest = subBuilder; + input.ReadMessage(Rows); + break; + } + case 18: { + resultSets_.AddEntriesFrom(ref input, _repeated_resultSets_codec); + break; + } + case 26: { + if (status_ == null) { + Status = new global::Google.Rpc.Status(); + } + input.ReadMessage(Status); + break; + } + case 32: { + HasMoreResults = input.ReadBool(); break; } } @@ -3908,7 +4807,7 @@ public sealed partial class ConnectionStreamResponse : pb::IMessageField number for the "row" field. - public const int RowFieldNumber = 1; + /// Field number for the "status" field. + public const int StatusFieldNumber = 1; + private global::Google.Rpc.Status status_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Rpc.Status Status { + get { return status_; } + set { + status_ = value; + } + } + + /// Field number for the "execute_response" field. + public const int ExecuteResponseFieldNumber = 2; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Cloud.SpannerLib.V1.ExecuteResponse ExecuteResponse { + get { return responseCase_ == ResponseOneofCase.ExecuteResponse ? (global::Google.Cloud.SpannerLib.V1.ExecuteResponse) response_ : null; } + set { + response_ = value; + responseCase_ = value == null ? ResponseOneofCase.None : ResponseOneofCase.ExecuteResponse; + } + } + + /// Field number for the "execute_batch_response" field. + public const int ExecuteBatchResponseFieldNumber = 3; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Cloud.Spanner.V1.ExecuteBatchDmlResponse ExecuteBatchResponse { + get { return responseCase_ == ResponseOneofCase.ExecuteBatchResponse ? (global::Google.Cloud.Spanner.V1.ExecuteBatchDmlResponse) response_ : null; } + set { + response_ = value; + responseCase_ = value == null ? ResponseOneofCase.None : ResponseOneofCase.ExecuteBatchResponse; + } + } + + /// Field number for the "begin_transaction_response" field. + public const int BeginTransactionResponseFieldNumber = 4; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Protobuf.WellKnownTypes.Empty BeginTransactionResponse { + get { return responseCase_ == ResponseOneofCase.BeginTransactionResponse ? (global::Google.Protobuf.WellKnownTypes.Empty) response_ : null; } + set { + response_ = value; + responseCase_ = value == null ? ResponseOneofCase.None : ResponseOneofCase.BeginTransactionResponse; + } + } + + /// Field number for the "commit_response" field. + public const int CommitResponseFieldNumber = 5; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Cloud.Spanner.V1.CommitResponse CommitResponse { + get { return responseCase_ == ResponseOneofCase.CommitResponse ? (global::Google.Cloud.Spanner.V1.CommitResponse) response_ : null; } + set { + response_ = value; + responseCase_ = value == null ? ResponseOneofCase.None : ResponseOneofCase.CommitResponse; + } + } + + /// Field number for the "rollback_response" field. + public const int RollbackResponseFieldNumber = 6; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Google.Protobuf.WellKnownTypes.Empty RollbackResponse { + get { return responseCase_ == ResponseOneofCase.RollbackResponse ? (global::Google.Protobuf.WellKnownTypes.Empty) response_ : null; } + set { + response_ = value; + responseCase_ = value == null ? ResponseOneofCase.None : ResponseOneofCase.RollbackResponse; + } + } + + /// Field number for the "write_mutations_response" field. + public const int WriteMutationsResponseFieldNumber = 7; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public global::Google.Cloud.Spanner.V1.PartialResultSet Row { - get { return responseCase_ == ResponseOneofCase.Row ? (global::Google.Cloud.Spanner.V1.PartialResultSet) response_ : null; } + public global::Google.Cloud.Spanner.V1.CommitResponse WriteMutationsResponse { + get { return responseCase_ == ResponseOneofCase.WriteMutationsResponse ? (global::Google.Cloud.Spanner.V1.CommitResponse) response_ : null; } set { response_ = value; - responseCase_ = value == null ? ResponseOneofCase.None : ResponseOneofCase.Row; + responseCase_ = value == null ? ResponseOneofCase.None : ResponseOneofCase.WriteMutationsResponse; } } @@ -3959,7 +4946,12 @@ public ConnectionStreamResponse Clone() { /// Enum of possible cases for the "response" oneof. public enum ResponseOneofCase { None = 0, - Row = 1, + ExecuteResponse = 2, + ExecuteBatchResponse = 3, + BeginTransactionResponse = 4, + CommitResponse = 5, + RollbackResponse = 6, + WriteMutationsResponse = 7, } private ResponseOneofCase responseCase_ = ResponseOneofCase.None; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -3990,7 +4982,13 @@ public bool Equals(ConnectionStreamResponse other) { if (ReferenceEquals(other, this)) { return true; } - if (!object.Equals(Row, other.Row)) return false; + if (!object.Equals(Status, other.Status)) return false; + if (!object.Equals(ExecuteResponse, other.ExecuteResponse)) return false; + if (!object.Equals(ExecuteBatchResponse, other.ExecuteBatchResponse)) return false; + if (!object.Equals(BeginTransactionResponse, other.BeginTransactionResponse)) return false; + if (!object.Equals(CommitResponse, other.CommitResponse)) return false; + if (!object.Equals(RollbackResponse, other.RollbackResponse)) return false; + if (!object.Equals(WriteMutationsResponse, other.WriteMutationsResponse)) return false; if (ResponseCase != other.ResponseCase) return false; return Equals(_unknownFields, other._unknownFields); } @@ -3999,7 +4997,13 @@ public bool Equals(ConnectionStreamResponse other) { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override int GetHashCode() { int hash = 1; - if (responseCase_ == ResponseOneofCase.Row) hash ^= Row.GetHashCode(); + if (status_ != null) hash ^= Status.GetHashCode(); + if (responseCase_ == ResponseOneofCase.ExecuteResponse) hash ^= ExecuteResponse.GetHashCode(); + if (responseCase_ == ResponseOneofCase.ExecuteBatchResponse) hash ^= ExecuteBatchResponse.GetHashCode(); + if (responseCase_ == ResponseOneofCase.BeginTransactionResponse) hash ^= BeginTransactionResponse.GetHashCode(); + if (responseCase_ == ResponseOneofCase.CommitResponse) hash ^= CommitResponse.GetHashCode(); + if (responseCase_ == ResponseOneofCase.RollbackResponse) hash ^= RollbackResponse.GetHashCode(); + if (responseCase_ == ResponseOneofCase.WriteMutationsResponse) hash ^= WriteMutationsResponse.GetHashCode(); hash ^= (int) responseCase_; if (_unknownFields != null) { hash ^= _unknownFields.GetHashCode(); @@ -4019,9 +5023,33 @@ public void WriteTo(pb::CodedOutputStream output) { #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE output.WriteRawMessage(this); #else - if (responseCase_ == ResponseOneofCase.Row) { + if (status_ != null) { output.WriteRawTag(10); - output.WriteMessage(Row); + output.WriteMessage(Status); + } + if (responseCase_ == ResponseOneofCase.ExecuteResponse) { + output.WriteRawTag(18); + output.WriteMessage(ExecuteResponse); + } + if (responseCase_ == ResponseOneofCase.ExecuteBatchResponse) { + output.WriteRawTag(26); + output.WriteMessage(ExecuteBatchResponse); + } + if (responseCase_ == ResponseOneofCase.BeginTransactionResponse) { + output.WriteRawTag(34); + output.WriteMessage(BeginTransactionResponse); + } + if (responseCase_ == ResponseOneofCase.CommitResponse) { + output.WriteRawTag(42); + output.WriteMessage(CommitResponse); + } + if (responseCase_ == ResponseOneofCase.RollbackResponse) { + output.WriteRawTag(50); + output.WriteMessage(RollbackResponse); + } + if (responseCase_ == ResponseOneofCase.WriteMutationsResponse) { + output.WriteRawTag(58); + output.WriteMessage(WriteMutationsResponse); } if (_unknownFields != null) { _unknownFields.WriteTo(output); @@ -4033,9 +5061,33 @@ public void WriteTo(pb::CodedOutputStream output) { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (responseCase_ == ResponseOneofCase.Row) { + if (status_ != null) { output.WriteRawTag(10); - output.WriteMessage(Row); + output.WriteMessage(Status); + } + if (responseCase_ == ResponseOneofCase.ExecuteResponse) { + output.WriteRawTag(18); + output.WriteMessage(ExecuteResponse); + } + if (responseCase_ == ResponseOneofCase.ExecuteBatchResponse) { + output.WriteRawTag(26); + output.WriteMessage(ExecuteBatchResponse); + } + if (responseCase_ == ResponseOneofCase.BeginTransactionResponse) { + output.WriteRawTag(34); + output.WriteMessage(BeginTransactionResponse); + } + if (responseCase_ == ResponseOneofCase.CommitResponse) { + output.WriteRawTag(42); + output.WriteMessage(CommitResponse); + } + if (responseCase_ == ResponseOneofCase.RollbackResponse) { + output.WriteRawTag(50); + output.WriteMessage(RollbackResponse); + } + if (responseCase_ == ResponseOneofCase.WriteMutationsResponse) { + output.WriteRawTag(58); + output.WriteMessage(WriteMutationsResponse); } if (_unknownFields != null) { _unknownFields.WriteTo(ref output); @@ -4047,8 +5099,26 @@ public void WriteTo(pb::CodedOutputStream output) { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public int CalculateSize() { int size = 0; - if (responseCase_ == ResponseOneofCase.Row) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(Row); + if (status_ != null) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(Status); + } + if (responseCase_ == ResponseOneofCase.ExecuteResponse) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(ExecuteResponse); + } + if (responseCase_ == ResponseOneofCase.ExecuteBatchResponse) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(ExecuteBatchResponse); + } + if (responseCase_ == ResponseOneofCase.BeginTransactionResponse) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(BeginTransactionResponse); + } + if (responseCase_ == ResponseOneofCase.CommitResponse) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(CommitResponse); + } + if (responseCase_ == ResponseOneofCase.RollbackResponse) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(RollbackResponse); + } + if (responseCase_ == ResponseOneofCase.WriteMutationsResponse) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(WriteMutationsResponse); } if (_unknownFields != null) { size += _unknownFields.CalculateSize(); @@ -4062,12 +5132,48 @@ public void MergeFrom(ConnectionStreamResponse other) { if (other == null) { return; } + if (other.status_ != null) { + if (status_ == null) { + Status = new global::Google.Rpc.Status(); + } + Status.MergeFrom(other.Status); + } switch (other.ResponseCase) { - case ResponseOneofCase.Row: - if (Row == null) { - Row = new global::Google.Cloud.Spanner.V1.PartialResultSet(); + case ResponseOneofCase.ExecuteResponse: + if (ExecuteResponse == null) { + ExecuteResponse = new global::Google.Cloud.SpannerLib.V1.ExecuteResponse(); + } + ExecuteResponse.MergeFrom(other.ExecuteResponse); + break; + case ResponseOneofCase.ExecuteBatchResponse: + if (ExecuteBatchResponse == null) { + ExecuteBatchResponse = new global::Google.Cloud.Spanner.V1.ExecuteBatchDmlResponse(); + } + ExecuteBatchResponse.MergeFrom(other.ExecuteBatchResponse); + break; + case ResponseOneofCase.BeginTransactionResponse: + if (BeginTransactionResponse == null) { + BeginTransactionResponse = new global::Google.Protobuf.WellKnownTypes.Empty(); + } + BeginTransactionResponse.MergeFrom(other.BeginTransactionResponse); + break; + case ResponseOneofCase.CommitResponse: + if (CommitResponse == null) { + CommitResponse = new global::Google.Cloud.Spanner.V1.CommitResponse(); } - Row.MergeFrom(other.Row); + CommitResponse.MergeFrom(other.CommitResponse); + break; + case ResponseOneofCase.RollbackResponse: + if (RollbackResponse == null) { + RollbackResponse = new global::Google.Protobuf.WellKnownTypes.Empty(); + } + RollbackResponse.MergeFrom(other.RollbackResponse); + break; + case ResponseOneofCase.WriteMutationsResponse: + if (WriteMutationsResponse == null) { + WriteMutationsResponse = new global::Google.Cloud.Spanner.V1.CommitResponse(); + } + WriteMutationsResponse.MergeFrom(other.WriteMutationsResponse); break; } @@ -4091,12 +5197,64 @@ public void MergeFrom(pb::CodedInputStream input) { _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); break; case 10: { - global::Google.Cloud.Spanner.V1.PartialResultSet subBuilder = new global::Google.Cloud.Spanner.V1.PartialResultSet(); - if (responseCase_ == ResponseOneofCase.Row) { - subBuilder.MergeFrom(Row); + if (status_ == null) { + Status = new global::Google.Rpc.Status(); + } + input.ReadMessage(Status); + break; + } + case 18: { + global::Google.Cloud.SpannerLib.V1.ExecuteResponse subBuilder = new global::Google.Cloud.SpannerLib.V1.ExecuteResponse(); + if (responseCase_ == ResponseOneofCase.ExecuteResponse) { + subBuilder.MergeFrom(ExecuteResponse); + } + input.ReadMessage(subBuilder); + ExecuteResponse = subBuilder; + break; + } + case 26: { + global::Google.Cloud.Spanner.V1.ExecuteBatchDmlResponse subBuilder = new global::Google.Cloud.Spanner.V1.ExecuteBatchDmlResponse(); + if (responseCase_ == ResponseOneofCase.ExecuteBatchResponse) { + subBuilder.MergeFrom(ExecuteBatchResponse); + } + input.ReadMessage(subBuilder); + ExecuteBatchResponse = subBuilder; + break; + } + case 34: { + global::Google.Protobuf.WellKnownTypes.Empty subBuilder = new global::Google.Protobuf.WellKnownTypes.Empty(); + if (responseCase_ == ResponseOneofCase.BeginTransactionResponse) { + subBuilder.MergeFrom(BeginTransactionResponse); + } + input.ReadMessage(subBuilder); + BeginTransactionResponse = subBuilder; + break; + } + case 42: { + global::Google.Cloud.Spanner.V1.CommitResponse subBuilder = new global::Google.Cloud.Spanner.V1.CommitResponse(); + if (responseCase_ == ResponseOneofCase.CommitResponse) { + subBuilder.MergeFrom(CommitResponse); + } + input.ReadMessage(subBuilder); + CommitResponse = subBuilder; + break; + } + case 50: { + global::Google.Protobuf.WellKnownTypes.Empty subBuilder = new global::Google.Protobuf.WellKnownTypes.Empty(); + if (responseCase_ == ResponseOneofCase.RollbackResponse) { + subBuilder.MergeFrom(RollbackResponse); + } + input.ReadMessage(subBuilder); + RollbackResponse = subBuilder; + break; + } + case 58: { + global::Google.Cloud.Spanner.V1.CommitResponse subBuilder = new global::Google.Cloud.Spanner.V1.CommitResponse(); + if (responseCase_ == ResponseOneofCase.WriteMutationsResponse) { + subBuilder.MergeFrom(WriteMutationsResponse); } input.ReadMessage(subBuilder); - Row = subBuilder; + WriteMutationsResponse = subBuilder; break; } } @@ -4119,12 +5277,64 @@ public void MergeFrom(pb::CodedInputStream input) { _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); break; case 10: { - global::Google.Cloud.Spanner.V1.PartialResultSet subBuilder = new global::Google.Cloud.Spanner.V1.PartialResultSet(); - if (responseCase_ == ResponseOneofCase.Row) { - subBuilder.MergeFrom(Row); + if (status_ == null) { + Status = new global::Google.Rpc.Status(); + } + input.ReadMessage(Status); + break; + } + case 18: { + global::Google.Cloud.SpannerLib.V1.ExecuteResponse subBuilder = new global::Google.Cloud.SpannerLib.V1.ExecuteResponse(); + if (responseCase_ == ResponseOneofCase.ExecuteResponse) { + subBuilder.MergeFrom(ExecuteResponse); + } + input.ReadMessage(subBuilder); + ExecuteResponse = subBuilder; + break; + } + case 26: { + global::Google.Cloud.Spanner.V1.ExecuteBatchDmlResponse subBuilder = new global::Google.Cloud.Spanner.V1.ExecuteBatchDmlResponse(); + if (responseCase_ == ResponseOneofCase.ExecuteBatchResponse) { + subBuilder.MergeFrom(ExecuteBatchResponse); + } + input.ReadMessage(subBuilder); + ExecuteBatchResponse = subBuilder; + break; + } + case 34: { + global::Google.Protobuf.WellKnownTypes.Empty subBuilder = new global::Google.Protobuf.WellKnownTypes.Empty(); + if (responseCase_ == ResponseOneofCase.BeginTransactionResponse) { + subBuilder.MergeFrom(BeginTransactionResponse); + } + input.ReadMessage(subBuilder); + BeginTransactionResponse = subBuilder; + break; + } + case 42: { + global::Google.Cloud.Spanner.V1.CommitResponse subBuilder = new global::Google.Cloud.Spanner.V1.CommitResponse(); + if (responseCase_ == ResponseOneofCase.CommitResponse) { + subBuilder.MergeFrom(CommitResponse); + } + input.ReadMessage(subBuilder); + CommitResponse = subBuilder; + break; + } + case 50: { + global::Google.Protobuf.WellKnownTypes.Empty subBuilder = new global::Google.Protobuf.WellKnownTypes.Empty(); + if (responseCase_ == ResponseOneofCase.RollbackResponse) { + subBuilder.MergeFrom(RollbackResponse); + } + input.ReadMessage(subBuilder); + RollbackResponse = subBuilder; + break; + } + case 58: { + global::Google.Cloud.Spanner.V1.CommitResponse subBuilder = new global::Google.Cloud.Spanner.V1.CommitResponse(); + if (responseCase_ == ResponseOneofCase.WriteMutationsResponse) { + subBuilder.MergeFrom(WriteMutationsResponse); } input.ReadMessage(subBuilder); - Row = subBuilder; + WriteMutationsResponse = subBuilder; break; } } diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-v1/SpannerlibGrpc.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-v1/SpannerlibGrpc.cs index 6814c717..b64e5bd4 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-v1/SpannerlibGrpc.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-v1/SpannerlibGrpc.cs @@ -232,6 +232,14 @@ static T __Helper_DeserializeMessage(grpc::DeserializationContext context, gl __Marshaller_google_spannerlib_v1_ConnectionStreamRequest, __Marshaller_google_spannerlib_v1_ConnectionStreamResponse); + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + static readonly grpc::Method __Method_ContinueStreaming = new grpc::Method( + grpc::MethodType.ServerStreaming, + __ServiceName, + "ContinueStreaming", + __Marshaller_google_spannerlib_v1_Rows, + __Marshaller_google_spannerlib_v1_RowData); + /// Service descriptor public static global::Google.Protobuf.Reflection.ServiceDescriptor Descriptor { @@ -605,6 +613,16 @@ protected SpannerLibClient(ClientBaseConfiguration configuration) : base(configu { return CallInvoker.AsyncDuplexStreamingCall(__Method_ConnectionStream, null, options); } + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public virtual grpc::AsyncServerStreamingCall ContinueStreaming(global::Google.Cloud.SpannerLib.V1.Rows request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + return ContinueStreaming(request, new grpc::CallOptions(headers, deadline, cancellationToken)); + } + [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] + public virtual grpc::AsyncServerStreamingCall ContinueStreaming(global::Google.Cloud.SpannerLib.V1.Rows request, grpc::CallOptions options) + { + return CallInvoker.AsyncServerStreamingCall(__Method_ContinueStreaming, null, options, request); + } /// Creates a new instance of client from given ClientBaseConfiguration. [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] protected override SpannerLibClient NewInstance(ClientBaseConfiguration configuration) diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-v1/spannerlib-dotnet-grpc-v1.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-v1/spannerlib-dotnet-grpc-v1.csproj index d7c7d457..99dab0cd 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-v1/spannerlib-dotnet-grpc-v1.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-v1/spannerlib-dotnet-grpc-v1.csproj @@ -6,7 +6,7 @@ enable default Alpha.Google.Cloud.SpannerLib.Grpc.V1 - 1.0.0-alpha.20251027150914 + 1.0.0-alpha.20251207191144 Google diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/spannerlib-dotnet-mockserver.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/spannerlib-dotnet-mockserver.csproj index 6bd59205..08b92b7e 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/spannerlib-dotnet-mockserver.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/spannerlib-dotnet-mockserver.csproj @@ -6,7 +6,7 @@ enable enable Alpha.Google.Cloud.SpannerLib.MockServer - 1.0.0-alpha.20251027150914 + 1.0.0-alpha.20251207191144 Google default diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native-impl/SharedLibSpanner.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native-impl/SharedLibSpanner.cs index 0bfef295..906031ab 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native-impl/SharedLibSpanner.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native-impl/SharedLibSpanner.cs @@ -111,7 +111,7 @@ public void CloseConnection(Connection connection) return await Task.Run(() => WriteMutations(connection, mutations), cancellationToken).ConfigureAwait(false); } - public Rows Execute(Connection connection, ExecuteSqlRequest statement) + public Rows Execute(Connection connection, ExecuteSqlRequest statement, int prefetchRows = 0) { using var handler = ExecuteLibraryFunction(() => { @@ -122,7 +122,7 @@ public Rows Execute(Connection connection, ExecuteSqlRequest statement) return new Rows(connection, handler.ObjectId()); } - public async Task ExecuteAsync(Connection connection, ExecuteSqlRequest statement, CancellationToken cancellationToken) + public async Task ExecuteAsync(Connection connection, ExecuteSqlRequest statement, int prefetchRows = 0, CancellationToken cancellationToken = default) { return await Task.Run(() => Execute(connection, statement), cancellationToken).ConfigureAwait(false); } @@ -163,7 +163,7 @@ public long[] ExecuteBatch(Connection connection, ExecuteBatchDmlRequest stateme public async Task ExecuteBatchAsync(Connection connection, ExecuteBatchDmlRequest statements, CancellationToken cancellationToken = default) { - return await Task.Run(() => ExecuteBatch(connection, statements), cancellationToken).ConfigureAwait(false); + return await Task.Run(() => ExecuteBatch(connection, statements), cancellationToken).ConfigureAwait(false); } public ResultSetMetadata? Metadata(Rows rows) @@ -225,6 +225,11 @@ public void BeginTransaction(Connection connection, TransactionOptions transacti }); } + public Task BeginTransactionAsync(Connection connection, TransactionOptions transactionOptions, CancellationToken cancellationToken = default) + { + return Task.Run(() => BeginTransaction(connection, transactionOptions), cancellationToken); + } + public CommitResponse? Commit(Connection connection) { using var handler = ExecuteLibraryFunction(() => SpannerLib.Commit(connection.Pool.Id, connection.Id)); diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native-impl/spannerlib-dotnet-native-impl.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native-impl/spannerlib-dotnet-native-impl.csproj index 212bc985..5d18cb1c 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native-impl/spannerlib-dotnet-native-impl.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native-impl/spannerlib-dotnet-native-impl.csproj @@ -6,7 +6,7 @@ enable default Alpha.Google.Cloud.SpannerLib.NativeImpl - 1.0.0-alpha.20251027150914 + 1.0.0-alpha.20251207191144 Google @@ -16,7 +16,7 @@ - + diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native/spannerlib-dotnet-native.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native/spannerlib-dotnet-native.csproj index accc02ef..d8be70f9 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native/spannerlib-dotnet-native.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native/spannerlib-dotnet-native.csproj @@ -9,7 +9,7 @@ Alpha.Google.Cloud.SpannerLib.Native .NET wrapper for the native SpannerLib shared library Google - 1.0.0-alpha.20251027150914 + 1.0.0-alpha.20251207191144 diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-tests/AbstractMockServerTests.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-tests/AbstractMockServerTests.cs index a98b679b..69bb7c1c 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-tests/AbstractMockServerTests.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-tests/AbstractMockServerTests.cs @@ -26,12 +26,14 @@ public enum LibType Shared, Grpc, GrpcTcp, + GrpcBidi, } protected readonly Dictionary SpannerLibDictionary = new([ new KeyValuePair(LibType.Shared, new SharedLibSpanner()), - new KeyValuePair(LibType.Grpc, new GrpcLibSpanner()), - new KeyValuePair(LibType.GrpcTcp, new GrpcLibSpanner(addressType: Server.AddressType.Tcp)), + new KeyValuePair(LibType.Grpc, new GrpcLibSpanner(communicationStyle: GrpcLibSpanner.CommunicationStyle.ServerStreaming)), + new KeyValuePair(LibType.GrpcTcp, new GrpcLibSpanner(communicationStyle: GrpcLibSpanner.CommunicationStyle.ServerStreaming, addressType: Server.AddressType.Tcp)), + new KeyValuePair(LibType.GrpcBidi, new GrpcLibSpanner(communicationStyle: GrpcLibSpanner.CommunicationStyle.BidiStreaming)), ]); protected SpannerMockServerFixture Fixture; diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-tests/BasicTests.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-tests/BasicTests.cs index 81358026..151e3ec3 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-tests/BasicTests.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-tests/BasicTests.cs @@ -132,7 +132,6 @@ public void TestQueryParameterStartingWithUnderscore([Values] LibType libType) } [Test] - [Ignore("execute async disabled for now")] public async Task TestExecuteQueryAsync([Values] LibType libType) { using var pool = Pool.Create(SpannerLibDictionary[libType], ConnectionString); diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-tests/RowsTests.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-tests/RowsTests.cs index 3274a3f7..cd0929bb 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-tests/RowsTests.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-tests/RowsTests.cs @@ -16,6 +16,7 @@ using Google.Cloud.Spanner.V1; using Google.Cloud.SpannerLib.MockServer; using Google.Rpc; +using Grpc.Core; using TypeCode = Google.Cloud.Spanner.V1.TypeCode; namespace Google.Cloud.SpannerLib.Tests; @@ -51,19 +52,23 @@ public void TestEmptyResults([Values] LibType libType) Assert.That(rows.Metadata, Is.Not.Null); Assert.That(rows.Metadata.RowType.Fields.Count, Is.EqualTo(1)); Assert.That(rows.Next(), Is.Null); + Assert.That(rows.Next(), Is.Null); + Assert.That(rows.NextResultSet(), Is.False); + Assert.That(rows.Next(), Is.Null); + Assert.That(rows.Next(), Is.Null); + Assert.That(rows.NextResultSet(), Is.False); } [Test] - public void TestRandomResults([Values] LibType libType) + public void TestRandomResults([Values] LibType libType, [Values(0, 1, 10)] int numRows, [Values(0, 1, 5, 9, 10, 11)] int prefetchRows) { - var numRows = 10; var rowType = RandomResultSetGenerator.GenerateAllTypesRowType(); var results = RandomResultSetGenerator.Generate(rowType, numRows); Fixture.SpannerMock.AddOrUpdateStatementResult("select * from random", StatementResult.CreateQuery(results)); using var pool = Pool.Create(SpannerLibDictionary[libType], ConnectionString); using var connection = pool.CreateConnection(); - using var rows = connection.Execute(new ExecuteSqlRequest { Sql = "select * from random" }); + using var rows = connection.Execute(new ExecuteSqlRequest { Sql = "select * from random" }, prefetchRows: prefetchRows); var rowCount = 0; while (rows.Next() is { } row) @@ -101,9 +106,8 @@ public void TestLargeStringValue([Values] LibType libType) } [Test] - public void TestStopHalfway([Values] LibType libType) + public void TestStopHalfway([Values] LibType libType, [Values(2, 10)] int numRows, [Values(0, 1, 2, 3, 5, 9, 10, 11)] int prefetchRows) { - var numRows = 10; var rowType = RandomResultSetGenerator.GenerateAllTypesRowType(); var results = RandomResultSetGenerator.Generate(rowType, numRows); Fixture.SpannerMock.AddOrUpdateStatementResult("select * from random", StatementResult.CreateQuery(results)); @@ -111,7 +115,7 @@ public void TestStopHalfway([Values] LibType libType) using var pool = Pool.Create(SpannerLibDictionary[libType], ConnectionString); using var connection = pool.CreateConnection(); - using var rows = connection.Execute(new ExecuteSqlRequest { Sql = "select * from random" }); + using var rows = connection.Execute(new ExecuteSqlRequest { Sql = "select * from random" }, prefetchRows); Assert.That(rows.Metadata, Is.Not.Null); Assert.That(rows.Metadata.RowType.Fields.Count, Is.EqualTo(rowType.Fields.Count)); @@ -131,6 +135,78 @@ public void TestStopHalfway([Values] LibType libType) Assert.That(request.Transaction?.SingleUse?.ReadOnly?.HasStrong ?? false); } + [Test] + public void TestStopHalfwayTwoQueries([Values] LibType libType, [Values(0, 1, 2, 3)] int prefetchRows) + { + const string sql = "select c from my_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "c")], [[1L], [2L]])); + + using var pool = Pool.Create(SpannerLibDictionary[libType], ConnectionString); + using var connection = pool.CreateConnection(); + + for (var i = 0; i < 2; i++) + { + using var rows = connection.Execute(new ExecuteSqlRequest { Sql = sql }, prefetchRows); + Assert.That(rows.Metadata, Is.Not.Null); + Assert.That(rows.Metadata.RowType.Fields.Count, Is.EqualTo(1)); + var row = rows.Next(); + Assert.That(row, Is.Not.Null); + Assert.That(row.Values.Count, Is.EqualTo(1)); + Assert.That(row.Values[0].HasStringValue); + Assert.That(row.Values[0].StringValue, Is.EqualTo("1")); + } + Assert.That(Fixture.SpannerMock.Requests.OfType().Count(), Is.EqualTo(2)); + } + + [Test] + public void TestStopHalfwayMultipleQueries( + [Values] LibType libType, + [Values(2, 10)] int numRows, + [Values(0, 1, 2, 3, 5, 9, 10, 11)] int prefetchRows, + [Values(1, 2, 3)] int numQueries) + { + const string query = "select * from random"; + + var rowType = RandomResultSetGenerator.GenerateAllTypesRowType(); + var results = RandomResultSetGenerator.Generate(rowType, numRows); + Fixture.SpannerMock.AddOrUpdateStatementResult(query, StatementResult.CreateQuery(results)); + + var stopAfterRows = new int[numQueries]; + var queries = new string[numQueries]; + for (var i = 0; i < numQueries; i++) + { + stopAfterRows[i] = Random.Shared.Next(1, numRows - 1); + queries[i] = query; + } + + var sql = string.Join(";", queries); + using var pool = Pool.Create(SpannerLibDictionary[libType], ConnectionString); + using var connection = pool.CreateConnection(); + using var rows = connection.Execute(new ExecuteSqlRequest { Sql = sql }, prefetchRows); + Assert.That(rows.Metadata, Is.Not.Null); + Assert.That(rows.Metadata.RowType.Fields.Count, Is.EqualTo(rowType.Fields.Count)); + + var totalRowCount = 0; + for (var i = 0; i < numQueries; i++) + { + var rowCount = 0; + while (rows.Next() is { } row) + { + rowCount++; + totalRowCount++; + Assert.That(row.Values.Count, Is.EqualTo(rowType.Fields.Count)); + if (rowCount == stopAfterRows[i]) + { + break; + } + } + Assert.That(rows.NextResultSet(), Is.EqualTo(i < numQueries-1)); + } + Assert.That(Fixture.SpannerMock.Requests.OfType().Count(), Is.EqualTo(numQueries)); + Assert.That(totalRowCount, Is.EqualTo(stopAfterRows.Sum())); + } + [Test] public void TestCloseConnectionWithOpenRows([Values] LibType libType) { @@ -143,42 +219,44 @@ public void TestCloseConnectionWithOpenRows([Values] LibType libType) using var connection = pool.CreateConnection(); using var rows = connection.Execute(new ExecuteSqlRequest { Sql = "select * from random" }); + var foundRows = 0; // Verify that we can fetch the first row. Assert.That(rows.Next(), Is.Not.Null); + foundRows++; // Close the connection while the rows object is still open. connection.Close(); // Getting all the rows should not be possible. // If the underlying Rows object uses a stream, then it could be that it still receives some rows, but it will // eventually fail. - var exception = Assert.Throws(() => + var exception = Assert.Catch(() => { while (rows.Next() is not null) { + foundRows++; } }); - // The error is 'Connection not found' or an internal exception from the underlying driver, depending on exactly - // when the driver detects that the connection and all related objects have been closed. - Assert.That(exception.Code is Code.NotFound or Code.Unknown, Is.True); - - if (libType == LibType.Shared) + if (exception is SpannerException spannerException) + { + // The error is 'Connection not found' or an internal exception from the underlying driver, depending on exactly + // when the driver detects that the connection and all related objects have been closed. + Assert.That(spannerException.Code is Code.NotFound or Code.Unknown, Is.True); + } + else { - // TODO: Remove this once it has been fixed in the shared library. - // Closing a Rows object that has already been closed because the connection has been closed, should - // be a no-op. - var closeException = Assert.Throws(() => rows.Close()); - Assert.That(closeException.Code, Is.EqualTo(Code.NotFound)); + Assert.That(exception, Is.InstanceOf()); } + Assert.That(foundRows, Is.LessThan(numRows)); } [Test] - public void TestExecuteDml([Values] LibType libType) + public void TestExecuteDml([Values] LibType libType, [Values(0, 1, 2)] int prefetchRows) { var sql = "update my_table set value=1 where id=2"; Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(1L)); using var pool = Pool.Create(SpannerLibDictionary[libType], ConnectionString); using var connection = pool.CreateConnection(); - using var rows = connection.Execute(new ExecuteSqlRequest { Sql = sql }); + using var rows = connection.Execute(new ExecuteSqlRequest { Sql = sql }, prefetchRows); Assert.That(rows.Next(), Is.Null); Assert.That(rows.UpdateCount, Is.EqualTo(1L)); @@ -266,13 +344,12 @@ public async Task TestMultipleQueries([Values] LibType libType, [Values] bool as } [Test] - public async Task TestMultipleMixedStatements([Values] LibType libType, [Values] bool async) + public async Task TestMultipleMixedStatements([Values] LibType libType, [Values(2, 10)] int numRows, [Values(0, 1, 2, 3, 5, 9, 10, 11)] int prefetchRows, [Values] bool async) { var updateCount = 3L; var dml = "update my_table set value=1 where id in (1,2,3)"; Fixture.SpannerMock.AddOrUpdateStatementResult(dml, StatementResult.CreateUpdateCount(updateCount)); - var numRows = 10; var rowType = RandomResultSetGenerator.GenerateAllTypesRowType(); var results = RandomResultSetGenerator.Generate(rowType, numRows); var query = "select * from random"; @@ -284,9 +361,9 @@ public async Task TestMultipleMixedStatements([Values] LibType libType, [Values] await using var pool = Pool.Create(SpannerLibDictionary[libType], ConnectionString); await using var connection = pool.CreateConnection(); await using var rows = async - ? await connection.ExecuteAsync(new ExecuteSqlRequest { Sql = sql }) + ? await connection.ExecuteAsync(new ExecuteSqlRequest { Sql = sql }, prefetchRows) // ReSharper disable once MethodHasAsyncOverload - : connection.Execute(new ExecuteSqlRequest { Sql = sql }); + : connection.Execute(new ExecuteSqlRequest { Sql = sql }, prefetchRows); var numResultSets = 0; var totalRows = 0; @@ -294,7 +371,7 @@ public async Task TestMultipleMixedStatements([Values] LibType libType, [Values] do { // ReSharper disable once MethodHasAsyncOverload - while ((async ? await rows.NextAsync() : rows.Next()) is { } row) + while ((async ? await rows.NextAsync() : rows.Next()) is not null) { totalRows++; } @@ -313,5 +390,117 @@ public async Task TestMultipleMixedStatements([Values] LibType libType, [Values] // There are 5 statements in the SQL string. Assert.That(numResultSets, Is.EqualTo(5)); } + + [Test] + public async Task TestMultipleMixedStatementsWithErrors( + [Values] LibType libType, + [Values(2, 10)] int numRows, + [Values(0, 1, 2, 3, 5, 9, 10, 11)] int prefetchRows, + [Values(0, 1, 2, 3, 4, 5)] int errorIndex, + [Values] bool async) + { + const long updateCount = 3L; + const string dml = "update my_table set value=1 where id in (1,2,3)"; + Fixture.SpannerMock.AddOrUpdateStatementResult(dml, StatementResult.CreateUpdateCount(updateCount)); + + var rowType = RandomResultSetGenerator.GenerateAllTypesRowType(); + var results = RandomResultSetGenerator.Generate(rowType, numRows); + const string query = "select * from random"; + Fixture.SpannerMock.AddOrUpdateStatementResult(query, StatementResult.CreateQuery(results)); + + const string invalidQuery = "select * from unknown_table"; + Fixture.SpannerMock.AddOrUpdateStatementResult(invalidQuery, StatementResult.CreateException(new RpcException(new global::Grpc.Core.Status(StatusCode.NotFound, "Table not found")))); + + // Create a SQL string containing a mix of DML and queries. + var numStatements = Random.Shared.Next(errorIndex + 1, errorIndex + 6); + var statements = new string[numStatements]; + var statementIsDml = new bool[numStatements]; + for (var i = 0; i < statements.Length; i++) + { + if (errorIndex == i) + { + statements[i] = invalidQuery; + } + else + { + statementIsDml[i] = Random.Shared.Next(2) == 1; + statements[i] = statementIsDml[i] ? dml : query; + } + } + var sql = string.Join(";", statements); + + await using var pool = Pool.Create(SpannerLibDictionary[libType], ConnectionString); + await using var connection = pool.CreateConnection(); + var numResultSets = 0; + var totalRows = 0; + var totalUpdateCount = 0L; + + if (errorIndex == 0) + { + if (async) + { + Assert.ThrowsAsync(() => connection.ExecuteAsync(new ExecuteSqlRequest { Sql = sql }, prefetchRows)); + } + else + { + Assert.Throws(() => connection.Execute(new ExecuteSqlRequest { Sql = sql }, prefetchRows)); + } + } + else + { + await using var rows = async + ? await connection.ExecuteAsync(new ExecuteSqlRequest { Sql = sql }, prefetchRows) + // ReSharper disable once MethodHasAsyncOverload + : connection.Execute(new ExecuteSqlRequest { Sql = sql }, prefetchRows); + + var statementIndex = 0; + while (statementIndex < numStatements) + { + // ReSharper disable once MethodHasAsyncOverload + while ((async ? await rows.NextAsync() : rows.Next()) is not null) + { + totalRows++; + } + numResultSets++; + if (rows.UpdateCount > -1) + { + totalUpdateCount += rows.UpdateCount; + } + statementIndex++; + if (statementIndex == errorIndex) + { + if (async) + { + Assert.ThrowsAsync(() => rows.NextResultSetAsync()); + } + else + { + Assert.Throws(() => rows.NextResultSet()); + } + break; + } + // ReSharper disable once MethodHasAsyncOverload + Assert.That(async ? await rows.NextResultSetAsync() : rows.NextResultSet()); + } + } + + var expectedUpdateCount = 0L; + var expectedRowCount = 0; + for (var i = 0; i < errorIndex; i++) + { + if (statementIsDml[i]) + { + expectedUpdateCount += updateCount; + } + else + { + expectedRowCount += numRows; + } + } + + Assert.That(totalRows, Is.EqualTo(expectedRowCount)); + Assert.That(totalUpdateCount, Is.EqualTo(expectedUpdateCount)); + Assert.That(numResultSets, Is.EqualTo(errorIndex)); + } } \ No newline at end of file diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/Connection.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/Connection.cs index e61a79d5..90ec12cb 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/Connection.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/Connection.cs @@ -43,6 +43,23 @@ public void BeginTransaction(TransactionOptions transactionOptions) Spanner.BeginTransaction(this, transactionOptions); } + /// + /// Begins a new transaction on this connection. A connection can have at most one active transaction at any time. + /// Calling this method does not immediately start the transaction on Spanner. Instead, the transaction is only + /// registered on the connection, and the BeginTransaction option will be inlined with the first statement in the + /// transaction. + /// + /// + /// The transaction options that will be used to create the transaction. The default is a read/write transaction. + /// Explicitly set the ReadOnly transaction option to start a read-only transaction. + /// + /// The cancellation token + public Task BeginTransactionAsync(TransactionOptions transactionOptions, + CancellationToken cancellationToken = default) + { + return Spanner.BeginTransactionAsync(this, transactionOptions, cancellationToken); + } + /// /// Commits the current transaction on this connection and returns the CommitResponse (if any). Both read/write and /// read-only transactions must be either committed or rolled back. Committing or rolling back a read-only @@ -112,10 +129,11 @@ public Task RollbackAsync(CancellationToken cancellationToken = default) /// connection. The contents of the returned Rows object depends on the type of SQL statement. /// /// The SQL statement that should be executed + /// The number of rows to prefetch and include in the initial result /// A Rows object with the statement result - public Rows Execute(ExecuteSqlRequest statement) + public Rows Execute(ExecuteSqlRequest statement, int prefetchRows = 0) { - return Spanner.Execute(this, statement); + return Spanner.Execute(this, statement, prefetchRows); } /// @@ -123,11 +141,12 @@ public Rows Execute(ExecuteSqlRequest statement) /// connection. The contents of the returned Rows object depends on the type of SQL statement. /// /// The SQL statement that should be executed + /// The number of rows to prefetch and include in the initial result /// The cancellation token /// A Rows object with the statement result - public Task ExecuteAsync(ExecuteSqlRequest statement, CancellationToken cancellationToken = default) + public Task ExecuteAsync(ExecuteSqlRequest statement, int prefetchRows = 0, CancellationToken cancellationToken = default) { - return Spanner.ExecuteAsync(this, statement, cancellationToken); + return Spanner.ExecuteAsync(this, statement, prefetchRows, cancellationToken); } /// diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/ISpannerLib.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/ISpannerLib.cs index 445cffbc..ef88890b 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/ISpannerLib.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/ISpannerLib.cs @@ -111,23 +111,25 @@ public enum RowEncoding /// /// The connection to use to execute the SQL statement /// The statement to execute + /// The number of rows to prefetch and include in the initial result /// /// A Rows object with the results of the statement. The contents of the Rows object depends on the type of SQL /// statement. /// - public Rows Execute(Connection connection, ExecuteSqlRequest statement); + public Rows Execute(Connection connection, ExecuteSqlRequest statement, int prefetchRows = 0); /// /// Executes a SQL statement of any type on the given connection. /// /// The connection to use to execute the SQL statement /// The statement to execute + /// The number of rows to prefetch and include in the initial result /// The cancellation token /// /// A Rows object with the results of the statement. The contents of the Rows object depends on the type of SQL /// statement. /// - public Task ExecuteAsync(Connection connection, ExecuteSqlRequest statement, CancellationToken cancellationToken = default); + public Task ExecuteAsync(Connection connection, ExecuteSqlRequest statement, int prefetchRows = 0, CancellationToken cancellationToken = default); /// /// Executes a batch of DML or DDL statements on Spanner. The batch may not contain a mix of DML and DDL statements. @@ -236,6 +238,18 @@ public enum RowEncoding /// to create a read-only transaction. /// public void BeginTransaction(Connection connection, TransactionOptions transactionOptions); + + /// + /// Starts a new transaction on this connection. A connection can have at most one transaction at any time. All + /// transactions, including read-only transactions, must be either committed or rolled back. + /// + /// The connection to use to start the transaction + /// + /// The options for the new transaction. The default is to create a read/write transaction. Set the ReadOnly option + /// to create a read-only transaction. + /// + /// The cancellation token + public Task BeginTransactionAsync(Connection connection, TransactionOptions transactionOptions, CancellationToken cancellationToken = default); /// /// Commits the current transaction on this connection. diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/Rows.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/Rows.cs index d932137d..cb25ad0e 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/Rows.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/Rows.cs @@ -128,7 +128,7 @@ public long GetTotalUpdateCount() public async Task GetTotalUpdateCountAsync(CancellationToken cancellationToken = default) { var result = UpdateCount; - while (await NextResultSetAsync(cancellationToken)) + while (await NextResultSetAsync(cancellationToken).ConfigureAwait(false)) { result += UpdateCount; } diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/spannerlib-dotnet.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/spannerlib-dotnet.csproj index 0dc4211a..52b173bc 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/spannerlib-dotnet.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/spannerlib-dotnet.csproj @@ -6,7 +6,7 @@ enable default Alpha.Google.Cloud.SpannerLib - 1.0.0-alpha.20251027150914 + 1.0.0-alpha.20251207191144 Google diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ConnectionStreamRequest.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ConnectionStreamRequest.java index d5e213d2..59acebe3 100644 --- a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ConnectionStreamRequest.java +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ConnectionStreamRequest.java @@ -50,6 +50,11 @@ public enum RequestCase implements com.google.protobuf.Internal.EnumLite, com.google.protobuf.AbstractMessage.InternalOneOfEnum { EXECUTE_REQUEST(1), + EXECUTE_BATCH_REQUEST(2), + BEGIN_TRANSACTION_REQUEST(3), + COMMIT_REQUEST(4), + ROLLBACK_REQUEST(5), + WRITE_MUTATIONS_REQUEST(6), REQUEST_NOT_SET(0); private final int value; private RequestCase(int value) { @@ -68,6 +73,11 @@ public static RequestCase valueOf(int value) { public static RequestCase forNumber(int value) { switch (value) { case 1: return EXECUTE_REQUEST; + case 2: return EXECUTE_BATCH_REQUEST; + case 3: return BEGIN_TRANSACTION_REQUEST; + case 4: return COMMIT_REQUEST; + case 5: return ROLLBACK_REQUEST; + case 6: return WRITE_MUTATIONS_REQUEST; case 0: return REQUEST_NOT_SET; default: return null; } @@ -114,6 +124,161 @@ public com.google.cloud.spannerlib.v1.ExecuteRequestOrBuilder getExecuteRequestO return com.google.cloud.spannerlib.v1.ExecuteRequest.getDefaultInstance(); } + public static final int EXECUTE_BATCH_REQUEST_FIELD_NUMBER = 2; + /** + * .google.spannerlib.v1.ExecuteBatchRequest execute_batch_request = 2; + * @return Whether the executeBatchRequest field is set. + */ + @java.lang.Override + public boolean hasExecuteBatchRequest() { + return requestCase_ == 2; + } + /** + * .google.spannerlib.v1.ExecuteBatchRequest execute_batch_request = 2; + * @return The executeBatchRequest. + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.ExecuteBatchRequest getExecuteBatchRequest() { + if (requestCase_ == 2) { + return (com.google.cloud.spannerlib.v1.ExecuteBatchRequest) request_; + } + return com.google.cloud.spannerlib.v1.ExecuteBatchRequest.getDefaultInstance(); + } + /** + * .google.spannerlib.v1.ExecuteBatchRequest execute_batch_request = 2; + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.ExecuteBatchRequestOrBuilder getExecuteBatchRequestOrBuilder() { + if (requestCase_ == 2) { + return (com.google.cloud.spannerlib.v1.ExecuteBatchRequest) request_; + } + return com.google.cloud.spannerlib.v1.ExecuteBatchRequest.getDefaultInstance(); + } + + public static final int BEGIN_TRANSACTION_REQUEST_FIELD_NUMBER = 3; + /** + * .google.spannerlib.v1.BeginTransactionRequest begin_transaction_request = 3; + * @return Whether the beginTransactionRequest field is set. + */ + @java.lang.Override + public boolean hasBeginTransactionRequest() { + return requestCase_ == 3; + } + /** + * .google.spannerlib.v1.BeginTransactionRequest begin_transaction_request = 3; + * @return The beginTransactionRequest. + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.BeginTransactionRequest getBeginTransactionRequest() { + if (requestCase_ == 3) { + return (com.google.cloud.spannerlib.v1.BeginTransactionRequest) request_; + } + return com.google.cloud.spannerlib.v1.BeginTransactionRequest.getDefaultInstance(); + } + /** + * .google.spannerlib.v1.BeginTransactionRequest begin_transaction_request = 3; + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.BeginTransactionRequestOrBuilder getBeginTransactionRequestOrBuilder() { + if (requestCase_ == 3) { + return (com.google.cloud.spannerlib.v1.BeginTransactionRequest) request_; + } + return com.google.cloud.spannerlib.v1.BeginTransactionRequest.getDefaultInstance(); + } + + public static final int COMMIT_REQUEST_FIELD_NUMBER = 4; + /** + * .google.spannerlib.v1.Connection commit_request = 4; + * @return Whether the commitRequest field is set. + */ + @java.lang.Override + public boolean hasCommitRequest() { + return requestCase_ == 4; + } + /** + * .google.spannerlib.v1.Connection commit_request = 4; + * @return The commitRequest. + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.Connection getCommitRequest() { + if (requestCase_ == 4) { + return (com.google.cloud.spannerlib.v1.Connection) request_; + } + return com.google.cloud.spannerlib.v1.Connection.getDefaultInstance(); + } + /** + * .google.spannerlib.v1.Connection commit_request = 4; + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.ConnectionOrBuilder getCommitRequestOrBuilder() { + if (requestCase_ == 4) { + return (com.google.cloud.spannerlib.v1.Connection) request_; + } + return com.google.cloud.spannerlib.v1.Connection.getDefaultInstance(); + } + + public static final int ROLLBACK_REQUEST_FIELD_NUMBER = 5; + /** + * .google.spannerlib.v1.Connection rollback_request = 5; + * @return Whether the rollbackRequest field is set. + */ + @java.lang.Override + public boolean hasRollbackRequest() { + return requestCase_ == 5; + } + /** + * .google.spannerlib.v1.Connection rollback_request = 5; + * @return The rollbackRequest. + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.Connection getRollbackRequest() { + if (requestCase_ == 5) { + return (com.google.cloud.spannerlib.v1.Connection) request_; + } + return com.google.cloud.spannerlib.v1.Connection.getDefaultInstance(); + } + /** + * .google.spannerlib.v1.Connection rollback_request = 5; + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.ConnectionOrBuilder getRollbackRequestOrBuilder() { + if (requestCase_ == 5) { + return (com.google.cloud.spannerlib.v1.Connection) request_; + } + return com.google.cloud.spannerlib.v1.Connection.getDefaultInstance(); + } + + public static final int WRITE_MUTATIONS_REQUEST_FIELD_NUMBER = 6; + /** + * .google.spannerlib.v1.WriteMutationsRequest write_mutations_request = 6; + * @return Whether the writeMutationsRequest field is set. + */ + @java.lang.Override + public boolean hasWriteMutationsRequest() { + return requestCase_ == 6; + } + /** + * .google.spannerlib.v1.WriteMutationsRequest write_mutations_request = 6; + * @return The writeMutationsRequest. + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.WriteMutationsRequest getWriteMutationsRequest() { + if (requestCase_ == 6) { + return (com.google.cloud.spannerlib.v1.WriteMutationsRequest) request_; + } + return com.google.cloud.spannerlib.v1.WriteMutationsRequest.getDefaultInstance(); + } + /** + * .google.spannerlib.v1.WriteMutationsRequest write_mutations_request = 6; + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.WriteMutationsRequestOrBuilder getWriteMutationsRequestOrBuilder() { + if (requestCase_ == 6) { + return (com.google.cloud.spannerlib.v1.WriteMutationsRequest) request_; + } + return com.google.cloud.spannerlib.v1.WriteMutationsRequest.getDefaultInstance(); + } + private byte memoizedIsInitialized = -1; @java.lang.Override public final boolean isInitialized() { @@ -131,6 +296,21 @@ public void writeTo(com.google.protobuf.CodedOutputStream output) if (requestCase_ == 1) { output.writeMessage(1, (com.google.cloud.spannerlib.v1.ExecuteRequest) request_); } + if (requestCase_ == 2) { + output.writeMessage(2, (com.google.cloud.spannerlib.v1.ExecuteBatchRequest) request_); + } + if (requestCase_ == 3) { + output.writeMessage(3, (com.google.cloud.spannerlib.v1.BeginTransactionRequest) request_); + } + if (requestCase_ == 4) { + output.writeMessage(4, (com.google.cloud.spannerlib.v1.Connection) request_); + } + if (requestCase_ == 5) { + output.writeMessage(5, (com.google.cloud.spannerlib.v1.Connection) request_); + } + if (requestCase_ == 6) { + output.writeMessage(6, (com.google.cloud.spannerlib.v1.WriteMutationsRequest) request_); + } getUnknownFields().writeTo(output); } @@ -144,6 +324,26 @@ public int getSerializedSize() { size += com.google.protobuf.CodedOutputStream .computeMessageSize(1, (com.google.cloud.spannerlib.v1.ExecuteRequest) request_); } + if (requestCase_ == 2) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(2, (com.google.cloud.spannerlib.v1.ExecuteBatchRequest) request_); + } + if (requestCase_ == 3) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(3, (com.google.cloud.spannerlib.v1.BeginTransactionRequest) request_); + } + if (requestCase_ == 4) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(4, (com.google.cloud.spannerlib.v1.Connection) request_); + } + if (requestCase_ == 5) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(5, (com.google.cloud.spannerlib.v1.Connection) request_); + } + if (requestCase_ == 6) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(6, (com.google.cloud.spannerlib.v1.WriteMutationsRequest) request_); + } size += getUnknownFields().getSerializedSize(); memoizedSize = size; return size; @@ -165,6 +365,26 @@ public boolean equals(final java.lang.Object obj) { if (!getExecuteRequest() .equals(other.getExecuteRequest())) return false; break; + case 2: + if (!getExecuteBatchRequest() + .equals(other.getExecuteBatchRequest())) return false; + break; + case 3: + if (!getBeginTransactionRequest() + .equals(other.getBeginTransactionRequest())) return false; + break; + case 4: + if (!getCommitRequest() + .equals(other.getCommitRequest())) return false; + break; + case 5: + if (!getRollbackRequest() + .equals(other.getRollbackRequest())) return false; + break; + case 6: + if (!getWriteMutationsRequest() + .equals(other.getWriteMutationsRequest())) return false; + break; case 0: default: } @@ -184,6 +404,26 @@ public int hashCode() { hash = (37 * hash) + EXECUTE_REQUEST_FIELD_NUMBER; hash = (53 * hash) + getExecuteRequest().hashCode(); break; + case 2: + hash = (37 * hash) + EXECUTE_BATCH_REQUEST_FIELD_NUMBER; + hash = (53 * hash) + getExecuteBatchRequest().hashCode(); + break; + case 3: + hash = (37 * hash) + BEGIN_TRANSACTION_REQUEST_FIELD_NUMBER; + hash = (53 * hash) + getBeginTransactionRequest().hashCode(); + break; + case 4: + hash = (37 * hash) + COMMIT_REQUEST_FIELD_NUMBER; + hash = (53 * hash) + getCommitRequest().hashCode(); + break; + case 5: + hash = (37 * hash) + ROLLBACK_REQUEST_FIELD_NUMBER; + hash = (53 * hash) + getRollbackRequest().hashCode(); + break; + case 6: + hash = (37 * hash) + WRITE_MUTATIONS_REQUEST_FIELD_NUMBER; + hash = (53 * hash) + getWriteMutationsRequest().hashCode(); + break; case 0: default: } @@ -321,6 +561,21 @@ public Builder clear() { if (executeRequestBuilder_ != null) { executeRequestBuilder_.clear(); } + if (executeBatchRequestBuilder_ != null) { + executeBatchRequestBuilder_.clear(); + } + if (beginTransactionRequestBuilder_ != null) { + beginTransactionRequestBuilder_.clear(); + } + if (commitRequestBuilder_ != null) { + commitRequestBuilder_.clear(); + } + if (rollbackRequestBuilder_ != null) { + rollbackRequestBuilder_.clear(); + } + if (writeMutationsRequestBuilder_ != null) { + writeMutationsRequestBuilder_.clear(); + } requestCase_ = 0; request_ = null; return this; @@ -366,6 +621,26 @@ private void buildPartialOneofs(com.google.cloud.spannerlib.v1.ConnectionStreamR executeRequestBuilder_ != null) { result.request_ = executeRequestBuilder_.build(); } + if (requestCase_ == 2 && + executeBatchRequestBuilder_ != null) { + result.request_ = executeBatchRequestBuilder_.build(); + } + if (requestCase_ == 3 && + beginTransactionRequestBuilder_ != null) { + result.request_ = beginTransactionRequestBuilder_.build(); + } + if (requestCase_ == 4 && + commitRequestBuilder_ != null) { + result.request_ = commitRequestBuilder_.build(); + } + if (requestCase_ == 5 && + rollbackRequestBuilder_ != null) { + result.request_ = rollbackRequestBuilder_.build(); + } + if (requestCase_ == 6 && + writeMutationsRequestBuilder_ != null) { + result.request_ = writeMutationsRequestBuilder_.build(); + } } @java.lang.Override @@ -385,6 +660,26 @@ public Builder mergeFrom(com.google.cloud.spannerlib.v1.ConnectionStreamRequest mergeExecuteRequest(other.getExecuteRequest()); break; } + case EXECUTE_BATCH_REQUEST: { + mergeExecuteBatchRequest(other.getExecuteBatchRequest()); + break; + } + case BEGIN_TRANSACTION_REQUEST: { + mergeBeginTransactionRequest(other.getBeginTransactionRequest()); + break; + } + case COMMIT_REQUEST: { + mergeCommitRequest(other.getCommitRequest()); + break; + } + case ROLLBACK_REQUEST: { + mergeRollbackRequest(other.getRollbackRequest()); + break; + } + case WRITE_MUTATIONS_REQUEST: { + mergeWriteMutationsRequest(other.getWriteMutationsRequest()); + break; + } case REQUEST_NOT_SET: { break; } @@ -422,6 +717,41 @@ public Builder mergeFrom( requestCase_ = 1; break; } // case 10 + case 18: { + input.readMessage( + internalGetExecuteBatchRequestFieldBuilder().getBuilder(), + extensionRegistry); + requestCase_ = 2; + break; + } // case 18 + case 26: { + input.readMessage( + internalGetBeginTransactionRequestFieldBuilder().getBuilder(), + extensionRegistry); + requestCase_ = 3; + break; + } // case 26 + case 34: { + input.readMessage( + internalGetCommitRequestFieldBuilder().getBuilder(), + extensionRegistry); + requestCase_ = 4; + break; + } // case 34 + case 42: { + input.readMessage( + internalGetRollbackRequestFieldBuilder().getBuilder(), + extensionRegistry); + requestCase_ = 5; + break; + } // case 42 + case 50: { + input.readMessage( + internalGetWriteMutationsRequestFieldBuilder().getBuilder(), + extensionRegistry); + requestCase_ = 6; + break; + } // case 50 default: { if (!super.parseUnknownField(input, extensionRegistry, tag)) { done = true; // was an endgroup tag @@ -596,6 +926,716 @@ public com.google.cloud.spannerlib.v1.ExecuteRequestOrBuilder getExecuteRequestO return executeRequestBuilder_; } + private com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.ExecuteBatchRequest, com.google.cloud.spannerlib.v1.ExecuteBatchRequest.Builder, com.google.cloud.spannerlib.v1.ExecuteBatchRequestOrBuilder> executeBatchRequestBuilder_; + /** + * .google.spannerlib.v1.ExecuteBatchRequest execute_batch_request = 2; + * @return Whether the executeBatchRequest field is set. + */ + @java.lang.Override + public boolean hasExecuteBatchRequest() { + return requestCase_ == 2; + } + /** + * .google.spannerlib.v1.ExecuteBatchRequest execute_batch_request = 2; + * @return The executeBatchRequest. + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.ExecuteBatchRequest getExecuteBatchRequest() { + if (executeBatchRequestBuilder_ == null) { + if (requestCase_ == 2) { + return (com.google.cloud.spannerlib.v1.ExecuteBatchRequest) request_; + } + return com.google.cloud.spannerlib.v1.ExecuteBatchRequest.getDefaultInstance(); + } else { + if (requestCase_ == 2) { + return executeBatchRequestBuilder_.getMessage(); + } + return com.google.cloud.spannerlib.v1.ExecuteBatchRequest.getDefaultInstance(); + } + } + /** + * .google.spannerlib.v1.ExecuteBatchRequest execute_batch_request = 2; + */ + public Builder setExecuteBatchRequest(com.google.cloud.spannerlib.v1.ExecuteBatchRequest value) { + if (executeBatchRequestBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + request_ = value; + onChanged(); + } else { + executeBatchRequestBuilder_.setMessage(value); + } + requestCase_ = 2; + return this; + } + /** + * .google.spannerlib.v1.ExecuteBatchRequest execute_batch_request = 2; + */ + public Builder setExecuteBatchRequest( + com.google.cloud.spannerlib.v1.ExecuteBatchRequest.Builder builderForValue) { + if (executeBatchRequestBuilder_ == null) { + request_ = builderForValue.build(); + onChanged(); + } else { + executeBatchRequestBuilder_.setMessage(builderForValue.build()); + } + requestCase_ = 2; + return this; + } + /** + * .google.spannerlib.v1.ExecuteBatchRequest execute_batch_request = 2; + */ + public Builder mergeExecuteBatchRequest(com.google.cloud.spannerlib.v1.ExecuteBatchRequest value) { + if (executeBatchRequestBuilder_ == null) { + if (requestCase_ == 2 && + request_ != com.google.cloud.spannerlib.v1.ExecuteBatchRequest.getDefaultInstance()) { + request_ = com.google.cloud.spannerlib.v1.ExecuteBatchRequest.newBuilder((com.google.cloud.spannerlib.v1.ExecuteBatchRequest) request_) + .mergeFrom(value).buildPartial(); + } else { + request_ = value; + } + onChanged(); + } else { + if (requestCase_ == 2) { + executeBatchRequestBuilder_.mergeFrom(value); + } else { + executeBatchRequestBuilder_.setMessage(value); + } + } + requestCase_ = 2; + return this; + } + /** + * .google.spannerlib.v1.ExecuteBatchRequest execute_batch_request = 2; + */ + public Builder clearExecuteBatchRequest() { + if (executeBatchRequestBuilder_ == null) { + if (requestCase_ == 2) { + requestCase_ = 0; + request_ = null; + onChanged(); + } + } else { + if (requestCase_ == 2) { + requestCase_ = 0; + request_ = null; + } + executeBatchRequestBuilder_.clear(); + } + return this; + } + /** + * .google.spannerlib.v1.ExecuteBatchRequest execute_batch_request = 2; + */ + public com.google.cloud.spannerlib.v1.ExecuteBatchRequest.Builder getExecuteBatchRequestBuilder() { + return internalGetExecuteBatchRequestFieldBuilder().getBuilder(); + } + /** + * .google.spannerlib.v1.ExecuteBatchRequest execute_batch_request = 2; + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.ExecuteBatchRequestOrBuilder getExecuteBatchRequestOrBuilder() { + if ((requestCase_ == 2) && (executeBatchRequestBuilder_ != null)) { + return executeBatchRequestBuilder_.getMessageOrBuilder(); + } else { + if (requestCase_ == 2) { + return (com.google.cloud.spannerlib.v1.ExecuteBatchRequest) request_; + } + return com.google.cloud.spannerlib.v1.ExecuteBatchRequest.getDefaultInstance(); + } + } + /** + * .google.spannerlib.v1.ExecuteBatchRequest execute_batch_request = 2; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.ExecuteBatchRequest, com.google.cloud.spannerlib.v1.ExecuteBatchRequest.Builder, com.google.cloud.spannerlib.v1.ExecuteBatchRequestOrBuilder> + internalGetExecuteBatchRequestFieldBuilder() { + if (executeBatchRequestBuilder_ == null) { + if (!(requestCase_ == 2)) { + request_ = com.google.cloud.spannerlib.v1.ExecuteBatchRequest.getDefaultInstance(); + } + executeBatchRequestBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.ExecuteBatchRequest, com.google.cloud.spannerlib.v1.ExecuteBatchRequest.Builder, com.google.cloud.spannerlib.v1.ExecuteBatchRequestOrBuilder>( + (com.google.cloud.spannerlib.v1.ExecuteBatchRequest) request_, + getParentForChildren(), + isClean()); + request_ = null; + } + requestCase_ = 2; + onChanged(); + return executeBatchRequestBuilder_; + } + + private com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.BeginTransactionRequest, com.google.cloud.spannerlib.v1.BeginTransactionRequest.Builder, com.google.cloud.spannerlib.v1.BeginTransactionRequestOrBuilder> beginTransactionRequestBuilder_; + /** + * .google.spannerlib.v1.BeginTransactionRequest begin_transaction_request = 3; + * @return Whether the beginTransactionRequest field is set. + */ + @java.lang.Override + public boolean hasBeginTransactionRequest() { + return requestCase_ == 3; + } + /** + * .google.spannerlib.v1.BeginTransactionRequest begin_transaction_request = 3; + * @return The beginTransactionRequest. + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.BeginTransactionRequest getBeginTransactionRequest() { + if (beginTransactionRequestBuilder_ == null) { + if (requestCase_ == 3) { + return (com.google.cloud.spannerlib.v1.BeginTransactionRequest) request_; + } + return com.google.cloud.spannerlib.v1.BeginTransactionRequest.getDefaultInstance(); + } else { + if (requestCase_ == 3) { + return beginTransactionRequestBuilder_.getMessage(); + } + return com.google.cloud.spannerlib.v1.BeginTransactionRequest.getDefaultInstance(); + } + } + /** + * .google.spannerlib.v1.BeginTransactionRequest begin_transaction_request = 3; + */ + public Builder setBeginTransactionRequest(com.google.cloud.spannerlib.v1.BeginTransactionRequest value) { + if (beginTransactionRequestBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + request_ = value; + onChanged(); + } else { + beginTransactionRequestBuilder_.setMessage(value); + } + requestCase_ = 3; + return this; + } + /** + * .google.spannerlib.v1.BeginTransactionRequest begin_transaction_request = 3; + */ + public Builder setBeginTransactionRequest( + com.google.cloud.spannerlib.v1.BeginTransactionRequest.Builder builderForValue) { + if (beginTransactionRequestBuilder_ == null) { + request_ = builderForValue.build(); + onChanged(); + } else { + beginTransactionRequestBuilder_.setMessage(builderForValue.build()); + } + requestCase_ = 3; + return this; + } + /** + * .google.spannerlib.v1.BeginTransactionRequest begin_transaction_request = 3; + */ + public Builder mergeBeginTransactionRequest(com.google.cloud.spannerlib.v1.BeginTransactionRequest value) { + if (beginTransactionRequestBuilder_ == null) { + if (requestCase_ == 3 && + request_ != com.google.cloud.spannerlib.v1.BeginTransactionRequest.getDefaultInstance()) { + request_ = com.google.cloud.spannerlib.v1.BeginTransactionRequest.newBuilder((com.google.cloud.spannerlib.v1.BeginTransactionRequest) request_) + .mergeFrom(value).buildPartial(); + } else { + request_ = value; + } + onChanged(); + } else { + if (requestCase_ == 3) { + beginTransactionRequestBuilder_.mergeFrom(value); + } else { + beginTransactionRequestBuilder_.setMessage(value); + } + } + requestCase_ = 3; + return this; + } + /** + * .google.spannerlib.v1.BeginTransactionRequest begin_transaction_request = 3; + */ + public Builder clearBeginTransactionRequest() { + if (beginTransactionRequestBuilder_ == null) { + if (requestCase_ == 3) { + requestCase_ = 0; + request_ = null; + onChanged(); + } + } else { + if (requestCase_ == 3) { + requestCase_ = 0; + request_ = null; + } + beginTransactionRequestBuilder_.clear(); + } + return this; + } + /** + * .google.spannerlib.v1.BeginTransactionRequest begin_transaction_request = 3; + */ + public com.google.cloud.spannerlib.v1.BeginTransactionRequest.Builder getBeginTransactionRequestBuilder() { + return internalGetBeginTransactionRequestFieldBuilder().getBuilder(); + } + /** + * .google.spannerlib.v1.BeginTransactionRequest begin_transaction_request = 3; + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.BeginTransactionRequestOrBuilder getBeginTransactionRequestOrBuilder() { + if ((requestCase_ == 3) && (beginTransactionRequestBuilder_ != null)) { + return beginTransactionRequestBuilder_.getMessageOrBuilder(); + } else { + if (requestCase_ == 3) { + return (com.google.cloud.spannerlib.v1.BeginTransactionRequest) request_; + } + return com.google.cloud.spannerlib.v1.BeginTransactionRequest.getDefaultInstance(); + } + } + /** + * .google.spannerlib.v1.BeginTransactionRequest begin_transaction_request = 3; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.BeginTransactionRequest, com.google.cloud.spannerlib.v1.BeginTransactionRequest.Builder, com.google.cloud.spannerlib.v1.BeginTransactionRequestOrBuilder> + internalGetBeginTransactionRequestFieldBuilder() { + if (beginTransactionRequestBuilder_ == null) { + if (!(requestCase_ == 3)) { + request_ = com.google.cloud.spannerlib.v1.BeginTransactionRequest.getDefaultInstance(); + } + beginTransactionRequestBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.BeginTransactionRequest, com.google.cloud.spannerlib.v1.BeginTransactionRequest.Builder, com.google.cloud.spannerlib.v1.BeginTransactionRequestOrBuilder>( + (com.google.cloud.spannerlib.v1.BeginTransactionRequest) request_, + getParentForChildren(), + isClean()); + request_ = null; + } + requestCase_ = 3; + onChanged(); + return beginTransactionRequestBuilder_; + } + + private com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.Connection, com.google.cloud.spannerlib.v1.Connection.Builder, com.google.cloud.spannerlib.v1.ConnectionOrBuilder> commitRequestBuilder_; + /** + * .google.spannerlib.v1.Connection commit_request = 4; + * @return Whether the commitRequest field is set. + */ + @java.lang.Override + public boolean hasCommitRequest() { + return requestCase_ == 4; + } + /** + * .google.spannerlib.v1.Connection commit_request = 4; + * @return The commitRequest. + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.Connection getCommitRequest() { + if (commitRequestBuilder_ == null) { + if (requestCase_ == 4) { + return (com.google.cloud.spannerlib.v1.Connection) request_; + } + return com.google.cloud.spannerlib.v1.Connection.getDefaultInstance(); + } else { + if (requestCase_ == 4) { + return commitRequestBuilder_.getMessage(); + } + return com.google.cloud.spannerlib.v1.Connection.getDefaultInstance(); + } + } + /** + * .google.spannerlib.v1.Connection commit_request = 4; + */ + public Builder setCommitRequest(com.google.cloud.spannerlib.v1.Connection value) { + if (commitRequestBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + request_ = value; + onChanged(); + } else { + commitRequestBuilder_.setMessage(value); + } + requestCase_ = 4; + return this; + } + /** + * .google.spannerlib.v1.Connection commit_request = 4; + */ + public Builder setCommitRequest( + com.google.cloud.spannerlib.v1.Connection.Builder builderForValue) { + if (commitRequestBuilder_ == null) { + request_ = builderForValue.build(); + onChanged(); + } else { + commitRequestBuilder_.setMessage(builderForValue.build()); + } + requestCase_ = 4; + return this; + } + /** + * .google.spannerlib.v1.Connection commit_request = 4; + */ + public Builder mergeCommitRequest(com.google.cloud.spannerlib.v1.Connection value) { + if (commitRequestBuilder_ == null) { + if (requestCase_ == 4 && + request_ != com.google.cloud.spannerlib.v1.Connection.getDefaultInstance()) { + request_ = com.google.cloud.spannerlib.v1.Connection.newBuilder((com.google.cloud.spannerlib.v1.Connection) request_) + .mergeFrom(value).buildPartial(); + } else { + request_ = value; + } + onChanged(); + } else { + if (requestCase_ == 4) { + commitRequestBuilder_.mergeFrom(value); + } else { + commitRequestBuilder_.setMessage(value); + } + } + requestCase_ = 4; + return this; + } + /** + * .google.spannerlib.v1.Connection commit_request = 4; + */ + public Builder clearCommitRequest() { + if (commitRequestBuilder_ == null) { + if (requestCase_ == 4) { + requestCase_ = 0; + request_ = null; + onChanged(); + } + } else { + if (requestCase_ == 4) { + requestCase_ = 0; + request_ = null; + } + commitRequestBuilder_.clear(); + } + return this; + } + /** + * .google.spannerlib.v1.Connection commit_request = 4; + */ + public com.google.cloud.spannerlib.v1.Connection.Builder getCommitRequestBuilder() { + return internalGetCommitRequestFieldBuilder().getBuilder(); + } + /** + * .google.spannerlib.v1.Connection commit_request = 4; + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.ConnectionOrBuilder getCommitRequestOrBuilder() { + if ((requestCase_ == 4) && (commitRequestBuilder_ != null)) { + return commitRequestBuilder_.getMessageOrBuilder(); + } else { + if (requestCase_ == 4) { + return (com.google.cloud.spannerlib.v1.Connection) request_; + } + return com.google.cloud.spannerlib.v1.Connection.getDefaultInstance(); + } + } + /** + * .google.spannerlib.v1.Connection commit_request = 4; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.Connection, com.google.cloud.spannerlib.v1.Connection.Builder, com.google.cloud.spannerlib.v1.ConnectionOrBuilder> + internalGetCommitRequestFieldBuilder() { + if (commitRequestBuilder_ == null) { + if (!(requestCase_ == 4)) { + request_ = com.google.cloud.spannerlib.v1.Connection.getDefaultInstance(); + } + commitRequestBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.Connection, com.google.cloud.spannerlib.v1.Connection.Builder, com.google.cloud.spannerlib.v1.ConnectionOrBuilder>( + (com.google.cloud.spannerlib.v1.Connection) request_, + getParentForChildren(), + isClean()); + request_ = null; + } + requestCase_ = 4; + onChanged(); + return commitRequestBuilder_; + } + + private com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.Connection, com.google.cloud.spannerlib.v1.Connection.Builder, com.google.cloud.spannerlib.v1.ConnectionOrBuilder> rollbackRequestBuilder_; + /** + * .google.spannerlib.v1.Connection rollback_request = 5; + * @return Whether the rollbackRequest field is set. + */ + @java.lang.Override + public boolean hasRollbackRequest() { + return requestCase_ == 5; + } + /** + * .google.spannerlib.v1.Connection rollback_request = 5; + * @return The rollbackRequest. + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.Connection getRollbackRequest() { + if (rollbackRequestBuilder_ == null) { + if (requestCase_ == 5) { + return (com.google.cloud.spannerlib.v1.Connection) request_; + } + return com.google.cloud.spannerlib.v1.Connection.getDefaultInstance(); + } else { + if (requestCase_ == 5) { + return rollbackRequestBuilder_.getMessage(); + } + return com.google.cloud.spannerlib.v1.Connection.getDefaultInstance(); + } + } + /** + * .google.spannerlib.v1.Connection rollback_request = 5; + */ + public Builder setRollbackRequest(com.google.cloud.spannerlib.v1.Connection value) { + if (rollbackRequestBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + request_ = value; + onChanged(); + } else { + rollbackRequestBuilder_.setMessage(value); + } + requestCase_ = 5; + return this; + } + /** + * .google.spannerlib.v1.Connection rollback_request = 5; + */ + public Builder setRollbackRequest( + com.google.cloud.spannerlib.v1.Connection.Builder builderForValue) { + if (rollbackRequestBuilder_ == null) { + request_ = builderForValue.build(); + onChanged(); + } else { + rollbackRequestBuilder_.setMessage(builderForValue.build()); + } + requestCase_ = 5; + return this; + } + /** + * .google.spannerlib.v1.Connection rollback_request = 5; + */ + public Builder mergeRollbackRequest(com.google.cloud.spannerlib.v1.Connection value) { + if (rollbackRequestBuilder_ == null) { + if (requestCase_ == 5 && + request_ != com.google.cloud.spannerlib.v1.Connection.getDefaultInstance()) { + request_ = com.google.cloud.spannerlib.v1.Connection.newBuilder((com.google.cloud.spannerlib.v1.Connection) request_) + .mergeFrom(value).buildPartial(); + } else { + request_ = value; + } + onChanged(); + } else { + if (requestCase_ == 5) { + rollbackRequestBuilder_.mergeFrom(value); + } else { + rollbackRequestBuilder_.setMessage(value); + } + } + requestCase_ = 5; + return this; + } + /** + * .google.spannerlib.v1.Connection rollback_request = 5; + */ + public Builder clearRollbackRequest() { + if (rollbackRequestBuilder_ == null) { + if (requestCase_ == 5) { + requestCase_ = 0; + request_ = null; + onChanged(); + } + } else { + if (requestCase_ == 5) { + requestCase_ = 0; + request_ = null; + } + rollbackRequestBuilder_.clear(); + } + return this; + } + /** + * .google.spannerlib.v1.Connection rollback_request = 5; + */ + public com.google.cloud.spannerlib.v1.Connection.Builder getRollbackRequestBuilder() { + return internalGetRollbackRequestFieldBuilder().getBuilder(); + } + /** + * .google.spannerlib.v1.Connection rollback_request = 5; + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.ConnectionOrBuilder getRollbackRequestOrBuilder() { + if ((requestCase_ == 5) && (rollbackRequestBuilder_ != null)) { + return rollbackRequestBuilder_.getMessageOrBuilder(); + } else { + if (requestCase_ == 5) { + return (com.google.cloud.spannerlib.v1.Connection) request_; + } + return com.google.cloud.spannerlib.v1.Connection.getDefaultInstance(); + } + } + /** + * .google.spannerlib.v1.Connection rollback_request = 5; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.Connection, com.google.cloud.spannerlib.v1.Connection.Builder, com.google.cloud.spannerlib.v1.ConnectionOrBuilder> + internalGetRollbackRequestFieldBuilder() { + if (rollbackRequestBuilder_ == null) { + if (!(requestCase_ == 5)) { + request_ = com.google.cloud.spannerlib.v1.Connection.getDefaultInstance(); + } + rollbackRequestBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.Connection, com.google.cloud.spannerlib.v1.Connection.Builder, com.google.cloud.spannerlib.v1.ConnectionOrBuilder>( + (com.google.cloud.spannerlib.v1.Connection) request_, + getParentForChildren(), + isClean()); + request_ = null; + } + requestCase_ = 5; + onChanged(); + return rollbackRequestBuilder_; + } + + private com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.WriteMutationsRequest, com.google.cloud.spannerlib.v1.WriteMutationsRequest.Builder, com.google.cloud.spannerlib.v1.WriteMutationsRequestOrBuilder> writeMutationsRequestBuilder_; + /** + * .google.spannerlib.v1.WriteMutationsRequest write_mutations_request = 6; + * @return Whether the writeMutationsRequest field is set. + */ + @java.lang.Override + public boolean hasWriteMutationsRequest() { + return requestCase_ == 6; + } + /** + * .google.spannerlib.v1.WriteMutationsRequest write_mutations_request = 6; + * @return The writeMutationsRequest. + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.WriteMutationsRequest getWriteMutationsRequest() { + if (writeMutationsRequestBuilder_ == null) { + if (requestCase_ == 6) { + return (com.google.cloud.spannerlib.v1.WriteMutationsRequest) request_; + } + return com.google.cloud.spannerlib.v1.WriteMutationsRequest.getDefaultInstance(); + } else { + if (requestCase_ == 6) { + return writeMutationsRequestBuilder_.getMessage(); + } + return com.google.cloud.spannerlib.v1.WriteMutationsRequest.getDefaultInstance(); + } + } + /** + * .google.spannerlib.v1.WriteMutationsRequest write_mutations_request = 6; + */ + public Builder setWriteMutationsRequest(com.google.cloud.spannerlib.v1.WriteMutationsRequest value) { + if (writeMutationsRequestBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + request_ = value; + onChanged(); + } else { + writeMutationsRequestBuilder_.setMessage(value); + } + requestCase_ = 6; + return this; + } + /** + * .google.spannerlib.v1.WriteMutationsRequest write_mutations_request = 6; + */ + public Builder setWriteMutationsRequest( + com.google.cloud.spannerlib.v1.WriteMutationsRequest.Builder builderForValue) { + if (writeMutationsRequestBuilder_ == null) { + request_ = builderForValue.build(); + onChanged(); + } else { + writeMutationsRequestBuilder_.setMessage(builderForValue.build()); + } + requestCase_ = 6; + return this; + } + /** + * .google.spannerlib.v1.WriteMutationsRequest write_mutations_request = 6; + */ + public Builder mergeWriteMutationsRequest(com.google.cloud.spannerlib.v1.WriteMutationsRequest value) { + if (writeMutationsRequestBuilder_ == null) { + if (requestCase_ == 6 && + request_ != com.google.cloud.spannerlib.v1.WriteMutationsRequest.getDefaultInstance()) { + request_ = com.google.cloud.spannerlib.v1.WriteMutationsRequest.newBuilder((com.google.cloud.spannerlib.v1.WriteMutationsRequest) request_) + .mergeFrom(value).buildPartial(); + } else { + request_ = value; + } + onChanged(); + } else { + if (requestCase_ == 6) { + writeMutationsRequestBuilder_.mergeFrom(value); + } else { + writeMutationsRequestBuilder_.setMessage(value); + } + } + requestCase_ = 6; + return this; + } + /** + * .google.spannerlib.v1.WriteMutationsRequest write_mutations_request = 6; + */ + public Builder clearWriteMutationsRequest() { + if (writeMutationsRequestBuilder_ == null) { + if (requestCase_ == 6) { + requestCase_ = 0; + request_ = null; + onChanged(); + } + } else { + if (requestCase_ == 6) { + requestCase_ = 0; + request_ = null; + } + writeMutationsRequestBuilder_.clear(); + } + return this; + } + /** + * .google.spannerlib.v1.WriteMutationsRequest write_mutations_request = 6; + */ + public com.google.cloud.spannerlib.v1.WriteMutationsRequest.Builder getWriteMutationsRequestBuilder() { + return internalGetWriteMutationsRequestFieldBuilder().getBuilder(); + } + /** + * .google.spannerlib.v1.WriteMutationsRequest write_mutations_request = 6; + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.WriteMutationsRequestOrBuilder getWriteMutationsRequestOrBuilder() { + if ((requestCase_ == 6) && (writeMutationsRequestBuilder_ != null)) { + return writeMutationsRequestBuilder_.getMessageOrBuilder(); + } else { + if (requestCase_ == 6) { + return (com.google.cloud.spannerlib.v1.WriteMutationsRequest) request_; + } + return com.google.cloud.spannerlib.v1.WriteMutationsRequest.getDefaultInstance(); + } + } + /** + * .google.spannerlib.v1.WriteMutationsRequest write_mutations_request = 6; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.WriteMutationsRequest, com.google.cloud.spannerlib.v1.WriteMutationsRequest.Builder, com.google.cloud.spannerlib.v1.WriteMutationsRequestOrBuilder> + internalGetWriteMutationsRequestFieldBuilder() { + if (writeMutationsRequestBuilder_ == null) { + if (!(requestCase_ == 6)) { + request_ = com.google.cloud.spannerlib.v1.WriteMutationsRequest.getDefaultInstance(); + } + writeMutationsRequestBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.WriteMutationsRequest, com.google.cloud.spannerlib.v1.WriteMutationsRequest.Builder, com.google.cloud.spannerlib.v1.WriteMutationsRequestOrBuilder>( + (com.google.cloud.spannerlib.v1.WriteMutationsRequest) request_, + getParentForChildren(), + isClean()); + request_ = null; + } + requestCase_ = 6; + onChanged(); + return writeMutationsRequestBuilder_; + } + // @@protoc_insertion_point(builder_scope:google.spannerlib.v1.ConnectionStreamRequest) } diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ConnectionStreamRequestOrBuilder.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ConnectionStreamRequestOrBuilder.java index cc414685..dd36c31f 100644 --- a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ConnectionStreamRequestOrBuilder.java +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ConnectionStreamRequestOrBuilder.java @@ -25,5 +25,80 @@ public interface ConnectionStreamRequestOrBuilder extends */ com.google.cloud.spannerlib.v1.ExecuteRequestOrBuilder getExecuteRequestOrBuilder(); + /** + * .google.spannerlib.v1.ExecuteBatchRequest execute_batch_request = 2; + * @return Whether the executeBatchRequest field is set. + */ + boolean hasExecuteBatchRequest(); + /** + * .google.spannerlib.v1.ExecuteBatchRequest execute_batch_request = 2; + * @return The executeBatchRequest. + */ + com.google.cloud.spannerlib.v1.ExecuteBatchRequest getExecuteBatchRequest(); + /** + * .google.spannerlib.v1.ExecuteBatchRequest execute_batch_request = 2; + */ + com.google.cloud.spannerlib.v1.ExecuteBatchRequestOrBuilder getExecuteBatchRequestOrBuilder(); + + /** + * .google.spannerlib.v1.BeginTransactionRequest begin_transaction_request = 3; + * @return Whether the beginTransactionRequest field is set. + */ + boolean hasBeginTransactionRequest(); + /** + * .google.spannerlib.v1.BeginTransactionRequest begin_transaction_request = 3; + * @return The beginTransactionRequest. + */ + com.google.cloud.spannerlib.v1.BeginTransactionRequest getBeginTransactionRequest(); + /** + * .google.spannerlib.v1.BeginTransactionRequest begin_transaction_request = 3; + */ + com.google.cloud.spannerlib.v1.BeginTransactionRequestOrBuilder getBeginTransactionRequestOrBuilder(); + + /** + * .google.spannerlib.v1.Connection commit_request = 4; + * @return Whether the commitRequest field is set. + */ + boolean hasCommitRequest(); + /** + * .google.spannerlib.v1.Connection commit_request = 4; + * @return The commitRequest. + */ + com.google.cloud.spannerlib.v1.Connection getCommitRequest(); + /** + * .google.spannerlib.v1.Connection commit_request = 4; + */ + com.google.cloud.spannerlib.v1.ConnectionOrBuilder getCommitRequestOrBuilder(); + + /** + * .google.spannerlib.v1.Connection rollback_request = 5; + * @return Whether the rollbackRequest field is set. + */ + boolean hasRollbackRequest(); + /** + * .google.spannerlib.v1.Connection rollback_request = 5; + * @return The rollbackRequest. + */ + com.google.cloud.spannerlib.v1.Connection getRollbackRequest(); + /** + * .google.spannerlib.v1.Connection rollback_request = 5; + */ + com.google.cloud.spannerlib.v1.ConnectionOrBuilder getRollbackRequestOrBuilder(); + + /** + * .google.spannerlib.v1.WriteMutationsRequest write_mutations_request = 6; + * @return Whether the writeMutationsRequest field is set. + */ + boolean hasWriteMutationsRequest(); + /** + * .google.spannerlib.v1.WriteMutationsRequest write_mutations_request = 6; + * @return The writeMutationsRequest. + */ + com.google.cloud.spannerlib.v1.WriteMutationsRequest getWriteMutationsRequest(); + /** + * .google.spannerlib.v1.WriteMutationsRequest write_mutations_request = 6; + */ + com.google.cloud.spannerlib.v1.WriteMutationsRequestOrBuilder getWriteMutationsRequestOrBuilder(); + com.google.cloud.spannerlib.v1.ConnectionStreamRequest.RequestCase getRequestCase(); } diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ConnectionStreamResponse.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ConnectionStreamResponse.java index b12f8f27..a0bfea2d 100644 --- a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ConnectionStreamResponse.java +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ConnectionStreamResponse.java @@ -43,13 +43,19 @@ private ConnectionStreamResponse() { com.google.cloud.spannerlib.v1.ConnectionStreamResponse.class, com.google.cloud.spannerlib.v1.ConnectionStreamResponse.Builder.class); } + private int bitField0_; private int responseCase_ = 0; @SuppressWarnings("serial") private java.lang.Object response_; public enum ResponseCase implements com.google.protobuf.Internal.EnumLite, com.google.protobuf.AbstractMessage.InternalOneOfEnum { - ROW(1), + EXECUTE_RESPONSE(2), + EXECUTE_BATCH_RESPONSE(3), + BEGIN_TRANSACTION_RESPONSE(4), + COMMIT_RESPONSE(5), + ROLLBACK_RESPONSE(6), + WRITE_MUTATIONS_RESPONSE(7), RESPONSE_NOT_SET(0); private final int value; private ResponseCase(int value) { @@ -67,7 +73,12 @@ public static ResponseCase valueOf(int value) { public static ResponseCase forNumber(int value) { switch (value) { - case 1: return ROW; + case 2: return EXECUTE_RESPONSE; + case 3: return EXECUTE_BATCH_RESPONSE; + case 4: return BEGIN_TRANSACTION_RESPONSE; + case 5: return COMMIT_RESPONSE; + case 6: return ROLLBACK_RESPONSE; + case 7: return WRITE_MUTATIONS_RESPONSE; case 0: return RESPONSE_NOT_SET; default: return null; } @@ -83,35 +94,216 @@ public int getNumber() { responseCase_); } - public static final int ROW_FIELD_NUMBER = 1; + public static final int STATUS_FIELD_NUMBER = 1; + private com.google.rpc.Status status_; /** - * .google.spanner.v1.PartialResultSet row = 1; - * @return Whether the row field is set. + * .google.rpc.Status status = 1; + * @return Whether the status field is set. */ @java.lang.Override - public boolean hasRow() { - return responseCase_ == 1; + public boolean hasStatus() { + return ((bitField0_ & 0x00000001) != 0); } /** - * .google.spanner.v1.PartialResultSet row = 1; - * @return The row. + * .google.rpc.Status status = 1; + * @return The status. */ @java.lang.Override - public com.google.spanner.v1.PartialResultSet getRow() { - if (responseCase_ == 1) { - return (com.google.spanner.v1.PartialResultSet) response_; + public com.google.rpc.Status getStatus() { + return status_ == null ? com.google.rpc.Status.getDefaultInstance() : status_; + } + /** + * .google.rpc.Status status = 1; + */ + @java.lang.Override + public com.google.rpc.StatusOrBuilder getStatusOrBuilder() { + return status_ == null ? com.google.rpc.Status.getDefaultInstance() : status_; + } + + public static final int EXECUTE_RESPONSE_FIELD_NUMBER = 2; + /** + * .google.spannerlib.v1.ExecuteResponse execute_response = 2; + * @return Whether the executeResponse field is set. + */ + @java.lang.Override + public boolean hasExecuteResponse() { + return responseCase_ == 2; + } + /** + * .google.spannerlib.v1.ExecuteResponse execute_response = 2; + * @return The executeResponse. + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.ExecuteResponse getExecuteResponse() { + if (responseCase_ == 2) { + return (com.google.cloud.spannerlib.v1.ExecuteResponse) response_; + } + return com.google.cloud.spannerlib.v1.ExecuteResponse.getDefaultInstance(); + } + /** + * .google.spannerlib.v1.ExecuteResponse execute_response = 2; + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.ExecuteResponseOrBuilder getExecuteResponseOrBuilder() { + if (responseCase_ == 2) { + return (com.google.cloud.spannerlib.v1.ExecuteResponse) response_; + } + return com.google.cloud.spannerlib.v1.ExecuteResponse.getDefaultInstance(); + } + + public static final int EXECUTE_BATCH_RESPONSE_FIELD_NUMBER = 3; + /** + * .google.spanner.v1.ExecuteBatchDmlResponse execute_batch_response = 3; + * @return Whether the executeBatchResponse field is set. + */ + @java.lang.Override + public boolean hasExecuteBatchResponse() { + return responseCase_ == 3; + } + /** + * .google.spanner.v1.ExecuteBatchDmlResponse execute_batch_response = 3; + * @return The executeBatchResponse. + */ + @java.lang.Override + public com.google.spanner.v1.ExecuteBatchDmlResponse getExecuteBatchResponse() { + if (responseCase_ == 3) { + return (com.google.spanner.v1.ExecuteBatchDmlResponse) response_; + } + return com.google.spanner.v1.ExecuteBatchDmlResponse.getDefaultInstance(); + } + /** + * .google.spanner.v1.ExecuteBatchDmlResponse execute_batch_response = 3; + */ + @java.lang.Override + public com.google.spanner.v1.ExecuteBatchDmlResponseOrBuilder getExecuteBatchResponseOrBuilder() { + if (responseCase_ == 3) { + return (com.google.spanner.v1.ExecuteBatchDmlResponse) response_; + } + return com.google.spanner.v1.ExecuteBatchDmlResponse.getDefaultInstance(); + } + + public static final int BEGIN_TRANSACTION_RESPONSE_FIELD_NUMBER = 4; + /** + * .google.protobuf.Empty begin_transaction_response = 4; + * @return Whether the beginTransactionResponse field is set. + */ + @java.lang.Override + public boolean hasBeginTransactionResponse() { + return responseCase_ == 4; + } + /** + * .google.protobuf.Empty begin_transaction_response = 4; + * @return The beginTransactionResponse. + */ + @java.lang.Override + public com.google.protobuf.Empty getBeginTransactionResponse() { + if (responseCase_ == 4) { + return (com.google.protobuf.Empty) response_; + } + return com.google.protobuf.Empty.getDefaultInstance(); + } + /** + * .google.protobuf.Empty begin_transaction_response = 4; + */ + @java.lang.Override + public com.google.protobuf.EmptyOrBuilder getBeginTransactionResponseOrBuilder() { + if (responseCase_ == 4) { + return (com.google.protobuf.Empty) response_; + } + return com.google.protobuf.Empty.getDefaultInstance(); + } + + public static final int COMMIT_RESPONSE_FIELD_NUMBER = 5; + /** + * .google.spanner.v1.CommitResponse commit_response = 5; + * @return Whether the commitResponse field is set. + */ + @java.lang.Override + public boolean hasCommitResponse() { + return responseCase_ == 5; + } + /** + * .google.spanner.v1.CommitResponse commit_response = 5; + * @return The commitResponse. + */ + @java.lang.Override + public com.google.spanner.v1.CommitResponse getCommitResponse() { + if (responseCase_ == 5) { + return (com.google.spanner.v1.CommitResponse) response_; + } + return com.google.spanner.v1.CommitResponse.getDefaultInstance(); + } + /** + * .google.spanner.v1.CommitResponse commit_response = 5; + */ + @java.lang.Override + public com.google.spanner.v1.CommitResponseOrBuilder getCommitResponseOrBuilder() { + if (responseCase_ == 5) { + return (com.google.spanner.v1.CommitResponse) response_; + } + return com.google.spanner.v1.CommitResponse.getDefaultInstance(); + } + + public static final int ROLLBACK_RESPONSE_FIELD_NUMBER = 6; + /** + * .google.protobuf.Empty rollback_response = 6; + * @return Whether the rollbackResponse field is set. + */ + @java.lang.Override + public boolean hasRollbackResponse() { + return responseCase_ == 6; + } + /** + * .google.protobuf.Empty rollback_response = 6; + * @return The rollbackResponse. + */ + @java.lang.Override + public com.google.protobuf.Empty getRollbackResponse() { + if (responseCase_ == 6) { + return (com.google.protobuf.Empty) response_; + } + return com.google.protobuf.Empty.getDefaultInstance(); + } + /** + * .google.protobuf.Empty rollback_response = 6; + */ + @java.lang.Override + public com.google.protobuf.EmptyOrBuilder getRollbackResponseOrBuilder() { + if (responseCase_ == 6) { + return (com.google.protobuf.Empty) response_; + } + return com.google.protobuf.Empty.getDefaultInstance(); + } + + public static final int WRITE_MUTATIONS_RESPONSE_FIELD_NUMBER = 7; + /** + * .google.spanner.v1.CommitResponse write_mutations_response = 7; + * @return Whether the writeMutationsResponse field is set. + */ + @java.lang.Override + public boolean hasWriteMutationsResponse() { + return responseCase_ == 7; + } + /** + * .google.spanner.v1.CommitResponse write_mutations_response = 7; + * @return The writeMutationsResponse. + */ + @java.lang.Override + public com.google.spanner.v1.CommitResponse getWriteMutationsResponse() { + if (responseCase_ == 7) { + return (com.google.spanner.v1.CommitResponse) response_; } - return com.google.spanner.v1.PartialResultSet.getDefaultInstance(); + return com.google.spanner.v1.CommitResponse.getDefaultInstance(); } /** - * .google.spanner.v1.PartialResultSet row = 1; + * .google.spanner.v1.CommitResponse write_mutations_response = 7; */ @java.lang.Override - public com.google.spanner.v1.PartialResultSetOrBuilder getRowOrBuilder() { - if (responseCase_ == 1) { - return (com.google.spanner.v1.PartialResultSet) response_; + public com.google.spanner.v1.CommitResponseOrBuilder getWriteMutationsResponseOrBuilder() { + if (responseCase_ == 7) { + return (com.google.spanner.v1.CommitResponse) response_; } - return com.google.spanner.v1.PartialResultSet.getDefaultInstance(); + return com.google.spanner.v1.CommitResponse.getDefaultInstance(); } private byte memoizedIsInitialized = -1; @@ -128,8 +320,26 @@ public final boolean isInitialized() { @java.lang.Override public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io.IOException { - if (responseCase_ == 1) { - output.writeMessage(1, (com.google.spanner.v1.PartialResultSet) response_); + if (((bitField0_ & 0x00000001) != 0)) { + output.writeMessage(1, getStatus()); + } + if (responseCase_ == 2) { + output.writeMessage(2, (com.google.cloud.spannerlib.v1.ExecuteResponse) response_); + } + if (responseCase_ == 3) { + output.writeMessage(3, (com.google.spanner.v1.ExecuteBatchDmlResponse) response_); + } + if (responseCase_ == 4) { + output.writeMessage(4, (com.google.protobuf.Empty) response_); + } + if (responseCase_ == 5) { + output.writeMessage(5, (com.google.spanner.v1.CommitResponse) response_); + } + if (responseCase_ == 6) { + output.writeMessage(6, (com.google.protobuf.Empty) response_); + } + if (responseCase_ == 7) { + output.writeMessage(7, (com.google.spanner.v1.CommitResponse) response_); } getUnknownFields().writeTo(output); } @@ -140,9 +350,33 @@ public int getSerializedSize() { if (size != -1) return size; size = 0; - if (responseCase_ == 1) { + if (((bitField0_ & 0x00000001) != 0)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(1, getStatus()); + } + if (responseCase_ == 2) { size += com.google.protobuf.CodedOutputStream - .computeMessageSize(1, (com.google.spanner.v1.PartialResultSet) response_); + .computeMessageSize(2, (com.google.cloud.spannerlib.v1.ExecuteResponse) response_); + } + if (responseCase_ == 3) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(3, (com.google.spanner.v1.ExecuteBatchDmlResponse) response_); + } + if (responseCase_ == 4) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(4, (com.google.protobuf.Empty) response_); + } + if (responseCase_ == 5) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(5, (com.google.spanner.v1.CommitResponse) response_); + } + if (responseCase_ == 6) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(6, (com.google.protobuf.Empty) response_); + } + if (responseCase_ == 7) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(7, (com.google.spanner.v1.CommitResponse) response_); } size += getUnknownFields().getSerializedSize(); memoizedSize = size; @@ -159,11 +393,36 @@ public boolean equals(final java.lang.Object obj) { } com.google.cloud.spannerlib.v1.ConnectionStreamResponse other = (com.google.cloud.spannerlib.v1.ConnectionStreamResponse) obj; + if (hasStatus() != other.hasStatus()) return false; + if (hasStatus()) { + if (!getStatus() + .equals(other.getStatus())) return false; + } if (!getResponseCase().equals(other.getResponseCase())) return false; switch (responseCase_) { - case 1: - if (!getRow() - .equals(other.getRow())) return false; + case 2: + if (!getExecuteResponse() + .equals(other.getExecuteResponse())) return false; + break; + case 3: + if (!getExecuteBatchResponse() + .equals(other.getExecuteBatchResponse())) return false; + break; + case 4: + if (!getBeginTransactionResponse() + .equals(other.getBeginTransactionResponse())) return false; + break; + case 5: + if (!getCommitResponse() + .equals(other.getCommitResponse())) return false; + break; + case 6: + if (!getRollbackResponse() + .equals(other.getRollbackResponse())) return false; + break; + case 7: + if (!getWriteMutationsResponse() + .equals(other.getWriteMutationsResponse())) return false; break; case 0: default: @@ -179,10 +438,34 @@ public int hashCode() { } int hash = 41; hash = (19 * hash) + getDescriptor().hashCode(); + if (hasStatus()) { + hash = (37 * hash) + STATUS_FIELD_NUMBER; + hash = (53 * hash) + getStatus().hashCode(); + } switch (responseCase_) { - case 1: - hash = (37 * hash) + ROW_FIELD_NUMBER; - hash = (53 * hash) + getRow().hashCode(); + case 2: + hash = (37 * hash) + EXECUTE_RESPONSE_FIELD_NUMBER; + hash = (53 * hash) + getExecuteResponse().hashCode(); + break; + case 3: + hash = (37 * hash) + EXECUTE_BATCH_RESPONSE_FIELD_NUMBER; + hash = (53 * hash) + getExecuteBatchResponse().hashCode(); + break; + case 4: + hash = (37 * hash) + BEGIN_TRANSACTION_RESPONSE_FIELD_NUMBER; + hash = (53 * hash) + getBeginTransactionResponse().hashCode(); + break; + case 5: + hash = (37 * hash) + COMMIT_RESPONSE_FIELD_NUMBER; + hash = (53 * hash) + getCommitResponse().hashCode(); + break; + case 6: + hash = (37 * hash) + ROLLBACK_RESPONSE_FIELD_NUMBER; + hash = (53 * hash) + getRollbackResponse().hashCode(); + break; + case 7: + hash = (37 * hash) + WRITE_MUTATIONS_RESPONSE_FIELD_NUMBER; + hash = (53 * hash) + getWriteMutationsResponse().hashCode(); break; case 0: default: @@ -306,20 +589,46 @@ public static final class Builder extends // Construct using com.google.cloud.spannerlib.v1.ConnectionStreamResponse.newBuilder() private Builder() { - + maybeForceBuilderInitialization(); } private Builder( com.google.protobuf.GeneratedMessage.BuilderParent parent) { super(parent); - + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage + .alwaysUseFieldBuilders) { + internalGetStatusFieldBuilder(); + } } @java.lang.Override public Builder clear() { super.clear(); bitField0_ = 0; - if (rowBuilder_ != null) { - rowBuilder_.clear(); + status_ = null; + if (statusBuilder_ != null) { + statusBuilder_.dispose(); + statusBuilder_ = null; + } + if (executeResponseBuilder_ != null) { + executeResponseBuilder_.clear(); + } + if (executeBatchResponseBuilder_ != null) { + executeBatchResponseBuilder_.clear(); + } + if (beginTransactionResponseBuilder_ != null) { + beginTransactionResponseBuilder_.clear(); + } + if (commitResponseBuilder_ != null) { + commitResponseBuilder_.clear(); + } + if (rollbackResponseBuilder_ != null) { + rollbackResponseBuilder_.clear(); + } + if (writeMutationsResponseBuilder_ != null) { + writeMutationsResponseBuilder_.clear(); } responseCase_ = 0; response_ = null; @@ -357,14 +666,42 @@ public com.google.cloud.spannerlib.v1.ConnectionStreamResponse buildPartial() { private void buildPartial0(com.google.cloud.spannerlib.v1.ConnectionStreamResponse result) { int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) != 0)) { + result.status_ = statusBuilder_ == null + ? status_ + : statusBuilder_.build(); + to_bitField0_ |= 0x00000001; + } + result.bitField0_ |= to_bitField0_; } private void buildPartialOneofs(com.google.cloud.spannerlib.v1.ConnectionStreamResponse result) { result.responseCase_ = responseCase_; result.response_ = this.response_; - if (responseCase_ == 1 && - rowBuilder_ != null) { - result.response_ = rowBuilder_.build(); + if (responseCase_ == 2 && + executeResponseBuilder_ != null) { + result.response_ = executeResponseBuilder_.build(); + } + if (responseCase_ == 3 && + executeBatchResponseBuilder_ != null) { + result.response_ = executeBatchResponseBuilder_.build(); + } + if (responseCase_ == 4 && + beginTransactionResponseBuilder_ != null) { + result.response_ = beginTransactionResponseBuilder_.build(); + } + if (responseCase_ == 5 && + commitResponseBuilder_ != null) { + result.response_ = commitResponseBuilder_.build(); + } + if (responseCase_ == 6 && + rollbackResponseBuilder_ != null) { + result.response_ = rollbackResponseBuilder_.build(); + } + if (responseCase_ == 7 && + writeMutationsResponseBuilder_ != null) { + result.response_ = writeMutationsResponseBuilder_.build(); } } @@ -380,9 +717,32 @@ public Builder mergeFrom(com.google.protobuf.Message other) { public Builder mergeFrom(com.google.cloud.spannerlib.v1.ConnectionStreamResponse other) { if (other == com.google.cloud.spannerlib.v1.ConnectionStreamResponse.getDefaultInstance()) return this; + if (other.hasStatus()) { + mergeStatus(other.getStatus()); + } switch (other.getResponseCase()) { - case ROW: { - mergeRow(other.getRow()); + case EXECUTE_RESPONSE: { + mergeExecuteResponse(other.getExecuteResponse()); + break; + } + case EXECUTE_BATCH_RESPONSE: { + mergeExecuteBatchResponse(other.getExecuteBatchResponse()); + break; + } + case BEGIN_TRANSACTION_RESPONSE: { + mergeBeginTransactionResponse(other.getBeginTransactionResponse()); + break; + } + case COMMIT_RESPONSE: { + mergeCommitResponse(other.getCommitResponse()); + break; + } + case ROLLBACK_RESPONSE: { + mergeRollbackResponse(other.getRollbackResponse()); + break; + } + case WRITE_MUTATIONS_RESPONSE: { + mergeWriteMutationsResponse(other.getWriteMutationsResponse()); break; } case RESPONSE_NOT_SET: { @@ -417,11 +777,53 @@ public Builder mergeFrom( break; case 10: { input.readMessage( - internalGetRowFieldBuilder().getBuilder(), + internalGetStatusFieldBuilder().getBuilder(), extensionRegistry); - responseCase_ = 1; + bitField0_ |= 0x00000001; break; } // case 10 + case 18: { + input.readMessage( + internalGetExecuteResponseFieldBuilder().getBuilder(), + extensionRegistry); + responseCase_ = 2; + break; + } // case 18 + case 26: { + input.readMessage( + internalGetExecuteBatchResponseFieldBuilder().getBuilder(), + extensionRegistry); + responseCase_ = 3; + break; + } // case 26 + case 34: { + input.readMessage( + internalGetBeginTransactionResponseFieldBuilder().getBuilder(), + extensionRegistry); + responseCase_ = 4; + break; + } // case 34 + case 42: { + input.readMessage( + internalGetCommitResponseFieldBuilder().getBuilder(), + extensionRegistry); + responseCase_ = 5; + break; + } // case 42 + case 50: { + input.readMessage( + internalGetRollbackResponseFieldBuilder().getBuilder(), + extensionRegistry); + responseCase_ = 6; + break; + } // case 50 + case 58: { + input.readMessage( + internalGetWriteMutationsResponseFieldBuilder().getBuilder(), + extensionRegistry); + responseCase_ = 7; + break; + } // case 58 default: { if (!super.parseUnknownField(input, extensionRegistry, tag)) { done = true; // was an endgroup tag @@ -454,146 +856,977 @@ public Builder clearResponse() { private int bitField0_; + private com.google.rpc.Status status_; + private com.google.protobuf.SingleFieldBuilder< + com.google.rpc.Status, com.google.rpc.Status.Builder, com.google.rpc.StatusOrBuilder> statusBuilder_; + /** + * .google.rpc.Status status = 1; + * @return Whether the status field is set. + */ + public boolean hasStatus() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + * .google.rpc.Status status = 1; + * @return The status. + */ + public com.google.rpc.Status getStatus() { + if (statusBuilder_ == null) { + return status_ == null ? com.google.rpc.Status.getDefaultInstance() : status_; + } else { + return statusBuilder_.getMessage(); + } + } + /** + * .google.rpc.Status status = 1; + */ + public Builder setStatus(com.google.rpc.Status value) { + if (statusBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + status_ = value; + } else { + statusBuilder_.setMessage(value); + } + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + * .google.rpc.Status status = 1; + */ + public Builder setStatus( + com.google.rpc.Status.Builder builderForValue) { + if (statusBuilder_ == null) { + status_ = builderForValue.build(); + } else { + statusBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + * .google.rpc.Status status = 1; + */ + public Builder mergeStatus(com.google.rpc.Status value) { + if (statusBuilder_ == null) { + if (((bitField0_ & 0x00000001) != 0) && + status_ != null && + status_ != com.google.rpc.Status.getDefaultInstance()) { + getStatusBuilder().mergeFrom(value); + } else { + status_ = value; + } + } else { + statusBuilder_.mergeFrom(value); + } + if (status_ != null) { + bitField0_ |= 0x00000001; + onChanged(); + } + return this; + } + /** + * .google.rpc.Status status = 1; + */ + public Builder clearStatus() { + bitField0_ = (bitField0_ & ~0x00000001); + status_ = null; + if (statusBuilder_ != null) { + statusBuilder_.dispose(); + statusBuilder_ = null; + } + onChanged(); + return this; + } + /** + * .google.rpc.Status status = 1; + */ + public com.google.rpc.Status.Builder getStatusBuilder() { + bitField0_ |= 0x00000001; + onChanged(); + return internalGetStatusFieldBuilder().getBuilder(); + } + /** + * .google.rpc.Status status = 1; + */ + public com.google.rpc.StatusOrBuilder getStatusOrBuilder() { + if (statusBuilder_ != null) { + return statusBuilder_.getMessageOrBuilder(); + } else { + return status_ == null ? + com.google.rpc.Status.getDefaultInstance() : status_; + } + } + /** + * .google.rpc.Status status = 1; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.rpc.Status, com.google.rpc.Status.Builder, com.google.rpc.StatusOrBuilder> + internalGetStatusFieldBuilder() { + if (statusBuilder_ == null) { + statusBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.rpc.Status, com.google.rpc.Status.Builder, com.google.rpc.StatusOrBuilder>( + getStatus(), + getParentForChildren(), + isClean()); + status_ = null; + } + return statusBuilder_; + } + private com.google.protobuf.SingleFieldBuilder< - com.google.spanner.v1.PartialResultSet, com.google.spanner.v1.PartialResultSet.Builder, com.google.spanner.v1.PartialResultSetOrBuilder> rowBuilder_; + com.google.cloud.spannerlib.v1.ExecuteResponse, com.google.cloud.spannerlib.v1.ExecuteResponse.Builder, com.google.cloud.spannerlib.v1.ExecuteResponseOrBuilder> executeResponseBuilder_; /** - * .google.spanner.v1.PartialResultSet row = 1; - * @return Whether the row field is set. + * .google.spannerlib.v1.ExecuteResponse execute_response = 2; + * @return Whether the executeResponse field is set. */ @java.lang.Override - public boolean hasRow() { - return responseCase_ == 1; + public boolean hasExecuteResponse() { + return responseCase_ == 2; } /** - * .google.spanner.v1.PartialResultSet row = 1; - * @return The row. + * .google.spannerlib.v1.ExecuteResponse execute_response = 2; + * @return The executeResponse. */ @java.lang.Override - public com.google.spanner.v1.PartialResultSet getRow() { - if (rowBuilder_ == null) { - if (responseCase_ == 1) { - return (com.google.spanner.v1.PartialResultSet) response_; + public com.google.cloud.spannerlib.v1.ExecuteResponse getExecuteResponse() { + if (executeResponseBuilder_ == null) { + if (responseCase_ == 2) { + return (com.google.cloud.spannerlib.v1.ExecuteResponse) response_; } - return com.google.spanner.v1.PartialResultSet.getDefaultInstance(); + return com.google.cloud.spannerlib.v1.ExecuteResponse.getDefaultInstance(); } else { - if (responseCase_ == 1) { - return rowBuilder_.getMessage(); + if (responseCase_ == 2) { + return executeResponseBuilder_.getMessage(); } - return com.google.spanner.v1.PartialResultSet.getDefaultInstance(); + return com.google.cloud.spannerlib.v1.ExecuteResponse.getDefaultInstance(); } } /** - * .google.spanner.v1.PartialResultSet row = 1; + * .google.spannerlib.v1.ExecuteResponse execute_response = 2; */ - public Builder setRow(com.google.spanner.v1.PartialResultSet value) { - if (rowBuilder_ == null) { + public Builder setExecuteResponse(com.google.cloud.spannerlib.v1.ExecuteResponse value) { + if (executeResponseBuilder_ == null) { if (value == null) { throw new NullPointerException(); } response_ = value; onChanged(); } else { - rowBuilder_.setMessage(value); + executeResponseBuilder_.setMessage(value); } - responseCase_ = 1; + responseCase_ = 2; return this; } /** - * .google.spanner.v1.PartialResultSet row = 1; + * .google.spannerlib.v1.ExecuteResponse execute_response = 2; */ - public Builder setRow( - com.google.spanner.v1.PartialResultSet.Builder builderForValue) { - if (rowBuilder_ == null) { + public Builder setExecuteResponse( + com.google.cloud.spannerlib.v1.ExecuteResponse.Builder builderForValue) { + if (executeResponseBuilder_ == null) { response_ = builderForValue.build(); onChanged(); } else { - rowBuilder_.setMessage(builderForValue.build()); + executeResponseBuilder_.setMessage(builderForValue.build()); } - responseCase_ = 1; + responseCase_ = 2; return this; } /** - * .google.spanner.v1.PartialResultSet row = 1; + * .google.spannerlib.v1.ExecuteResponse execute_response = 2; */ - public Builder mergeRow(com.google.spanner.v1.PartialResultSet value) { - if (rowBuilder_ == null) { - if (responseCase_ == 1 && - response_ != com.google.spanner.v1.PartialResultSet.getDefaultInstance()) { - response_ = com.google.spanner.v1.PartialResultSet.newBuilder((com.google.spanner.v1.PartialResultSet) response_) + public Builder mergeExecuteResponse(com.google.cloud.spannerlib.v1.ExecuteResponse value) { + if (executeResponseBuilder_ == null) { + if (responseCase_ == 2 && + response_ != com.google.cloud.spannerlib.v1.ExecuteResponse.getDefaultInstance()) { + response_ = com.google.cloud.spannerlib.v1.ExecuteResponse.newBuilder((com.google.cloud.spannerlib.v1.ExecuteResponse) response_) .mergeFrom(value).buildPartial(); } else { response_ = value; } onChanged(); } else { - if (responseCase_ == 1) { - rowBuilder_.mergeFrom(value); + if (responseCase_ == 2) { + executeResponseBuilder_.mergeFrom(value); } else { - rowBuilder_.setMessage(value); + executeResponseBuilder_.setMessage(value); } } - responseCase_ = 1; + responseCase_ = 2; return this; } /** - * .google.spanner.v1.PartialResultSet row = 1; + * .google.spannerlib.v1.ExecuteResponse execute_response = 2; */ - public Builder clearRow() { - if (rowBuilder_ == null) { - if (responseCase_ == 1) { + public Builder clearExecuteResponse() { + if (executeResponseBuilder_ == null) { + if (responseCase_ == 2) { responseCase_ = 0; response_ = null; onChanged(); } } else { - if (responseCase_ == 1) { + if (responseCase_ == 2) { responseCase_ = 0; response_ = null; } - rowBuilder_.clear(); + executeResponseBuilder_.clear(); } return this; } /** - * .google.spanner.v1.PartialResultSet row = 1; + * .google.spannerlib.v1.ExecuteResponse execute_response = 2; */ - public com.google.spanner.v1.PartialResultSet.Builder getRowBuilder() { - return internalGetRowFieldBuilder().getBuilder(); + public com.google.cloud.spannerlib.v1.ExecuteResponse.Builder getExecuteResponseBuilder() { + return internalGetExecuteResponseFieldBuilder().getBuilder(); } /** - * .google.spanner.v1.PartialResultSet row = 1; + * .google.spannerlib.v1.ExecuteResponse execute_response = 2; */ @java.lang.Override - public com.google.spanner.v1.PartialResultSetOrBuilder getRowOrBuilder() { - if ((responseCase_ == 1) && (rowBuilder_ != null)) { - return rowBuilder_.getMessageOrBuilder(); + public com.google.cloud.spannerlib.v1.ExecuteResponseOrBuilder getExecuteResponseOrBuilder() { + if ((responseCase_ == 2) && (executeResponseBuilder_ != null)) { + return executeResponseBuilder_.getMessageOrBuilder(); } else { - if (responseCase_ == 1) { - return (com.google.spanner.v1.PartialResultSet) response_; + if (responseCase_ == 2) { + return (com.google.cloud.spannerlib.v1.ExecuteResponse) response_; } - return com.google.spanner.v1.PartialResultSet.getDefaultInstance(); + return com.google.cloud.spannerlib.v1.ExecuteResponse.getDefaultInstance(); } } /** - * .google.spanner.v1.PartialResultSet row = 1; + * .google.spannerlib.v1.ExecuteResponse execute_response = 2; */ private com.google.protobuf.SingleFieldBuilder< - com.google.spanner.v1.PartialResultSet, com.google.spanner.v1.PartialResultSet.Builder, com.google.spanner.v1.PartialResultSetOrBuilder> - internalGetRowFieldBuilder() { - if (rowBuilder_ == null) { - if (!(responseCase_ == 1)) { - response_ = com.google.spanner.v1.PartialResultSet.getDefaultInstance(); - } - rowBuilder_ = new com.google.protobuf.SingleFieldBuilder< - com.google.spanner.v1.PartialResultSet, com.google.spanner.v1.PartialResultSet.Builder, com.google.spanner.v1.PartialResultSetOrBuilder>( - (com.google.spanner.v1.PartialResultSet) response_, + com.google.cloud.spannerlib.v1.ExecuteResponse, com.google.cloud.spannerlib.v1.ExecuteResponse.Builder, com.google.cloud.spannerlib.v1.ExecuteResponseOrBuilder> + internalGetExecuteResponseFieldBuilder() { + if (executeResponseBuilder_ == null) { + if (!(responseCase_ == 2)) { + response_ = com.google.cloud.spannerlib.v1.ExecuteResponse.getDefaultInstance(); + } + executeResponseBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.ExecuteResponse, com.google.cloud.spannerlib.v1.ExecuteResponse.Builder, com.google.cloud.spannerlib.v1.ExecuteResponseOrBuilder>( + (com.google.cloud.spannerlib.v1.ExecuteResponse) response_, + getParentForChildren(), + isClean()); + response_ = null; + } + responseCase_ = 2; + onChanged(); + return executeResponseBuilder_; + } + + private com.google.protobuf.SingleFieldBuilder< + com.google.spanner.v1.ExecuteBatchDmlResponse, com.google.spanner.v1.ExecuteBatchDmlResponse.Builder, com.google.spanner.v1.ExecuteBatchDmlResponseOrBuilder> executeBatchResponseBuilder_; + /** + * .google.spanner.v1.ExecuteBatchDmlResponse execute_batch_response = 3; + * @return Whether the executeBatchResponse field is set. + */ + @java.lang.Override + public boolean hasExecuteBatchResponse() { + return responseCase_ == 3; + } + /** + * .google.spanner.v1.ExecuteBatchDmlResponse execute_batch_response = 3; + * @return The executeBatchResponse. + */ + @java.lang.Override + public com.google.spanner.v1.ExecuteBatchDmlResponse getExecuteBatchResponse() { + if (executeBatchResponseBuilder_ == null) { + if (responseCase_ == 3) { + return (com.google.spanner.v1.ExecuteBatchDmlResponse) response_; + } + return com.google.spanner.v1.ExecuteBatchDmlResponse.getDefaultInstance(); + } else { + if (responseCase_ == 3) { + return executeBatchResponseBuilder_.getMessage(); + } + return com.google.spanner.v1.ExecuteBatchDmlResponse.getDefaultInstance(); + } + } + /** + * .google.spanner.v1.ExecuteBatchDmlResponse execute_batch_response = 3; + */ + public Builder setExecuteBatchResponse(com.google.spanner.v1.ExecuteBatchDmlResponse value) { + if (executeBatchResponseBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + response_ = value; + onChanged(); + } else { + executeBatchResponseBuilder_.setMessage(value); + } + responseCase_ = 3; + return this; + } + /** + * .google.spanner.v1.ExecuteBatchDmlResponse execute_batch_response = 3; + */ + public Builder setExecuteBatchResponse( + com.google.spanner.v1.ExecuteBatchDmlResponse.Builder builderForValue) { + if (executeBatchResponseBuilder_ == null) { + response_ = builderForValue.build(); + onChanged(); + } else { + executeBatchResponseBuilder_.setMessage(builderForValue.build()); + } + responseCase_ = 3; + return this; + } + /** + * .google.spanner.v1.ExecuteBatchDmlResponse execute_batch_response = 3; + */ + public Builder mergeExecuteBatchResponse(com.google.spanner.v1.ExecuteBatchDmlResponse value) { + if (executeBatchResponseBuilder_ == null) { + if (responseCase_ == 3 && + response_ != com.google.spanner.v1.ExecuteBatchDmlResponse.getDefaultInstance()) { + response_ = com.google.spanner.v1.ExecuteBatchDmlResponse.newBuilder((com.google.spanner.v1.ExecuteBatchDmlResponse) response_) + .mergeFrom(value).buildPartial(); + } else { + response_ = value; + } + onChanged(); + } else { + if (responseCase_ == 3) { + executeBatchResponseBuilder_.mergeFrom(value); + } else { + executeBatchResponseBuilder_.setMessage(value); + } + } + responseCase_ = 3; + return this; + } + /** + * .google.spanner.v1.ExecuteBatchDmlResponse execute_batch_response = 3; + */ + public Builder clearExecuteBatchResponse() { + if (executeBatchResponseBuilder_ == null) { + if (responseCase_ == 3) { + responseCase_ = 0; + response_ = null; + onChanged(); + } + } else { + if (responseCase_ == 3) { + responseCase_ = 0; + response_ = null; + } + executeBatchResponseBuilder_.clear(); + } + return this; + } + /** + * .google.spanner.v1.ExecuteBatchDmlResponse execute_batch_response = 3; + */ + public com.google.spanner.v1.ExecuteBatchDmlResponse.Builder getExecuteBatchResponseBuilder() { + return internalGetExecuteBatchResponseFieldBuilder().getBuilder(); + } + /** + * .google.spanner.v1.ExecuteBatchDmlResponse execute_batch_response = 3; + */ + @java.lang.Override + public com.google.spanner.v1.ExecuteBatchDmlResponseOrBuilder getExecuteBatchResponseOrBuilder() { + if ((responseCase_ == 3) && (executeBatchResponseBuilder_ != null)) { + return executeBatchResponseBuilder_.getMessageOrBuilder(); + } else { + if (responseCase_ == 3) { + return (com.google.spanner.v1.ExecuteBatchDmlResponse) response_; + } + return com.google.spanner.v1.ExecuteBatchDmlResponse.getDefaultInstance(); + } + } + /** + * .google.spanner.v1.ExecuteBatchDmlResponse execute_batch_response = 3; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.spanner.v1.ExecuteBatchDmlResponse, com.google.spanner.v1.ExecuteBatchDmlResponse.Builder, com.google.spanner.v1.ExecuteBatchDmlResponseOrBuilder> + internalGetExecuteBatchResponseFieldBuilder() { + if (executeBatchResponseBuilder_ == null) { + if (!(responseCase_ == 3)) { + response_ = com.google.spanner.v1.ExecuteBatchDmlResponse.getDefaultInstance(); + } + executeBatchResponseBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.spanner.v1.ExecuteBatchDmlResponse, com.google.spanner.v1.ExecuteBatchDmlResponse.Builder, com.google.spanner.v1.ExecuteBatchDmlResponseOrBuilder>( + (com.google.spanner.v1.ExecuteBatchDmlResponse) response_, + getParentForChildren(), + isClean()); + response_ = null; + } + responseCase_ = 3; + onChanged(); + return executeBatchResponseBuilder_; + } + + private com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Empty, com.google.protobuf.Empty.Builder, com.google.protobuf.EmptyOrBuilder> beginTransactionResponseBuilder_; + /** + * .google.protobuf.Empty begin_transaction_response = 4; + * @return Whether the beginTransactionResponse field is set. + */ + @java.lang.Override + public boolean hasBeginTransactionResponse() { + return responseCase_ == 4; + } + /** + * .google.protobuf.Empty begin_transaction_response = 4; + * @return The beginTransactionResponse. + */ + @java.lang.Override + public com.google.protobuf.Empty getBeginTransactionResponse() { + if (beginTransactionResponseBuilder_ == null) { + if (responseCase_ == 4) { + return (com.google.protobuf.Empty) response_; + } + return com.google.protobuf.Empty.getDefaultInstance(); + } else { + if (responseCase_ == 4) { + return beginTransactionResponseBuilder_.getMessage(); + } + return com.google.protobuf.Empty.getDefaultInstance(); + } + } + /** + * .google.protobuf.Empty begin_transaction_response = 4; + */ + public Builder setBeginTransactionResponse(com.google.protobuf.Empty value) { + if (beginTransactionResponseBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + response_ = value; + onChanged(); + } else { + beginTransactionResponseBuilder_.setMessage(value); + } + responseCase_ = 4; + return this; + } + /** + * .google.protobuf.Empty begin_transaction_response = 4; + */ + public Builder setBeginTransactionResponse( + com.google.protobuf.Empty.Builder builderForValue) { + if (beginTransactionResponseBuilder_ == null) { + response_ = builderForValue.build(); + onChanged(); + } else { + beginTransactionResponseBuilder_.setMessage(builderForValue.build()); + } + responseCase_ = 4; + return this; + } + /** + * .google.protobuf.Empty begin_transaction_response = 4; + */ + public Builder mergeBeginTransactionResponse(com.google.protobuf.Empty value) { + if (beginTransactionResponseBuilder_ == null) { + if (responseCase_ == 4 && + response_ != com.google.protobuf.Empty.getDefaultInstance()) { + response_ = com.google.protobuf.Empty.newBuilder((com.google.protobuf.Empty) response_) + .mergeFrom(value).buildPartial(); + } else { + response_ = value; + } + onChanged(); + } else { + if (responseCase_ == 4) { + beginTransactionResponseBuilder_.mergeFrom(value); + } else { + beginTransactionResponseBuilder_.setMessage(value); + } + } + responseCase_ = 4; + return this; + } + /** + * .google.protobuf.Empty begin_transaction_response = 4; + */ + public Builder clearBeginTransactionResponse() { + if (beginTransactionResponseBuilder_ == null) { + if (responseCase_ == 4) { + responseCase_ = 0; + response_ = null; + onChanged(); + } + } else { + if (responseCase_ == 4) { + responseCase_ = 0; + response_ = null; + } + beginTransactionResponseBuilder_.clear(); + } + return this; + } + /** + * .google.protobuf.Empty begin_transaction_response = 4; + */ + public com.google.protobuf.Empty.Builder getBeginTransactionResponseBuilder() { + return internalGetBeginTransactionResponseFieldBuilder().getBuilder(); + } + /** + * .google.protobuf.Empty begin_transaction_response = 4; + */ + @java.lang.Override + public com.google.protobuf.EmptyOrBuilder getBeginTransactionResponseOrBuilder() { + if ((responseCase_ == 4) && (beginTransactionResponseBuilder_ != null)) { + return beginTransactionResponseBuilder_.getMessageOrBuilder(); + } else { + if (responseCase_ == 4) { + return (com.google.protobuf.Empty) response_; + } + return com.google.protobuf.Empty.getDefaultInstance(); + } + } + /** + * .google.protobuf.Empty begin_transaction_response = 4; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Empty, com.google.protobuf.Empty.Builder, com.google.protobuf.EmptyOrBuilder> + internalGetBeginTransactionResponseFieldBuilder() { + if (beginTransactionResponseBuilder_ == null) { + if (!(responseCase_ == 4)) { + response_ = com.google.protobuf.Empty.getDefaultInstance(); + } + beginTransactionResponseBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Empty, com.google.protobuf.Empty.Builder, com.google.protobuf.EmptyOrBuilder>( + (com.google.protobuf.Empty) response_, + getParentForChildren(), + isClean()); + response_ = null; + } + responseCase_ = 4; + onChanged(); + return beginTransactionResponseBuilder_; + } + + private com.google.protobuf.SingleFieldBuilder< + com.google.spanner.v1.CommitResponse, com.google.spanner.v1.CommitResponse.Builder, com.google.spanner.v1.CommitResponseOrBuilder> commitResponseBuilder_; + /** + * .google.spanner.v1.CommitResponse commit_response = 5; + * @return Whether the commitResponse field is set. + */ + @java.lang.Override + public boolean hasCommitResponse() { + return responseCase_ == 5; + } + /** + * .google.spanner.v1.CommitResponse commit_response = 5; + * @return The commitResponse. + */ + @java.lang.Override + public com.google.spanner.v1.CommitResponse getCommitResponse() { + if (commitResponseBuilder_ == null) { + if (responseCase_ == 5) { + return (com.google.spanner.v1.CommitResponse) response_; + } + return com.google.spanner.v1.CommitResponse.getDefaultInstance(); + } else { + if (responseCase_ == 5) { + return commitResponseBuilder_.getMessage(); + } + return com.google.spanner.v1.CommitResponse.getDefaultInstance(); + } + } + /** + * .google.spanner.v1.CommitResponse commit_response = 5; + */ + public Builder setCommitResponse(com.google.spanner.v1.CommitResponse value) { + if (commitResponseBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + response_ = value; + onChanged(); + } else { + commitResponseBuilder_.setMessage(value); + } + responseCase_ = 5; + return this; + } + /** + * .google.spanner.v1.CommitResponse commit_response = 5; + */ + public Builder setCommitResponse( + com.google.spanner.v1.CommitResponse.Builder builderForValue) { + if (commitResponseBuilder_ == null) { + response_ = builderForValue.build(); + onChanged(); + } else { + commitResponseBuilder_.setMessage(builderForValue.build()); + } + responseCase_ = 5; + return this; + } + /** + * .google.spanner.v1.CommitResponse commit_response = 5; + */ + public Builder mergeCommitResponse(com.google.spanner.v1.CommitResponse value) { + if (commitResponseBuilder_ == null) { + if (responseCase_ == 5 && + response_ != com.google.spanner.v1.CommitResponse.getDefaultInstance()) { + response_ = com.google.spanner.v1.CommitResponse.newBuilder((com.google.spanner.v1.CommitResponse) response_) + .mergeFrom(value).buildPartial(); + } else { + response_ = value; + } + onChanged(); + } else { + if (responseCase_ == 5) { + commitResponseBuilder_.mergeFrom(value); + } else { + commitResponseBuilder_.setMessage(value); + } + } + responseCase_ = 5; + return this; + } + /** + * .google.spanner.v1.CommitResponse commit_response = 5; + */ + public Builder clearCommitResponse() { + if (commitResponseBuilder_ == null) { + if (responseCase_ == 5) { + responseCase_ = 0; + response_ = null; + onChanged(); + } + } else { + if (responseCase_ == 5) { + responseCase_ = 0; + response_ = null; + } + commitResponseBuilder_.clear(); + } + return this; + } + /** + * .google.spanner.v1.CommitResponse commit_response = 5; + */ + public com.google.spanner.v1.CommitResponse.Builder getCommitResponseBuilder() { + return internalGetCommitResponseFieldBuilder().getBuilder(); + } + /** + * .google.spanner.v1.CommitResponse commit_response = 5; + */ + @java.lang.Override + public com.google.spanner.v1.CommitResponseOrBuilder getCommitResponseOrBuilder() { + if ((responseCase_ == 5) && (commitResponseBuilder_ != null)) { + return commitResponseBuilder_.getMessageOrBuilder(); + } else { + if (responseCase_ == 5) { + return (com.google.spanner.v1.CommitResponse) response_; + } + return com.google.spanner.v1.CommitResponse.getDefaultInstance(); + } + } + /** + * .google.spanner.v1.CommitResponse commit_response = 5; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.spanner.v1.CommitResponse, com.google.spanner.v1.CommitResponse.Builder, com.google.spanner.v1.CommitResponseOrBuilder> + internalGetCommitResponseFieldBuilder() { + if (commitResponseBuilder_ == null) { + if (!(responseCase_ == 5)) { + response_ = com.google.spanner.v1.CommitResponse.getDefaultInstance(); + } + commitResponseBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.spanner.v1.CommitResponse, com.google.spanner.v1.CommitResponse.Builder, com.google.spanner.v1.CommitResponseOrBuilder>( + (com.google.spanner.v1.CommitResponse) response_, + getParentForChildren(), + isClean()); + response_ = null; + } + responseCase_ = 5; + onChanged(); + return commitResponseBuilder_; + } + + private com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Empty, com.google.protobuf.Empty.Builder, com.google.protobuf.EmptyOrBuilder> rollbackResponseBuilder_; + /** + * .google.protobuf.Empty rollback_response = 6; + * @return Whether the rollbackResponse field is set. + */ + @java.lang.Override + public boolean hasRollbackResponse() { + return responseCase_ == 6; + } + /** + * .google.protobuf.Empty rollback_response = 6; + * @return The rollbackResponse. + */ + @java.lang.Override + public com.google.protobuf.Empty getRollbackResponse() { + if (rollbackResponseBuilder_ == null) { + if (responseCase_ == 6) { + return (com.google.protobuf.Empty) response_; + } + return com.google.protobuf.Empty.getDefaultInstance(); + } else { + if (responseCase_ == 6) { + return rollbackResponseBuilder_.getMessage(); + } + return com.google.protobuf.Empty.getDefaultInstance(); + } + } + /** + * .google.protobuf.Empty rollback_response = 6; + */ + public Builder setRollbackResponse(com.google.protobuf.Empty value) { + if (rollbackResponseBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + response_ = value; + onChanged(); + } else { + rollbackResponseBuilder_.setMessage(value); + } + responseCase_ = 6; + return this; + } + /** + * .google.protobuf.Empty rollback_response = 6; + */ + public Builder setRollbackResponse( + com.google.protobuf.Empty.Builder builderForValue) { + if (rollbackResponseBuilder_ == null) { + response_ = builderForValue.build(); + onChanged(); + } else { + rollbackResponseBuilder_.setMessage(builderForValue.build()); + } + responseCase_ = 6; + return this; + } + /** + * .google.protobuf.Empty rollback_response = 6; + */ + public Builder mergeRollbackResponse(com.google.protobuf.Empty value) { + if (rollbackResponseBuilder_ == null) { + if (responseCase_ == 6 && + response_ != com.google.protobuf.Empty.getDefaultInstance()) { + response_ = com.google.protobuf.Empty.newBuilder((com.google.protobuf.Empty) response_) + .mergeFrom(value).buildPartial(); + } else { + response_ = value; + } + onChanged(); + } else { + if (responseCase_ == 6) { + rollbackResponseBuilder_.mergeFrom(value); + } else { + rollbackResponseBuilder_.setMessage(value); + } + } + responseCase_ = 6; + return this; + } + /** + * .google.protobuf.Empty rollback_response = 6; + */ + public Builder clearRollbackResponse() { + if (rollbackResponseBuilder_ == null) { + if (responseCase_ == 6) { + responseCase_ = 0; + response_ = null; + onChanged(); + } + } else { + if (responseCase_ == 6) { + responseCase_ = 0; + response_ = null; + } + rollbackResponseBuilder_.clear(); + } + return this; + } + /** + * .google.protobuf.Empty rollback_response = 6; + */ + public com.google.protobuf.Empty.Builder getRollbackResponseBuilder() { + return internalGetRollbackResponseFieldBuilder().getBuilder(); + } + /** + * .google.protobuf.Empty rollback_response = 6; + */ + @java.lang.Override + public com.google.protobuf.EmptyOrBuilder getRollbackResponseOrBuilder() { + if ((responseCase_ == 6) && (rollbackResponseBuilder_ != null)) { + return rollbackResponseBuilder_.getMessageOrBuilder(); + } else { + if (responseCase_ == 6) { + return (com.google.protobuf.Empty) response_; + } + return com.google.protobuf.Empty.getDefaultInstance(); + } + } + /** + * .google.protobuf.Empty rollback_response = 6; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Empty, com.google.protobuf.Empty.Builder, com.google.protobuf.EmptyOrBuilder> + internalGetRollbackResponseFieldBuilder() { + if (rollbackResponseBuilder_ == null) { + if (!(responseCase_ == 6)) { + response_ = com.google.protobuf.Empty.getDefaultInstance(); + } + rollbackResponseBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.protobuf.Empty, com.google.protobuf.Empty.Builder, com.google.protobuf.EmptyOrBuilder>( + (com.google.protobuf.Empty) response_, + getParentForChildren(), + isClean()); + response_ = null; + } + responseCase_ = 6; + onChanged(); + return rollbackResponseBuilder_; + } + + private com.google.protobuf.SingleFieldBuilder< + com.google.spanner.v1.CommitResponse, com.google.spanner.v1.CommitResponse.Builder, com.google.spanner.v1.CommitResponseOrBuilder> writeMutationsResponseBuilder_; + /** + * .google.spanner.v1.CommitResponse write_mutations_response = 7; + * @return Whether the writeMutationsResponse field is set. + */ + @java.lang.Override + public boolean hasWriteMutationsResponse() { + return responseCase_ == 7; + } + /** + * .google.spanner.v1.CommitResponse write_mutations_response = 7; + * @return The writeMutationsResponse. + */ + @java.lang.Override + public com.google.spanner.v1.CommitResponse getWriteMutationsResponse() { + if (writeMutationsResponseBuilder_ == null) { + if (responseCase_ == 7) { + return (com.google.spanner.v1.CommitResponse) response_; + } + return com.google.spanner.v1.CommitResponse.getDefaultInstance(); + } else { + if (responseCase_ == 7) { + return writeMutationsResponseBuilder_.getMessage(); + } + return com.google.spanner.v1.CommitResponse.getDefaultInstance(); + } + } + /** + * .google.spanner.v1.CommitResponse write_mutations_response = 7; + */ + public Builder setWriteMutationsResponse(com.google.spanner.v1.CommitResponse value) { + if (writeMutationsResponseBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + response_ = value; + onChanged(); + } else { + writeMutationsResponseBuilder_.setMessage(value); + } + responseCase_ = 7; + return this; + } + /** + * .google.spanner.v1.CommitResponse write_mutations_response = 7; + */ + public Builder setWriteMutationsResponse( + com.google.spanner.v1.CommitResponse.Builder builderForValue) { + if (writeMutationsResponseBuilder_ == null) { + response_ = builderForValue.build(); + onChanged(); + } else { + writeMutationsResponseBuilder_.setMessage(builderForValue.build()); + } + responseCase_ = 7; + return this; + } + /** + * .google.spanner.v1.CommitResponse write_mutations_response = 7; + */ + public Builder mergeWriteMutationsResponse(com.google.spanner.v1.CommitResponse value) { + if (writeMutationsResponseBuilder_ == null) { + if (responseCase_ == 7 && + response_ != com.google.spanner.v1.CommitResponse.getDefaultInstance()) { + response_ = com.google.spanner.v1.CommitResponse.newBuilder((com.google.spanner.v1.CommitResponse) response_) + .mergeFrom(value).buildPartial(); + } else { + response_ = value; + } + onChanged(); + } else { + if (responseCase_ == 7) { + writeMutationsResponseBuilder_.mergeFrom(value); + } else { + writeMutationsResponseBuilder_.setMessage(value); + } + } + responseCase_ = 7; + return this; + } + /** + * .google.spanner.v1.CommitResponse write_mutations_response = 7; + */ + public Builder clearWriteMutationsResponse() { + if (writeMutationsResponseBuilder_ == null) { + if (responseCase_ == 7) { + responseCase_ = 0; + response_ = null; + onChanged(); + } + } else { + if (responseCase_ == 7) { + responseCase_ = 0; + response_ = null; + } + writeMutationsResponseBuilder_.clear(); + } + return this; + } + /** + * .google.spanner.v1.CommitResponse write_mutations_response = 7; + */ + public com.google.spanner.v1.CommitResponse.Builder getWriteMutationsResponseBuilder() { + return internalGetWriteMutationsResponseFieldBuilder().getBuilder(); + } + /** + * .google.spanner.v1.CommitResponse write_mutations_response = 7; + */ + @java.lang.Override + public com.google.spanner.v1.CommitResponseOrBuilder getWriteMutationsResponseOrBuilder() { + if ((responseCase_ == 7) && (writeMutationsResponseBuilder_ != null)) { + return writeMutationsResponseBuilder_.getMessageOrBuilder(); + } else { + if (responseCase_ == 7) { + return (com.google.spanner.v1.CommitResponse) response_; + } + return com.google.spanner.v1.CommitResponse.getDefaultInstance(); + } + } + /** + * .google.spanner.v1.CommitResponse write_mutations_response = 7; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.spanner.v1.CommitResponse, com.google.spanner.v1.CommitResponse.Builder, com.google.spanner.v1.CommitResponseOrBuilder> + internalGetWriteMutationsResponseFieldBuilder() { + if (writeMutationsResponseBuilder_ == null) { + if (!(responseCase_ == 7)) { + response_ = com.google.spanner.v1.CommitResponse.getDefaultInstance(); + } + writeMutationsResponseBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.spanner.v1.CommitResponse, com.google.spanner.v1.CommitResponse.Builder, com.google.spanner.v1.CommitResponseOrBuilder>( + (com.google.spanner.v1.CommitResponse) response_, getParentForChildren(), isClean()); response_ = null; } - responseCase_ = 1; + responseCase_ = 7; onChanged(); - return rowBuilder_; + return writeMutationsResponseBuilder_; } // @@protoc_insertion_point(builder_scope:google.spannerlib.v1.ConnectionStreamResponse) diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ConnectionStreamResponseOrBuilder.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ConnectionStreamResponseOrBuilder.java index f3f7a97a..ef7e6c86 100644 --- a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ConnectionStreamResponseOrBuilder.java +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ConnectionStreamResponseOrBuilder.java @@ -11,19 +11,109 @@ public interface ConnectionStreamResponseOrBuilder extends com.google.protobuf.MessageOrBuilder { /** - * .google.spanner.v1.PartialResultSet row = 1; - * @return Whether the row field is set. + * .google.rpc.Status status = 1; + * @return Whether the status field is set. */ - boolean hasRow(); + boolean hasStatus(); /** - * .google.spanner.v1.PartialResultSet row = 1; - * @return The row. + * .google.rpc.Status status = 1; + * @return The status. */ - com.google.spanner.v1.PartialResultSet getRow(); + com.google.rpc.Status getStatus(); /** - * .google.spanner.v1.PartialResultSet row = 1; + * .google.rpc.Status status = 1; */ - com.google.spanner.v1.PartialResultSetOrBuilder getRowOrBuilder(); + com.google.rpc.StatusOrBuilder getStatusOrBuilder(); + + /** + * .google.spannerlib.v1.ExecuteResponse execute_response = 2; + * @return Whether the executeResponse field is set. + */ + boolean hasExecuteResponse(); + /** + * .google.spannerlib.v1.ExecuteResponse execute_response = 2; + * @return The executeResponse. + */ + com.google.cloud.spannerlib.v1.ExecuteResponse getExecuteResponse(); + /** + * .google.spannerlib.v1.ExecuteResponse execute_response = 2; + */ + com.google.cloud.spannerlib.v1.ExecuteResponseOrBuilder getExecuteResponseOrBuilder(); + + /** + * .google.spanner.v1.ExecuteBatchDmlResponse execute_batch_response = 3; + * @return Whether the executeBatchResponse field is set. + */ + boolean hasExecuteBatchResponse(); + /** + * .google.spanner.v1.ExecuteBatchDmlResponse execute_batch_response = 3; + * @return The executeBatchResponse. + */ + com.google.spanner.v1.ExecuteBatchDmlResponse getExecuteBatchResponse(); + /** + * .google.spanner.v1.ExecuteBatchDmlResponse execute_batch_response = 3; + */ + com.google.spanner.v1.ExecuteBatchDmlResponseOrBuilder getExecuteBatchResponseOrBuilder(); + + /** + * .google.protobuf.Empty begin_transaction_response = 4; + * @return Whether the beginTransactionResponse field is set. + */ + boolean hasBeginTransactionResponse(); + /** + * .google.protobuf.Empty begin_transaction_response = 4; + * @return The beginTransactionResponse. + */ + com.google.protobuf.Empty getBeginTransactionResponse(); + /** + * .google.protobuf.Empty begin_transaction_response = 4; + */ + com.google.protobuf.EmptyOrBuilder getBeginTransactionResponseOrBuilder(); + + /** + * .google.spanner.v1.CommitResponse commit_response = 5; + * @return Whether the commitResponse field is set. + */ + boolean hasCommitResponse(); + /** + * .google.spanner.v1.CommitResponse commit_response = 5; + * @return The commitResponse. + */ + com.google.spanner.v1.CommitResponse getCommitResponse(); + /** + * .google.spanner.v1.CommitResponse commit_response = 5; + */ + com.google.spanner.v1.CommitResponseOrBuilder getCommitResponseOrBuilder(); + + /** + * .google.protobuf.Empty rollback_response = 6; + * @return Whether the rollbackResponse field is set. + */ + boolean hasRollbackResponse(); + /** + * .google.protobuf.Empty rollback_response = 6; + * @return The rollbackResponse. + */ + com.google.protobuf.Empty getRollbackResponse(); + /** + * .google.protobuf.Empty rollback_response = 6; + */ + com.google.protobuf.EmptyOrBuilder getRollbackResponseOrBuilder(); + + /** + * .google.spanner.v1.CommitResponse write_mutations_response = 7; + * @return Whether the writeMutationsResponse field is set. + */ + boolean hasWriteMutationsResponse(); + /** + * .google.spanner.v1.CommitResponse write_mutations_response = 7; + * @return The writeMutationsResponse. + */ + com.google.spanner.v1.CommitResponse getWriteMutationsResponse(); + /** + * .google.spanner.v1.CommitResponse write_mutations_response = 7; + */ + com.google.spanner.v1.CommitResponseOrBuilder getWriteMutationsResponseOrBuilder(); com.google.cloud.spannerlib.v1.ConnectionStreamResponse.ResponseCase getResponseCase(); } diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteAllResponse.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteAllResponse.java new file mode 100644 index 00000000..1d55c691 --- /dev/null +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteAllResponse.java @@ -0,0 +1,720 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// NO CHECKED-IN PROTOBUF GENCODE +// source: google/spannerlib/v1/spannerlib.proto +// Protobuf Java Version: 4.32.1 + +package com.google.cloud.spannerlib.v1; + +/** + * Protobuf type {@code google.spannerlib.v1.ExecuteAllResponse} + */ +@com.google.protobuf.Generated +public final class ExecuteAllResponse extends + com.google.protobuf.GeneratedMessage implements + // @@protoc_insertion_point(message_implements:google.spannerlib.v1.ExecuteAllResponse) + ExecuteAllResponseOrBuilder { +private static final long serialVersionUID = 0L; + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 32, + /* patch= */ 1, + /* suffix= */ "", + ExecuteAllResponse.class.getName()); + } + // Use ExecuteAllResponse.newBuilder() to construct. + private ExecuteAllResponse(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + } + private ExecuteAllResponse() { + resultSets_ = java.util.Collections.emptyList(); + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteAllResponse_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteAllResponse_fieldAccessorTable + .ensureFieldAccessorsInitialized( + com.google.cloud.spannerlib.v1.ExecuteAllResponse.class, com.google.cloud.spannerlib.v1.ExecuteAllResponse.Builder.class); + } + + public static final int RESULT_SETS_FIELD_NUMBER = 1; + @SuppressWarnings("serial") + private java.util.List resultSets_; + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + @java.lang.Override + public java.util.List getResultSetsList() { + return resultSets_; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + @java.lang.Override + public java.util.List + getResultSetsOrBuilderList() { + return resultSets_; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + @java.lang.Override + public int getResultSetsCount() { + return resultSets_.size(); + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + @java.lang.Override + public com.google.spanner.v1.ResultSet getResultSets(int index) { + return resultSets_.get(index); + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + @java.lang.Override + public com.google.spanner.v1.ResultSetOrBuilder getResultSetsOrBuilder( + int index) { + return resultSets_.get(index); + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + for (int i = 0; i < resultSets_.size(); i++) { + output.writeMessage(1, resultSets_.get(i)); + } + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + for (int i = 0; i < resultSets_.size(); i++) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(1, resultSets_.get(i)); + } + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof com.google.cloud.spannerlib.v1.ExecuteAllResponse)) { + return super.equals(obj); + } + com.google.cloud.spannerlib.v1.ExecuteAllResponse other = (com.google.cloud.spannerlib.v1.ExecuteAllResponse) obj; + + if (!getResultSetsList() + .equals(other.getResultSetsList())) return false; + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + if (getResultSetsCount() > 0) { + hash = (37 * hash) + RESULT_SETS_FIELD_NUMBER; + hash = (53 * hash) + getResultSetsList().hashCode(); + } + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input); + } + + public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(com.google.cloud.spannerlib.v1.ExecuteAllResponse prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code google.spannerlib.v1.ExecuteAllResponse} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder implements + // @@protoc_insertion_point(builder_implements:google.spannerlib.v1.ExecuteAllResponse) + com.google.cloud.spannerlib.v1.ExecuteAllResponseOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteAllResponse_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteAllResponse_fieldAccessorTable + .ensureFieldAccessorsInitialized( + com.google.cloud.spannerlib.v1.ExecuteAllResponse.class, com.google.cloud.spannerlib.v1.ExecuteAllResponse.Builder.class); + } + + // Construct using com.google.cloud.spannerlib.v1.ExecuteAllResponse.newBuilder() + private Builder() { + + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + + } + @java.lang.Override + public Builder clear() { + super.clear(); + bitField0_ = 0; + if (resultSetsBuilder_ == null) { + resultSets_ = java.util.Collections.emptyList(); + } else { + resultSets_ = null; + resultSetsBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000001); + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteAllResponse_descriptor; + } + + @java.lang.Override + public com.google.cloud.spannerlib.v1.ExecuteAllResponse getDefaultInstanceForType() { + return com.google.cloud.spannerlib.v1.ExecuteAllResponse.getDefaultInstance(); + } + + @java.lang.Override + public com.google.cloud.spannerlib.v1.ExecuteAllResponse build() { + com.google.cloud.spannerlib.v1.ExecuteAllResponse result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public com.google.cloud.spannerlib.v1.ExecuteAllResponse buildPartial() { + com.google.cloud.spannerlib.v1.ExecuteAllResponse result = new com.google.cloud.spannerlib.v1.ExecuteAllResponse(this); + buildPartialRepeatedFields(result); + if (bitField0_ != 0) { buildPartial0(result); } + onBuilt(); + return result; + } + + private void buildPartialRepeatedFields(com.google.cloud.spannerlib.v1.ExecuteAllResponse result) { + if (resultSetsBuilder_ == null) { + if (((bitField0_ & 0x00000001) != 0)) { + resultSets_ = java.util.Collections.unmodifiableList(resultSets_); + bitField0_ = (bitField0_ & ~0x00000001); + } + result.resultSets_ = resultSets_; + } else { + result.resultSets_ = resultSetsBuilder_.build(); + } + } + + private void buildPartial0(com.google.cloud.spannerlib.v1.ExecuteAllResponse result) { + int from_bitField0_ = bitField0_; + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof com.google.cloud.spannerlib.v1.ExecuteAllResponse) { + return mergeFrom((com.google.cloud.spannerlib.v1.ExecuteAllResponse)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(com.google.cloud.spannerlib.v1.ExecuteAllResponse other) { + if (other == com.google.cloud.spannerlib.v1.ExecuteAllResponse.getDefaultInstance()) return this; + if (resultSetsBuilder_ == null) { + if (!other.resultSets_.isEmpty()) { + if (resultSets_.isEmpty()) { + resultSets_ = other.resultSets_; + bitField0_ = (bitField0_ & ~0x00000001); + } else { + ensureResultSetsIsMutable(); + resultSets_.addAll(other.resultSets_); + } + onChanged(); + } + } else { + if (!other.resultSets_.isEmpty()) { + if (resultSetsBuilder_.isEmpty()) { + resultSetsBuilder_.dispose(); + resultSetsBuilder_ = null; + resultSets_ = other.resultSets_; + bitField0_ = (bitField0_ & ~0x00000001); + resultSetsBuilder_ = + com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ? + internalGetResultSetsFieldBuilder() : null; + } else { + resultSetsBuilder_.addAllMessages(other.resultSets_); + } + } + } + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 10: { + com.google.spanner.v1.ResultSet m = + input.readMessage( + com.google.spanner.v1.ResultSet.parser(), + extensionRegistry); + if (resultSetsBuilder_ == null) { + ensureResultSetsIsMutable(); + resultSets_.add(m); + } else { + resultSetsBuilder_.addMessage(m); + } + break; + } // case 10 + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + private int bitField0_; + + private java.util.List resultSets_ = + java.util.Collections.emptyList(); + private void ensureResultSetsIsMutable() { + if (!((bitField0_ & 0x00000001) != 0)) { + resultSets_ = new java.util.ArrayList(resultSets_); + bitField0_ |= 0x00000001; + } + } + + private com.google.protobuf.RepeatedFieldBuilder< + com.google.spanner.v1.ResultSet, com.google.spanner.v1.ResultSet.Builder, com.google.spanner.v1.ResultSetOrBuilder> resultSetsBuilder_; + + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + public java.util.List getResultSetsList() { + if (resultSetsBuilder_ == null) { + return java.util.Collections.unmodifiableList(resultSets_); + } else { + return resultSetsBuilder_.getMessageList(); + } + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + public int getResultSetsCount() { + if (resultSetsBuilder_ == null) { + return resultSets_.size(); + } else { + return resultSetsBuilder_.getCount(); + } + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + public com.google.spanner.v1.ResultSet getResultSets(int index) { + if (resultSetsBuilder_ == null) { + return resultSets_.get(index); + } else { + return resultSetsBuilder_.getMessage(index); + } + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + public Builder setResultSets( + int index, com.google.spanner.v1.ResultSet value) { + if (resultSetsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureResultSetsIsMutable(); + resultSets_.set(index, value); + onChanged(); + } else { + resultSetsBuilder_.setMessage(index, value); + } + return this; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + public Builder setResultSets( + int index, com.google.spanner.v1.ResultSet.Builder builderForValue) { + if (resultSetsBuilder_ == null) { + ensureResultSetsIsMutable(); + resultSets_.set(index, builderForValue.build()); + onChanged(); + } else { + resultSetsBuilder_.setMessage(index, builderForValue.build()); + } + return this; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + public Builder addResultSets(com.google.spanner.v1.ResultSet value) { + if (resultSetsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureResultSetsIsMutable(); + resultSets_.add(value); + onChanged(); + } else { + resultSetsBuilder_.addMessage(value); + } + return this; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + public Builder addResultSets( + int index, com.google.spanner.v1.ResultSet value) { + if (resultSetsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureResultSetsIsMutable(); + resultSets_.add(index, value); + onChanged(); + } else { + resultSetsBuilder_.addMessage(index, value); + } + return this; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + public Builder addResultSets( + com.google.spanner.v1.ResultSet.Builder builderForValue) { + if (resultSetsBuilder_ == null) { + ensureResultSetsIsMutable(); + resultSets_.add(builderForValue.build()); + onChanged(); + } else { + resultSetsBuilder_.addMessage(builderForValue.build()); + } + return this; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + public Builder addResultSets( + int index, com.google.spanner.v1.ResultSet.Builder builderForValue) { + if (resultSetsBuilder_ == null) { + ensureResultSetsIsMutable(); + resultSets_.add(index, builderForValue.build()); + onChanged(); + } else { + resultSetsBuilder_.addMessage(index, builderForValue.build()); + } + return this; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + public Builder addAllResultSets( + java.lang.Iterable values) { + if (resultSetsBuilder_ == null) { + ensureResultSetsIsMutable(); + com.google.protobuf.AbstractMessageLite.Builder.addAll( + values, resultSets_); + onChanged(); + } else { + resultSetsBuilder_.addAllMessages(values); + } + return this; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + public Builder clearResultSets() { + if (resultSetsBuilder_ == null) { + resultSets_ = java.util.Collections.emptyList(); + bitField0_ = (bitField0_ & ~0x00000001); + onChanged(); + } else { + resultSetsBuilder_.clear(); + } + return this; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + public Builder removeResultSets(int index) { + if (resultSetsBuilder_ == null) { + ensureResultSetsIsMutable(); + resultSets_.remove(index); + onChanged(); + } else { + resultSetsBuilder_.remove(index); + } + return this; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + public com.google.spanner.v1.ResultSet.Builder getResultSetsBuilder( + int index) { + return internalGetResultSetsFieldBuilder().getBuilder(index); + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + public com.google.spanner.v1.ResultSetOrBuilder getResultSetsOrBuilder( + int index) { + if (resultSetsBuilder_ == null) { + return resultSets_.get(index); } else { + return resultSetsBuilder_.getMessageOrBuilder(index); + } + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + public java.util.List + getResultSetsOrBuilderList() { + if (resultSetsBuilder_ != null) { + return resultSetsBuilder_.getMessageOrBuilderList(); + } else { + return java.util.Collections.unmodifiableList(resultSets_); + } + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + public com.google.spanner.v1.ResultSet.Builder addResultSetsBuilder() { + return internalGetResultSetsFieldBuilder().addBuilder( + com.google.spanner.v1.ResultSet.getDefaultInstance()); + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + public com.google.spanner.v1.ResultSet.Builder addResultSetsBuilder( + int index) { + return internalGetResultSetsFieldBuilder().addBuilder( + index, com.google.spanner.v1.ResultSet.getDefaultInstance()); + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + public java.util.List + getResultSetsBuilderList() { + return internalGetResultSetsFieldBuilder().getBuilderList(); + } + private com.google.protobuf.RepeatedFieldBuilder< + com.google.spanner.v1.ResultSet, com.google.spanner.v1.ResultSet.Builder, com.google.spanner.v1.ResultSetOrBuilder> + internalGetResultSetsFieldBuilder() { + if (resultSetsBuilder_ == null) { + resultSetsBuilder_ = new com.google.protobuf.RepeatedFieldBuilder< + com.google.spanner.v1.ResultSet, com.google.spanner.v1.ResultSet.Builder, com.google.spanner.v1.ResultSetOrBuilder>( + resultSets_, + ((bitField0_ & 0x00000001) != 0), + getParentForChildren(), + isClean()); + resultSets_ = null; + } + return resultSetsBuilder_; + } + + // @@protoc_insertion_point(builder_scope:google.spannerlib.v1.ExecuteAllResponse) + } + + // @@protoc_insertion_point(class_scope:google.spannerlib.v1.ExecuteAllResponse) + private static final com.google.cloud.spannerlib.v1.ExecuteAllResponse DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new com.google.cloud.spannerlib.v1.ExecuteAllResponse(); + } + + public static com.google.cloud.spannerlib.v1.ExecuteAllResponse getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public ExecuteAllResponse parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public com.google.cloud.spannerlib.v1.ExecuteAllResponse getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + +} + diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteAllResponseOrBuilder.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteAllResponseOrBuilder.java new file mode 100644 index 00000000..1369da1c --- /dev/null +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteAllResponseOrBuilder.java @@ -0,0 +1,36 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// NO CHECKED-IN PROTOBUF GENCODE +// source: google/spannerlib/v1/spannerlib.proto +// Protobuf Java Version: 4.32.1 + +package com.google.cloud.spannerlib.v1; + +@com.google.protobuf.Generated +public interface ExecuteAllResponseOrBuilder extends + // @@protoc_insertion_point(interface_extends:google.spannerlib.v1.ExecuteAllResponse) + com.google.protobuf.MessageOrBuilder { + + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + java.util.List + getResultSetsList(); + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + com.google.spanner.v1.ResultSet getResultSets(int index); + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + int getResultSetsCount(); + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + java.util.List + getResultSetsOrBuilderList(); + /** + * repeated .google.spanner.v1.ResultSet result_sets = 1; + */ + com.google.spanner.v1.ResultSetOrBuilder getResultSetsOrBuilder( + int index); +} diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteRequest.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteRequest.java index 06f0fff1..66ba8512 100644 --- a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteRequest.java +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteRequest.java @@ -96,6 +96,32 @@ public com.google.spanner.v1.ExecuteSqlRequestOrBuilder getExecuteSqlRequestOrBu return executeSqlRequest_ == null ? com.google.spanner.v1.ExecuteSqlRequest.getDefaultInstance() : executeSqlRequest_; } + public static final int FETCH_OPTIONS_FIELD_NUMBER = 3; + private com.google.cloud.spannerlib.v1.FetchOptions fetchOptions_; + /** + * .google.spannerlib.v1.FetchOptions fetch_options = 3; + * @return Whether the fetchOptions field is set. + */ + @java.lang.Override + public boolean hasFetchOptions() { + return ((bitField0_ & 0x00000004) != 0); + } + /** + * .google.spannerlib.v1.FetchOptions fetch_options = 3; + * @return The fetchOptions. + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.FetchOptions getFetchOptions() { + return fetchOptions_ == null ? com.google.cloud.spannerlib.v1.FetchOptions.getDefaultInstance() : fetchOptions_; + } + /** + * .google.spannerlib.v1.FetchOptions fetch_options = 3; + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.FetchOptionsOrBuilder getFetchOptionsOrBuilder() { + return fetchOptions_ == null ? com.google.cloud.spannerlib.v1.FetchOptions.getDefaultInstance() : fetchOptions_; + } + private byte memoizedIsInitialized = -1; @java.lang.Override public final boolean isInitialized() { @@ -116,6 +142,9 @@ public void writeTo(com.google.protobuf.CodedOutputStream output) if (((bitField0_ & 0x00000002) != 0)) { output.writeMessage(2, getExecuteSqlRequest()); } + if (((bitField0_ & 0x00000004) != 0)) { + output.writeMessage(3, getFetchOptions()); + } getUnknownFields().writeTo(output); } @@ -133,6 +162,10 @@ public int getSerializedSize() { size += com.google.protobuf.CodedOutputStream .computeMessageSize(2, getExecuteSqlRequest()); } + if (((bitField0_ & 0x00000004) != 0)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(3, getFetchOptions()); + } size += getUnknownFields().getSerializedSize(); memoizedSize = size; return size; @@ -158,6 +191,11 @@ public boolean equals(final java.lang.Object obj) { if (!getExecuteSqlRequest() .equals(other.getExecuteSqlRequest())) return false; } + if (hasFetchOptions() != other.hasFetchOptions()) return false; + if (hasFetchOptions()) { + if (!getFetchOptions() + .equals(other.getFetchOptions())) return false; + } if (!getUnknownFields().equals(other.getUnknownFields())) return false; return true; } @@ -177,6 +215,10 @@ public int hashCode() { hash = (37 * hash) + EXECUTE_SQL_REQUEST_FIELD_NUMBER; hash = (53 * hash) + getExecuteSqlRequest().hashCode(); } + if (hasFetchOptions()) { + hash = (37 * hash) + FETCH_OPTIONS_FIELD_NUMBER; + hash = (53 * hash) + getFetchOptions().hashCode(); + } hash = (29 * hash) + getUnknownFields().hashCode(); memoizedHashCode = hash; return hash; @@ -309,6 +351,7 @@ private void maybeForceBuilderInitialization() { .alwaysUseFieldBuilders) { internalGetConnectionFieldBuilder(); internalGetExecuteSqlRequestFieldBuilder(); + internalGetFetchOptionsFieldBuilder(); } } @java.lang.Override @@ -325,6 +368,11 @@ public Builder clear() { executeSqlRequestBuilder_.dispose(); executeSqlRequestBuilder_ = null; } + fetchOptions_ = null; + if (fetchOptionsBuilder_ != null) { + fetchOptionsBuilder_.dispose(); + fetchOptionsBuilder_ = null; + } return this; } @@ -371,6 +419,12 @@ private void buildPartial0(com.google.cloud.spannerlib.v1.ExecuteRequest result) : executeSqlRequestBuilder_.build(); to_bitField0_ |= 0x00000002; } + if (((from_bitField0_ & 0x00000004) != 0)) { + result.fetchOptions_ = fetchOptionsBuilder_ == null + ? fetchOptions_ + : fetchOptionsBuilder_.build(); + to_bitField0_ |= 0x00000004; + } result.bitField0_ |= to_bitField0_; } @@ -392,6 +446,9 @@ public Builder mergeFrom(com.google.cloud.spannerlib.v1.ExecuteRequest other) { if (other.hasExecuteSqlRequest()) { mergeExecuteSqlRequest(other.getExecuteSqlRequest()); } + if (other.hasFetchOptions()) { + mergeFetchOptions(other.getFetchOptions()); + } this.mergeUnknownFields(other.getUnknownFields()); onChanged(); return this; @@ -432,6 +489,13 @@ public Builder mergeFrom( bitField0_ |= 0x00000002; break; } // case 18 + case 26: { + input.readMessage( + internalGetFetchOptionsFieldBuilder().getBuilder(), + extensionRegistry); + bitField0_ |= 0x00000004; + break; + } // case 26 default: { if (!super.parseUnknownField(input, extensionRegistry, tag)) { done = true; // was an endgroup tag @@ -691,6 +755,127 @@ public com.google.spanner.v1.ExecuteSqlRequestOrBuilder getExecuteSqlRequestOrBu return executeSqlRequestBuilder_; } + private com.google.cloud.spannerlib.v1.FetchOptions fetchOptions_; + private com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.FetchOptions, com.google.cloud.spannerlib.v1.FetchOptions.Builder, com.google.cloud.spannerlib.v1.FetchOptionsOrBuilder> fetchOptionsBuilder_; + /** + * .google.spannerlib.v1.FetchOptions fetch_options = 3; + * @return Whether the fetchOptions field is set. + */ + public boolean hasFetchOptions() { + return ((bitField0_ & 0x00000004) != 0); + } + /** + * .google.spannerlib.v1.FetchOptions fetch_options = 3; + * @return The fetchOptions. + */ + public com.google.cloud.spannerlib.v1.FetchOptions getFetchOptions() { + if (fetchOptionsBuilder_ == null) { + return fetchOptions_ == null ? com.google.cloud.spannerlib.v1.FetchOptions.getDefaultInstance() : fetchOptions_; + } else { + return fetchOptionsBuilder_.getMessage(); + } + } + /** + * .google.spannerlib.v1.FetchOptions fetch_options = 3; + */ + public Builder setFetchOptions(com.google.cloud.spannerlib.v1.FetchOptions value) { + if (fetchOptionsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + fetchOptions_ = value; + } else { + fetchOptionsBuilder_.setMessage(value); + } + bitField0_ |= 0x00000004; + onChanged(); + return this; + } + /** + * .google.spannerlib.v1.FetchOptions fetch_options = 3; + */ + public Builder setFetchOptions( + com.google.cloud.spannerlib.v1.FetchOptions.Builder builderForValue) { + if (fetchOptionsBuilder_ == null) { + fetchOptions_ = builderForValue.build(); + } else { + fetchOptionsBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000004; + onChanged(); + return this; + } + /** + * .google.spannerlib.v1.FetchOptions fetch_options = 3; + */ + public Builder mergeFetchOptions(com.google.cloud.spannerlib.v1.FetchOptions value) { + if (fetchOptionsBuilder_ == null) { + if (((bitField0_ & 0x00000004) != 0) && + fetchOptions_ != null && + fetchOptions_ != com.google.cloud.spannerlib.v1.FetchOptions.getDefaultInstance()) { + getFetchOptionsBuilder().mergeFrom(value); + } else { + fetchOptions_ = value; + } + } else { + fetchOptionsBuilder_.mergeFrom(value); + } + if (fetchOptions_ != null) { + bitField0_ |= 0x00000004; + onChanged(); + } + return this; + } + /** + * .google.spannerlib.v1.FetchOptions fetch_options = 3; + */ + public Builder clearFetchOptions() { + bitField0_ = (bitField0_ & ~0x00000004); + fetchOptions_ = null; + if (fetchOptionsBuilder_ != null) { + fetchOptionsBuilder_.dispose(); + fetchOptionsBuilder_ = null; + } + onChanged(); + return this; + } + /** + * .google.spannerlib.v1.FetchOptions fetch_options = 3; + */ + public com.google.cloud.spannerlib.v1.FetchOptions.Builder getFetchOptionsBuilder() { + bitField0_ |= 0x00000004; + onChanged(); + return internalGetFetchOptionsFieldBuilder().getBuilder(); + } + /** + * .google.spannerlib.v1.FetchOptions fetch_options = 3; + */ + public com.google.cloud.spannerlib.v1.FetchOptionsOrBuilder getFetchOptionsOrBuilder() { + if (fetchOptionsBuilder_ != null) { + return fetchOptionsBuilder_.getMessageOrBuilder(); + } else { + return fetchOptions_ == null ? + com.google.cloud.spannerlib.v1.FetchOptions.getDefaultInstance() : fetchOptions_; + } + } + /** + * .google.spannerlib.v1.FetchOptions fetch_options = 3; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.FetchOptions, com.google.cloud.spannerlib.v1.FetchOptions.Builder, com.google.cloud.spannerlib.v1.FetchOptionsOrBuilder> + internalGetFetchOptionsFieldBuilder() { + if (fetchOptionsBuilder_ == null) { + fetchOptionsBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.FetchOptions, com.google.cloud.spannerlib.v1.FetchOptions.Builder, com.google.cloud.spannerlib.v1.FetchOptionsOrBuilder>( + getFetchOptions(), + getParentForChildren(), + isClean()); + fetchOptions_ = null; + } + return fetchOptionsBuilder_; + } + // @@protoc_insertion_point(builder_scope:google.spannerlib.v1.ExecuteRequest) } diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteRequestOrBuilder.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteRequestOrBuilder.java index 89f82663..a24acd84 100644 --- a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteRequestOrBuilder.java +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteRequestOrBuilder.java @@ -39,4 +39,19 @@ public interface ExecuteRequestOrBuilder extends * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; */ com.google.spanner.v1.ExecuteSqlRequestOrBuilder getExecuteSqlRequestOrBuilder(); + + /** + * .google.spannerlib.v1.FetchOptions fetch_options = 3; + * @return Whether the fetchOptions field is set. + */ + boolean hasFetchOptions(); + /** + * .google.spannerlib.v1.FetchOptions fetch_options = 3; + * @return The fetchOptions. + */ + com.google.cloud.spannerlib.v1.FetchOptions getFetchOptions(); + /** + * .google.spannerlib.v1.FetchOptions fetch_options = 3; + */ + com.google.cloud.spannerlib.v1.FetchOptionsOrBuilder getFetchOptionsOrBuilder(); } diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteResponse.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteResponse.java new file mode 100644 index 00000000..08dd85c7 --- /dev/null +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteResponse.java @@ -0,0 +1,1166 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// NO CHECKED-IN PROTOBUF GENCODE +// source: google/spannerlib/v1/spannerlib.proto +// Protobuf Java Version: 4.32.1 + +package com.google.cloud.spannerlib.v1; + +/** + * Protobuf type {@code google.spannerlib.v1.ExecuteResponse} + */ +@com.google.protobuf.Generated +public final class ExecuteResponse extends + com.google.protobuf.GeneratedMessage implements + // @@protoc_insertion_point(message_implements:google.spannerlib.v1.ExecuteResponse) + ExecuteResponseOrBuilder { +private static final long serialVersionUID = 0L; + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 32, + /* patch= */ 1, + /* suffix= */ "", + ExecuteResponse.class.getName()); + } + // Use ExecuteResponse.newBuilder() to construct. + private ExecuteResponse(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + } + private ExecuteResponse() { + resultSets_ = java.util.Collections.emptyList(); + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteResponse_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteResponse_fieldAccessorTable + .ensureFieldAccessorsInitialized( + com.google.cloud.spannerlib.v1.ExecuteResponse.class, com.google.cloud.spannerlib.v1.ExecuteResponse.Builder.class); + } + + private int bitField0_; + public static final int ROWS_FIELD_NUMBER = 1; + private com.google.cloud.spannerlib.v1.Rows rows_; + /** + * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * @return Whether the rows field is set. + */ + @java.lang.Override + public boolean hasRows() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * @return The rows. + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.Rows getRows() { + return rows_ == null ? com.google.cloud.spannerlib.v1.Rows.getDefaultInstance() : rows_; + } + /** + * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.RowsOrBuilder getRowsOrBuilder() { + return rows_ == null ? com.google.cloud.spannerlib.v1.Rows.getDefaultInstance() : rows_; + } + + public static final int RESULT_SETS_FIELD_NUMBER = 2; + @SuppressWarnings("serial") + private java.util.List resultSets_; + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + @java.lang.Override + public java.util.List getResultSetsList() { + return resultSets_; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + @java.lang.Override + public java.util.List + getResultSetsOrBuilderList() { + return resultSets_; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + @java.lang.Override + public int getResultSetsCount() { + return resultSets_.size(); + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + @java.lang.Override + public com.google.spanner.v1.ResultSet getResultSets(int index) { + return resultSets_.get(index); + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + @java.lang.Override + public com.google.spanner.v1.ResultSetOrBuilder getResultSetsOrBuilder( + int index) { + return resultSets_.get(index); + } + + public static final int STATUS_FIELD_NUMBER = 3; + private com.google.rpc.Status status_; + /** + * .google.rpc.Status status = 3; + * @return Whether the status field is set. + */ + @java.lang.Override + public boolean hasStatus() { + return ((bitField0_ & 0x00000002) != 0); + } + /** + * .google.rpc.Status status = 3; + * @return The status. + */ + @java.lang.Override + public com.google.rpc.Status getStatus() { + return status_ == null ? com.google.rpc.Status.getDefaultInstance() : status_; + } + /** + * .google.rpc.Status status = 3; + */ + @java.lang.Override + public com.google.rpc.StatusOrBuilder getStatusOrBuilder() { + return status_ == null ? com.google.rpc.Status.getDefaultInstance() : status_; + } + + public static final int HAS_MORE_RESULTS_FIELD_NUMBER = 4; + private boolean hasMoreResults_ = false; + /** + * bool has_more_results = 4; + * @return The hasMoreResults. + */ + @java.lang.Override + public boolean getHasMoreResults() { + return hasMoreResults_; + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + if (((bitField0_ & 0x00000001) != 0)) { + output.writeMessage(1, getRows()); + } + for (int i = 0; i < resultSets_.size(); i++) { + output.writeMessage(2, resultSets_.get(i)); + } + if (((bitField0_ & 0x00000002) != 0)) { + output.writeMessage(3, getStatus()); + } + if (hasMoreResults_ != false) { + output.writeBool(4, hasMoreResults_); + } + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + if (((bitField0_ & 0x00000001) != 0)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(1, getRows()); + } + for (int i = 0; i < resultSets_.size(); i++) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(2, resultSets_.get(i)); + } + if (((bitField0_ & 0x00000002) != 0)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(3, getStatus()); + } + if (hasMoreResults_ != false) { + size += com.google.protobuf.CodedOutputStream + .computeBoolSize(4, hasMoreResults_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof com.google.cloud.spannerlib.v1.ExecuteResponse)) { + return super.equals(obj); + } + com.google.cloud.spannerlib.v1.ExecuteResponse other = (com.google.cloud.spannerlib.v1.ExecuteResponse) obj; + + if (hasRows() != other.hasRows()) return false; + if (hasRows()) { + if (!getRows() + .equals(other.getRows())) return false; + } + if (!getResultSetsList() + .equals(other.getResultSetsList())) return false; + if (hasStatus() != other.hasStatus()) return false; + if (hasStatus()) { + if (!getStatus() + .equals(other.getStatus())) return false; + } + if (getHasMoreResults() + != other.getHasMoreResults()) return false; + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + if (hasRows()) { + hash = (37 * hash) + ROWS_FIELD_NUMBER; + hash = (53 * hash) + getRows().hashCode(); + } + if (getResultSetsCount() > 0) { + hash = (37 * hash) + RESULT_SETS_FIELD_NUMBER; + hash = (53 * hash) + getResultSetsList().hashCode(); + } + if (hasStatus()) { + hash = (37 * hash) + STATUS_FIELD_NUMBER; + hash = (53 * hash) + getStatus().hashCode(); + } + hash = (37 * hash) + HAS_MORE_RESULTS_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashBoolean( + getHasMoreResults()); + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static com.google.cloud.spannerlib.v1.ExecuteResponse parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static com.google.cloud.spannerlib.v1.ExecuteResponse parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static com.google.cloud.spannerlib.v1.ExecuteResponse parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static com.google.cloud.spannerlib.v1.ExecuteResponse parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static com.google.cloud.spannerlib.v1.ExecuteResponse parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static com.google.cloud.spannerlib.v1.ExecuteResponse parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static com.google.cloud.spannerlib.v1.ExecuteResponse parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static com.google.cloud.spannerlib.v1.ExecuteResponse parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static com.google.cloud.spannerlib.v1.ExecuteResponse parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input); + } + + public static com.google.cloud.spannerlib.v1.ExecuteResponse parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static com.google.cloud.spannerlib.v1.ExecuteResponse parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static com.google.cloud.spannerlib.v1.ExecuteResponse parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(com.google.cloud.spannerlib.v1.ExecuteResponse prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code google.spannerlib.v1.ExecuteResponse} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder implements + // @@protoc_insertion_point(builder_implements:google.spannerlib.v1.ExecuteResponse) + com.google.cloud.spannerlib.v1.ExecuteResponseOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteResponse_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteResponse_fieldAccessorTable + .ensureFieldAccessorsInitialized( + com.google.cloud.spannerlib.v1.ExecuteResponse.class, com.google.cloud.spannerlib.v1.ExecuteResponse.Builder.class); + } + + // Construct using com.google.cloud.spannerlib.v1.ExecuteResponse.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage + .alwaysUseFieldBuilders) { + internalGetRowsFieldBuilder(); + internalGetResultSetsFieldBuilder(); + internalGetStatusFieldBuilder(); + } + } + @java.lang.Override + public Builder clear() { + super.clear(); + bitField0_ = 0; + rows_ = null; + if (rowsBuilder_ != null) { + rowsBuilder_.dispose(); + rowsBuilder_ = null; + } + if (resultSetsBuilder_ == null) { + resultSets_ = java.util.Collections.emptyList(); + } else { + resultSets_ = null; + resultSetsBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000002); + status_ = null; + if (statusBuilder_ != null) { + statusBuilder_.dispose(); + statusBuilder_ = null; + } + hasMoreResults_ = false; + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteResponse_descriptor; + } + + @java.lang.Override + public com.google.cloud.spannerlib.v1.ExecuteResponse getDefaultInstanceForType() { + return com.google.cloud.spannerlib.v1.ExecuteResponse.getDefaultInstance(); + } + + @java.lang.Override + public com.google.cloud.spannerlib.v1.ExecuteResponse build() { + com.google.cloud.spannerlib.v1.ExecuteResponse result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public com.google.cloud.spannerlib.v1.ExecuteResponse buildPartial() { + com.google.cloud.spannerlib.v1.ExecuteResponse result = new com.google.cloud.spannerlib.v1.ExecuteResponse(this); + buildPartialRepeatedFields(result); + if (bitField0_ != 0) { buildPartial0(result); } + onBuilt(); + return result; + } + + private void buildPartialRepeatedFields(com.google.cloud.spannerlib.v1.ExecuteResponse result) { + if (resultSetsBuilder_ == null) { + if (((bitField0_ & 0x00000002) != 0)) { + resultSets_ = java.util.Collections.unmodifiableList(resultSets_); + bitField0_ = (bitField0_ & ~0x00000002); + } + result.resultSets_ = resultSets_; + } else { + result.resultSets_ = resultSetsBuilder_.build(); + } + } + + private void buildPartial0(com.google.cloud.spannerlib.v1.ExecuteResponse result) { + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) != 0)) { + result.rows_ = rowsBuilder_ == null + ? rows_ + : rowsBuilder_.build(); + to_bitField0_ |= 0x00000001; + } + if (((from_bitField0_ & 0x00000004) != 0)) { + result.status_ = statusBuilder_ == null + ? status_ + : statusBuilder_.build(); + to_bitField0_ |= 0x00000002; + } + if (((from_bitField0_ & 0x00000008) != 0)) { + result.hasMoreResults_ = hasMoreResults_; + } + result.bitField0_ |= to_bitField0_; + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof com.google.cloud.spannerlib.v1.ExecuteResponse) { + return mergeFrom((com.google.cloud.spannerlib.v1.ExecuteResponse)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(com.google.cloud.spannerlib.v1.ExecuteResponse other) { + if (other == com.google.cloud.spannerlib.v1.ExecuteResponse.getDefaultInstance()) return this; + if (other.hasRows()) { + mergeRows(other.getRows()); + } + if (resultSetsBuilder_ == null) { + if (!other.resultSets_.isEmpty()) { + if (resultSets_.isEmpty()) { + resultSets_ = other.resultSets_; + bitField0_ = (bitField0_ & ~0x00000002); + } else { + ensureResultSetsIsMutable(); + resultSets_.addAll(other.resultSets_); + } + onChanged(); + } + } else { + if (!other.resultSets_.isEmpty()) { + if (resultSetsBuilder_.isEmpty()) { + resultSetsBuilder_.dispose(); + resultSetsBuilder_ = null; + resultSets_ = other.resultSets_; + bitField0_ = (bitField0_ & ~0x00000002); + resultSetsBuilder_ = + com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ? + internalGetResultSetsFieldBuilder() : null; + } else { + resultSetsBuilder_.addAllMessages(other.resultSets_); + } + } + } + if (other.hasStatus()) { + mergeStatus(other.getStatus()); + } + if (other.getHasMoreResults() != false) { + setHasMoreResults(other.getHasMoreResults()); + } + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 10: { + input.readMessage( + internalGetRowsFieldBuilder().getBuilder(), + extensionRegistry); + bitField0_ |= 0x00000001; + break; + } // case 10 + case 18: { + com.google.spanner.v1.ResultSet m = + input.readMessage( + com.google.spanner.v1.ResultSet.parser(), + extensionRegistry); + if (resultSetsBuilder_ == null) { + ensureResultSetsIsMutable(); + resultSets_.add(m); + } else { + resultSetsBuilder_.addMessage(m); + } + break; + } // case 18 + case 26: { + input.readMessage( + internalGetStatusFieldBuilder().getBuilder(), + extensionRegistry); + bitField0_ |= 0x00000004; + break; + } // case 26 + case 32: { + hasMoreResults_ = input.readBool(); + bitField0_ |= 0x00000008; + break; + } // case 32 + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + private int bitField0_; + + private com.google.cloud.spannerlib.v1.Rows rows_; + private com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.Rows, com.google.cloud.spannerlib.v1.Rows.Builder, com.google.cloud.spannerlib.v1.RowsOrBuilder> rowsBuilder_; + /** + * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * @return Whether the rows field is set. + */ + public boolean hasRows() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * @return The rows. + */ + public com.google.cloud.spannerlib.v1.Rows getRows() { + if (rowsBuilder_ == null) { + return rows_ == null ? com.google.cloud.spannerlib.v1.Rows.getDefaultInstance() : rows_; + } else { + return rowsBuilder_.getMessage(); + } + } + /** + * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + */ + public Builder setRows(com.google.cloud.spannerlib.v1.Rows value) { + if (rowsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + rows_ = value; + } else { + rowsBuilder_.setMessage(value); + } + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + */ + public Builder setRows( + com.google.cloud.spannerlib.v1.Rows.Builder builderForValue) { + if (rowsBuilder_ == null) { + rows_ = builderForValue.build(); + } else { + rowsBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + */ + public Builder mergeRows(com.google.cloud.spannerlib.v1.Rows value) { + if (rowsBuilder_ == null) { + if (((bitField0_ & 0x00000001) != 0) && + rows_ != null && + rows_ != com.google.cloud.spannerlib.v1.Rows.getDefaultInstance()) { + getRowsBuilder().mergeFrom(value); + } else { + rows_ = value; + } + } else { + rowsBuilder_.mergeFrom(value); + } + if (rows_ != null) { + bitField0_ |= 0x00000001; + onChanged(); + } + return this; + } + /** + * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + */ + public Builder clearRows() { + bitField0_ = (bitField0_ & ~0x00000001); + rows_ = null; + if (rowsBuilder_ != null) { + rowsBuilder_.dispose(); + rowsBuilder_ = null; + } + onChanged(); + return this; + } + /** + * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + */ + public com.google.cloud.spannerlib.v1.Rows.Builder getRowsBuilder() { + bitField0_ |= 0x00000001; + onChanged(); + return internalGetRowsFieldBuilder().getBuilder(); + } + /** + * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + */ + public com.google.cloud.spannerlib.v1.RowsOrBuilder getRowsOrBuilder() { + if (rowsBuilder_ != null) { + return rowsBuilder_.getMessageOrBuilder(); + } else { + return rows_ == null ? + com.google.cloud.spannerlib.v1.Rows.getDefaultInstance() : rows_; + } + } + /** + * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.Rows, com.google.cloud.spannerlib.v1.Rows.Builder, com.google.cloud.spannerlib.v1.RowsOrBuilder> + internalGetRowsFieldBuilder() { + if (rowsBuilder_ == null) { + rowsBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.Rows, com.google.cloud.spannerlib.v1.Rows.Builder, com.google.cloud.spannerlib.v1.RowsOrBuilder>( + getRows(), + getParentForChildren(), + isClean()); + rows_ = null; + } + return rowsBuilder_; + } + + private java.util.List resultSets_ = + java.util.Collections.emptyList(); + private void ensureResultSetsIsMutable() { + if (!((bitField0_ & 0x00000002) != 0)) { + resultSets_ = new java.util.ArrayList(resultSets_); + bitField0_ |= 0x00000002; + } + } + + private com.google.protobuf.RepeatedFieldBuilder< + com.google.spanner.v1.ResultSet, com.google.spanner.v1.ResultSet.Builder, com.google.spanner.v1.ResultSetOrBuilder> resultSetsBuilder_; + + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + public java.util.List getResultSetsList() { + if (resultSetsBuilder_ == null) { + return java.util.Collections.unmodifiableList(resultSets_); + } else { + return resultSetsBuilder_.getMessageList(); + } + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + public int getResultSetsCount() { + if (resultSetsBuilder_ == null) { + return resultSets_.size(); + } else { + return resultSetsBuilder_.getCount(); + } + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + public com.google.spanner.v1.ResultSet getResultSets(int index) { + if (resultSetsBuilder_ == null) { + return resultSets_.get(index); + } else { + return resultSetsBuilder_.getMessage(index); + } + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + public Builder setResultSets( + int index, com.google.spanner.v1.ResultSet value) { + if (resultSetsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureResultSetsIsMutable(); + resultSets_.set(index, value); + onChanged(); + } else { + resultSetsBuilder_.setMessage(index, value); + } + return this; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + public Builder setResultSets( + int index, com.google.spanner.v1.ResultSet.Builder builderForValue) { + if (resultSetsBuilder_ == null) { + ensureResultSetsIsMutable(); + resultSets_.set(index, builderForValue.build()); + onChanged(); + } else { + resultSetsBuilder_.setMessage(index, builderForValue.build()); + } + return this; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + public Builder addResultSets(com.google.spanner.v1.ResultSet value) { + if (resultSetsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureResultSetsIsMutable(); + resultSets_.add(value); + onChanged(); + } else { + resultSetsBuilder_.addMessage(value); + } + return this; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + public Builder addResultSets( + int index, com.google.spanner.v1.ResultSet value) { + if (resultSetsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureResultSetsIsMutable(); + resultSets_.add(index, value); + onChanged(); + } else { + resultSetsBuilder_.addMessage(index, value); + } + return this; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + public Builder addResultSets( + com.google.spanner.v1.ResultSet.Builder builderForValue) { + if (resultSetsBuilder_ == null) { + ensureResultSetsIsMutable(); + resultSets_.add(builderForValue.build()); + onChanged(); + } else { + resultSetsBuilder_.addMessage(builderForValue.build()); + } + return this; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + public Builder addResultSets( + int index, com.google.spanner.v1.ResultSet.Builder builderForValue) { + if (resultSetsBuilder_ == null) { + ensureResultSetsIsMutable(); + resultSets_.add(index, builderForValue.build()); + onChanged(); + } else { + resultSetsBuilder_.addMessage(index, builderForValue.build()); + } + return this; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + public Builder addAllResultSets( + java.lang.Iterable values) { + if (resultSetsBuilder_ == null) { + ensureResultSetsIsMutable(); + com.google.protobuf.AbstractMessageLite.Builder.addAll( + values, resultSets_); + onChanged(); + } else { + resultSetsBuilder_.addAllMessages(values); + } + return this; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + public Builder clearResultSets() { + if (resultSetsBuilder_ == null) { + resultSets_ = java.util.Collections.emptyList(); + bitField0_ = (bitField0_ & ~0x00000002); + onChanged(); + } else { + resultSetsBuilder_.clear(); + } + return this; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + public Builder removeResultSets(int index) { + if (resultSetsBuilder_ == null) { + ensureResultSetsIsMutable(); + resultSets_.remove(index); + onChanged(); + } else { + resultSetsBuilder_.remove(index); + } + return this; + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + public com.google.spanner.v1.ResultSet.Builder getResultSetsBuilder( + int index) { + return internalGetResultSetsFieldBuilder().getBuilder(index); + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + public com.google.spanner.v1.ResultSetOrBuilder getResultSetsOrBuilder( + int index) { + if (resultSetsBuilder_ == null) { + return resultSets_.get(index); } else { + return resultSetsBuilder_.getMessageOrBuilder(index); + } + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + public java.util.List + getResultSetsOrBuilderList() { + if (resultSetsBuilder_ != null) { + return resultSetsBuilder_.getMessageOrBuilderList(); + } else { + return java.util.Collections.unmodifiableList(resultSets_); + } + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + public com.google.spanner.v1.ResultSet.Builder addResultSetsBuilder() { + return internalGetResultSetsFieldBuilder().addBuilder( + com.google.spanner.v1.ResultSet.getDefaultInstance()); + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + public com.google.spanner.v1.ResultSet.Builder addResultSetsBuilder( + int index) { + return internalGetResultSetsFieldBuilder().addBuilder( + index, com.google.spanner.v1.ResultSet.getDefaultInstance()); + } + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + public java.util.List + getResultSetsBuilderList() { + return internalGetResultSetsFieldBuilder().getBuilderList(); + } + private com.google.protobuf.RepeatedFieldBuilder< + com.google.spanner.v1.ResultSet, com.google.spanner.v1.ResultSet.Builder, com.google.spanner.v1.ResultSetOrBuilder> + internalGetResultSetsFieldBuilder() { + if (resultSetsBuilder_ == null) { + resultSetsBuilder_ = new com.google.protobuf.RepeatedFieldBuilder< + com.google.spanner.v1.ResultSet, com.google.spanner.v1.ResultSet.Builder, com.google.spanner.v1.ResultSetOrBuilder>( + resultSets_, + ((bitField0_ & 0x00000002) != 0), + getParentForChildren(), + isClean()); + resultSets_ = null; + } + return resultSetsBuilder_; + } + + private com.google.rpc.Status status_; + private com.google.protobuf.SingleFieldBuilder< + com.google.rpc.Status, com.google.rpc.Status.Builder, com.google.rpc.StatusOrBuilder> statusBuilder_; + /** + * .google.rpc.Status status = 3; + * @return Whether the status field is set. + */ + public boolean hasStatus() { + return ((bitField0_ & 0x00000004) != 0); + } + /** + * .google.rpc.Status status = 3; + * @return The status. + */ + public com.google.rpc.Status getStatus() { + if (statusBuilder_ == null) { + return status_ == null ? com.google.rpc.Status.getDefaultInstance() : status_; + } else { + return statusBuilder_.getMessage(); + } + } + /** + * .google.rpc.Status status = 3; + */ + public Builder setStatus(com.google.rpc.Status value) { + if (statusBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + status_ = value; + } else { + statusBuilder_.setMessage(value); + } + bitField0_ |= 0x00000004; + onChanged(); + return this; + } + /** + * .google.rpc.Status status = 3; + */ + public Builder setStatus( + com.google.rpc.Status.Builder builderForValue) { + if (statusBuilder_ == null) { + status_ = builderForValue.build(); + } else { + statusBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000004; + onChanged(); + return this; + } + /** + * .google.rpc.Status status = 3; + */ + public Builder mergeStatus(com.google.rpc.Status value) { + if (statusBuilder_ == null) { + if (((bitField0_ & 0x00000004) != 0) && + status_ != null && + status_ != com.google.rpc.Status.getDefaultInstance()) { + getStatusBuilder().mergeFrom(value); + } else { + status_ = value; + } + } else { + statusBuilder_.mergeFrom(value); + } + if (status_ != null) { + bitField0_ |= 0x00000004; + onChanged(); + } + return this; + } + /** + * .google.rpc.Status status = 3; + */ + public Builder clearStatus() { + bitField0_ = (bitField0_ & ~0x00000004); + status_ = null; + if (statusBuilder_ != null) { + statusBuilder_.dispose(); + statusBuilder_ = null; + } + onChanged(); + return this; + } + /** + * .google.rpc.Status status = 3; + */ + public com.google.rpc.Status.Builder getStatusBuilder() { + bitField0_ |= 0x00000004; + onChanged(); + return internalGetStatusFieldBuilder().getBuilder(); + } + /** + * .google.rpc.Status status = 3; + */ + public com.google.rpc.StatusOrBuilder getStatusOrBuilder() { + if (statusBuilder_ != null) { + return statusBuilder_.getMessageOrBuilder(); + } else { + return status_ == null ? + com.google.rpc.Status.getDefaultInstance() : status_; + } + } + /** + * .google.rpc.Status status = 3; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.rpc.Status, com.google.rpc.Status.Builder, com.google.rpc.StatusOrBuilder> + internalGetStatusFieldBuilder() { + if (statusBuilder_ == null) { + statusBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.rpc.Status, com.google.rpc.Status.Builder, com.google.rpc.StatusOrBuilder>( + getStatus(), + getParentForChildren(), + isClean()); + status_ = null; + } + return statusBuilder_; + } + + private boolean hasMoreResults_ ; + /** + * bool has_more_results = 4; + * @return The hasMoreResults. + */ + @java.lang.Override + public boolean getHasMoreResults() { + return hasMoreResults_; + } + /** + * bool has_more_results = 4; + * @param value The hasMoreResults to set. + * @return This builder for chaining. + */ + public Builder setHasMoreResults(boolean value) { + + hasMoreResults_ = value; + bitField0_ |= 0x00000008; + onChanged(); + return this; + } + /** + * bool has_more_results = 4; + * @return This builder for chaining. + */ + public Builder clearHasMoreResults() { + bitField0_ = (bitField0_ & ~0x00000008); + hasMoreResults_ = false; + onChanged(); + return this; + } + + // @@protoc_insertion_point(builder_scope:google.spannerlib.v1.ExecuteResponse) + } + + // @@protoc_insertion_point(class_scope:google.spannerlib.v1.ExecuteResponse) + private static final com.google.cloud.spannerlib.v1.ExecuteResponse DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new com.google.cloud.spannerlib.v1.ExecuteResponse(); + } + + public static com.google.cloud.spannerlib.v1.ExecuteResponse getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public ExecuteResponse parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public com.google.cloud.spannerlib.v1.ExecuteResponse getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + +} + diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteResponseOrBuilder.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteResponseOrBuilder.java new file mode 100644 index 00000000..8872a5d2 --- /dev/null +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteResponseOrBuilder.java @@ -0,0 +1,72 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// NO CHECKED-IN PROTOBUF GENCODE +// source: google/spannerlib/v1/spannerlib.proto +// Protobuf Java Version: 4.32.1 + +package com.google.cloud.spannerlib.v1; + +@com.google.protobuf.Generated +public interface ExecuteResponseOrBuilder extends + // @@protoc_insertion_point(interface_extends:google.spannerlib.v1.ExecuteResponse) + com.google.protobuf.MessageOrBuilder { + + /** + * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * @return Whether the rows field is set. + */ + boolean hasRows(); + /** + * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * @return The rows. + */ + com.google.cloud.spannerlib.v1.Rows getRows(); + /** + * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + */ + com.google.cloud.spannerlib.v1.RowsOrBuilder getRowsOrBuilder(); + + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + java.util.List + getResultSetsList(); + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + com.google.spanner.v1.ResultSet getResultSets(int index); + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + int getResultSetsCount(); + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + java.util.List + getResultSetsOrBuilderList(); + /** + * repeated .google.spanner.v1.ResultSet result_sets = 2; + */ + com.google.spanner.v1.ResultSetOrBuilder getResultSetsOrBuilder( + int index); + + /** + * .google.rpc.Status status = 3; + * @return Whether the status field is set. + */ + boolean hasStatus(); + /** + * .google.rpc.Status status = 3; + * @return The status. + */ + com.google.rpc.Status getStatus(); + /** + * .google.rpc.Status status = 3; + */ + com.google.rpc.StatusOrBuilder getStatusOrBuilder(); + + /** + * bool has_more_results = 4; + * @return The hasMoreResults. + */ + boolean getHasMoreResults(); +} diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteStreamingRequest.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteStreamingRequest.java new file mode 100644 index 00000000..d8bb7d1a --- /dev/null +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteStreamingRequest.java @@ -0,0 +1,744 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// NO CHECKED-IN PROTOBUF GENCODE +// source: google/spannerlib/v1/spannerlib.proto +// Protobuf Java Version: 4.32.1 + +package com.google.cloud.spannerlib.v1; + +/** + * Protobuf type {@code google.spannerlib.v1.ExecuteStreamingRequest} + */ +@com.google.protobuf.Generated +public final class ExecuteStreamingRequest extends + com.google.protobuf.GeneratedMessage implements + // @@protoc_insertion_point(message_implements:google.spannerlib.v1.ExecuteStreamingRequest) + ExecuteStreamingRequestOrBuilder { +private static final long serialVersionUID = 0L; + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 32, + /* patch= */ 1, + /* suffix= */ "", + ExecuteStreamingRequest.class.getName()); + } + // Use ExecuteStreamingRequest.newBuilder() to construct. + private ExecuteStreamingRequest(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + } + private ExecuteStreamingRequest() { + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteStreamingRequest_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteStreamingRequest_fieldAccessorTable + .ensureFieldAccessorsInitialized( + com.google.cloud.spannerlib.v1.ExecuteStreamingRequest.class, com.google.cloud.spannerlib.v1.ExecuteStreamingRequest.Builder.class); + } + + private int bitField0_; + public static final int CONNECTION_FIELD_NUMBER = 1; + private com.google.cloud.spannerlib.v1.Connection connection_; + /** + * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; + * @return Whether the connection field is set. + */ + @java.lang.Override + public boolean hasConnection() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; + * @return The connection. + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.Connection getConnection() { + return connection_ == null ? com.google.cloud.spannerlib.v1.Connection.getDefaultInstance() : connection_; + } + /** + * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.ConnectionOrBuilder getConnectionOrBuilder() { + return connection_ == null ? com.google.cloud.spannerlib.v1.Connection.getDefaultInstance() : connection_; + } + + public static final int EXECUTE_SQL_REQUEST_FIELD_NUMBER = 2; + private com.google.spanner.v1.ExecuteSqlRequest executeSqlRequest_; + /** + * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; + * @return Whether the executeSqlRequest field is set. + */ + @java.lang.Override + public boolean hasExecuteSqlRequest() { + return ((bitField0_ & 0x00000002) != 0); + } + /** + * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; + * @return The executeSqlRequest. + */ + @java.lang.Override + public com.google.spanner.v1.ExecuteSqlRequest getExecuteSqlRequest() { + return executeSqlRequest_ == null ? com.google.spanner.v1.ExecuteSqlRequest.getDefaultInstance() : executeSqlRequest_; + } + /** + * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; + */ + @java.lang.Override + public com.google.spanner.v1.ExecuteSqlRequestOrBuilder getExecuteSqlRequestOrBuilder() { + return executeSqlRequest_ == null ? com.google.spanner.v1.ExecuteSqlRequest.getDefaultInstance() : executeSqlRequest_; + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + if (((bitField0_ & 0x00000001) != 0)) { + output.writeMessage(1, getConnection()); + } + if (((bitField0_ & 0x00000002) != 0)) { + output.writeMessage(2, getExecuteSqlRequest()); + } + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + if (((bitField0_ & 0x00000001) != 0)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(1, getConnection()); + } + if (((bitField0_ & 0x00000002) != 0)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(2, getExecuteSqlRequest()); + } + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof com.google.cloud.spannerlib.v1.ExecuteStreamingRequest)) { + return super.equals(obj); + } + com.google.cloud.spannerlib.v1.ExecuteStreamingRequest other = (com.google.cloud.spannerlib.v1.ExecuteStreamingRequest) obj; + + if (hasConnection() != other.hasConnection()) return false; + if (hasConnection()) { + if (!getConnection() + .equals(other.getConnection())) return false; + } + if (hasExecuteSqlRequest() != other.hasExecuteSqlRequest()) return false; + if (hasExecuteSqlRequest()) { + if (!getExecuteSqlRequest() + .equals(other.getExecuteSqlRequest())) return false; + } + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + if (hasConnection()) { + hash = (37 * hash) + CONNECTION_FIELD_NUMBER; + hash = (53 * hash) + getConnection().hashCode(); + } + if (hasExecuteSqlRequest()) { + hash = (37 * hash) + EXECUTE_SQL_REQUEST_FIELD_NUMBER; + hash = (53 * hash) + getExecuteSqlRequest().hashCode(); + } + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input); + } + + public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(com.google.cloud.spannerlib.v1.ExecuteStreamingRequest prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code google.spannerlib.v1.ExecuteStreamingRequest} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder implements + // @@protoc_insertion_point(builder_implements:google.spannerlib.v1.ExecuteStreamingRequest) + com.google.cloud.spannerlib.v1.ExecuteStreamingRequestOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteStreamingRequest_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteStreamingRequest_fieldAccessorTable + .ensureFieldAccessorsInitialized( + com.google.cloud.spannerlib.v1.ExecuteStreamingRequest.class, com.google.cloud.spannerlib.v1.ExecuteStreamingRequest.Builder.class); + } + + // Construct using com.google.cloud.spannerlib.v1.ExecuteStreamingRequest.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage + .alwaysUseFieldBuilders) { + internalGetConnectionFieldBuilder(); + internalGetExecuteSqlRequestFieldBuilder(); + } + } + @java.lang.Override + public Builder clear() { + super.clear(); + bitField0_ = 0; + connection_ = null; + if (connectionBuilder_ != null) { + connectionBuilder_.dispose(); + connectionBuilder_ = null; + } + executeSqlRequest_ = null; + if (executeSqlRequestBuilder_ != null) { + executeSqlRequestBuilder_.dispose(); + executeSqlRequestBuilder_ = null; + } + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteStreamingRequest_descriptor; + } + + @java.lang.Override + public com.google.cloud.spannerlib.v1.ExecuteStreamingRequest getDefaultInstanceForType() { + return com.google.cloud.spannerlib.v1.ExecuteStreamingRequest.getDefaultInstance(); + } + + @java.lang.Override + public com.google.cloud.spannerlib.v1.ExecuteStreamingRequest build() { + com.google.cloud.spannerlib.v1.ExecuteStreamingRequest result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public com.google.cloud.spannerlib.v1.ExecuteStreamingRequest buildPartial() { + com.google.cloud.spannerlib.v1.ExecuteStreamingRequest result = new com.google.cloud.spannerlib.v1.ExecuteStreamingRequest(this); + if (bitField0_ != 0) { buildPartial0(result); } + onBuilt(); + return result; + } + + private void buildPartial0(com.google.cloud.spannerlib.v1.ExecuteStreamingRequest result) { + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) != 0)) { + result.connection_ = connectionBuilder_ == null + ? connection_ + : connectionBuilder_.build(); + to_bitField0_ |= 0x00000001; + } + if (((from_bitField0_ & 0x00000002) != 0)) { + result.executeSqlRequest_ = executeSqlRequestBuilder_ == null + ? executeSqlRequest_ + : executeSqlRequestBuilder_.build(); + to_bitField0_ |= 0x00000002; + } + result.bitField0_ |= to_bitField0_; + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof com.google.cloud.spannerlib.v1.ExecuteStreamingRequest) { + return mergeFrom((com.google.cloud.spannerlib.v1.ExecuteStreamingRequest)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(com.google.cloud.spannerlib.v1.ExecuteStreamingRequest other) { + if (other == com.google.cloud.spannerlib.v1.ExecuteStreamingRequest.getDefaultInstance()) return this; + if (other.hasConnection()) { + mergeConnection(other.getConnection()); + } + if (other.hasExecuteSqlRequest()) { + mergeExecuteSqlRequest(other.getExecuteSqlRequest()); + } + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 10: { + input.readMessage( + internalGetConnectionFieldBuilder().getBuilder(), + extensionRegistry); + bitField0_ |= 0x00000001; + break; + } // case 10 + case 18: { + input.readMessage( + internalGetExecuteSqlRequestFieldBuilder().getBuilder(), + extensionRegistry); + bitField0_ |= 0x00000002; + break; + } // case 18 + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + private int bitField0_; + + private com.google.cloud.spannerlib.v1.Connection connection_; + private com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.Connection, com.google.cloud.spannerlib.v1.Connection.Builder, com.google.cloud.spannerlib.v1.ConnectionOrBuilder> connectionBuilder_; + /** + * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; + * @return Whether the connection field is set. + */ + public boolean hasConnection() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; + * @return The connection. + */ + public com.google.cloud.spannerlib.v1.Connection getConnection() { + if (connectionBuilder_ == null) { + return connection_ == null ? com.google.cloud.spannerlib.v1.Connection.getDefaultInstance() : connection_; + } else { + return connectionBuilder_.getMessage(); + } + } + /** + * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; + */ + public Builder setConnection(com.google.cloud.spannerlib.v1.Connection value) { + if (connectionBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + connection_ = value; + } else { + connectionBuilder_.setMessage(value); + } + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; + */ + public Builder setConnection( + com.google.cloud.spannerlib.v1.Connection.Builder builderForValue) { + if (connectionBuilder_ == null) { + connection_ = builderForValue.build(); + } else { + connectionBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; + */ + public Builder mergeConnection(com.google.cloud.spannerlib.v1.Connection value) { + if (connectionBuilder_ == null) { + if (((bitField0_ & 0x00000001) != 0) && + connection_ != null && + connection_ != com.google.cloud.spannerlib.v1.Connection.getDefaultInstance()) { + getConnectionBuilder().mergeFrom(value); + } else { + connection_ = value; + } + } else { + connectionBuilder_.mergeFrom(value); + } + if (connection_ != null) { + bitField0_ |= 0x00000001; + onChanged(); + } + return this; + } + /** + * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; + */ + public Builder clearConnection() { + bitField0_ = (bitField0_ & ~0x00000001); + connection_ = null; + if (connectionBuilder_ != null) { + connectionBuilder_.dispose(); + connectionBuilder_ = null; + } + onChanged(); + return this; + } + /** + * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; + */ + public com.google.cloud.spannerlib.v1.Connection.Builder getConnectionBuilder() { + bitField0_ |= 0x00000001; + onChanged(); + return internalGetConnectionFieldBuilder().getBuilder(); + } + /** + * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; + */ + public com.google.cloud.spannerlib.v1.ConnectionOrBuilder getConnectionOrBuilder() { + if (connectionBuilder_ != null) { + return connectionBuilder_.getMessageOrBuilder(); + } else { + return connection_ == null ? + com.google.cloud.spannerlib.v1.Connection.getDefaultInstance() : connection_; + } + } + /** + * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.Connection, com.google.cloud.spannerlib.v1.Connection.Builder, com.google.cloud.spannerlib.v1.ConnectionOrBuilder> + internalGetConnectionFieldBuilder() { + if (connectionBuilder_ == null) { + connectionBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.Connection, com.google.cloud.spannerlib.v1.Connection.Builder, com.google.cloud.spannerlib.v1.ConnectionOrBuilder>( + getConnection(), + getParentForChildren(), + isClean()); + connection_ = null; + } + return connectionBuilder_; + } + + private com.google.spanner.v1.ExecuteSqlRequest executeSqlRequest_; + private com.google.protobuf.SingleFieldBuilder< + com.google.spanner.v1.ExecuteSqlRequest, com.google.spanner.v1.ExecuteSqlRequest.Builder, com.google.spanner.v1.ExecuteSqlRequestOrBuilder> executeSqlRequestBuilder_; + /** + * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; + * @return Whether the executeSqlRequest field is set. + */ + public boolean hasExecuteSqlRequest() { + return ((bitField0_ & 0x00000002) != 0); + } + /** + * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; + * @return The executeSqlRequest. + */ + public com.google.spanner.v1.ExecuteSqlRequest getExecuteSqlRequest() { + if (executeSqlRequestBuilder_ == null) { + return executeSqlRequest_ == null ? com.google.spanner.v1.ExecuteSqlRequest.getDefaultInstance() : executeSqlRequest_; + } else { + return executeSqlRequestBuilder_.getMessage(); + } + } + /** + * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; + */ + public Builder setExecuteSqlRequest(com.google.spanner.v1.ExecuteSqlRequest value) { + if (executeSqlRequestBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + executeSqlRequest_ = value; + } else { + executeSqlRequestBuilder_.setMessage(value); + } + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + /** + * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; + */ + public Builder setExecuteSqlRequest( + com.google.spanner.v1.ExecuteSqlRequest.Builder builderForValue) { + if (executeSqlRequestBuilder_ == null) { + executeSqlRequest_ = builderForValue.build(); + } else { + executeSqlRequestBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + /** + * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; + */ + public Builder mergeExecuteSqlRequest(com.google.spanner.v1.ExecuteSqlRequest value) { + if (executeSqlRequestBuilder_ == null) { + if (((bitField0_ & 0x00000002) != 0) && + executeSqlRequest_ != null && + executeSqlRequest_ != com.google.spanner.v1.ExecuteSqlRequest.getDefaultInstance()) { + getExecuteSqlRequestBuilder().mergeFrom(value); + } else { + executeSqlRequest_ = value; + } + } else { + executeSqlRequestBuilder_.mergeFrom(value); + } + if (executeSqlRequest_ != null) { + bitField0_ |= 0x00000002; + onChanged(); + } + return this; + } + /** + * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; + */ + public Builder clearExecuteSqlRequest() { + bitField0_ = (bitField0_ & ~0x00000002); + executeSqlRequest_ = null; + if (executeSqlRequestBuilder_ != null) { + executeSqlRequestBuilder_.dispose(); + executeSqlRequestBuilder_ = null; + } + onChanged(); + return this; + } + /** + * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; + */ + public com.google.spanner.v1.ExecuteSqlRequest.Builder getExecuteSqlRequestBuilder() { + bitField0_ |= 0x00000002; + onChanged(); + return internalGetExecuteSqlRequestFieldBuilder().getBuilder(); + } + /** + * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; + */ + public com.google.spanner.v1.ExecuteSqlRequestOrBuilder getExecuteSqlRequestOrBuilder() { + if (executeSqlRequestBuilder_ != null) { + return executeSqlRequestBuilder_.getMessageOrBuilder(); + } else { + return executeSqlRequest_ == null ? + com.google.spanner.v1.ExecuteSqlRequest.getDefaultInstance() : executeSqlRequest_; + } + } + /** + * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.spanner.v1.ExecuteSqlRequest, com.google.spanner.v1.ExecuteSqlRequest.Builder, com.google.spanner.v1.ExecuteSqlRequestOrBuilder> + internalGetExecuteSqlRequestFieldBuilder() { + if (executeSqlRequestBuilder_ == null) { + executeSqlRequestBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.spanner.v1.ExecuteSqlRequest, com.google.spanner.v1.ExecuteSqlRequest.Builder, com.google.spanner.v1.ExecuteSqlRequestOrBuilder>( + getExecuteSqlRequest(), + getParentForChildren(), + isClean()); + executeSqlRequest_ = null; + } + return executeSqlRequestBuilder_; + } + + // @@protoc_insertion_point(builder_scope:google.spannerlib.v1.ExecuteStreamingRequest) + } + + // @@protoc_insertion_point(class_scope:google.spannerlib.v1.ExecuteStreamingRequest) + private static final com.google.cloud.spannerlib.v1.ExecuteStreamingRequest DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new com.google.cloud.spannerlib.v1.ExecuteStreamingRequest(); + } + + public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public ExecuteStreamingRequest parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public com.google.cloud.spannerlib.v1.ExecuteStreamingRequest getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + +} + diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteStreamingRequestOrBuilder.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteStreamingRequestOrBuilder.java new file mode 100644 index 00000000..50e68339 --- /dev/null +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteStreamingRequestOrBuilder.java @@ -0,0 +1,42 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// NO CHECKED-IN PROTOBUF GENCODE +// source: google/spannerlib/v1/spannerlib.proto +// Protobuf Java Version: 4.32.1 + +package com.google.cloud.spannerlib.v1; + +@com.google.protobuf.Generated +public interface ExecuteStreamingRequestOrBuilder extends + // @@protoc_insertion_point(interface_extends:google.spannerlib.v1.ExecuteStreamingRequest) + com.google.protobuf.MessageOrBuilder { + + /** + * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; + * @return Whether the connection field is set. + */ + boolean hasConnection(); + /** + * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; + * @return The connection. + */ + com.google.cloud.spannerlib.v1.Connection getConnection(); + /** + * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; + */ + com.google.cloud.spannerlib.v1.ConnectionOrBuilder getConnectionOrBuilder(); + + /** + * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; + * @return Whether the executeSqlRequest field is set. + */ + boolean hasExecuteSqlRequest(); + /** + * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; + * @return The executeSqlRequest. + */ + com.google.spanner.v1.ExecuteSqlRequest getExecuteSqlRequest(); + /** + * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; + */ + com.google.spanner.v1.ExecuteSqlRequestOrBuilder getExecuteSqlRequestOrBuilder(); +} diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/FetchOptions.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/FetchOptions.java new file mode 100644 index 00000000..2a60d958 --- /dev/null +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/FetchOptions.java @@ -0,0 +1,500 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// NO CHECKED-IN PROTOBUF GENCODE +// source: google/spannerlib/v1/spannerlib.proto +// Protobuf Java Version: 4.32.1 + +package com.google.cloud.spannerlib.v1; + +/** + * Protobuf type {@code google.spannerlib.v1.FetchOptions} + */ +@com.google.protobuf.Generated +public final class FetchOptions extends + com.google.protobuf.GeneratedMessage implements + // @@protoc_insertion_point(message_implements:google.spannerlib.v1.FetchOptions) + FetchOptionsOrBuilder { +private static final long serialVersionUID = 0L; + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 32, + /* patch= */ 1, + /* suffix= */ "", + FetchOptions.class.getName()); + } + // Use FetchOptions.newBuilder() to construct. + private FetchOptions(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + } + private FetchOptions() { + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_FetchOptions_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_FetchOptions_fieldAccessorTable + .ensureFieldAccessorsInitialized( + com.google.cloud.spannerlib.v1.FetchOptions.class, com.google.cloud.spannerlib.v1.FetchOptions.Builder.class); + } + + public static final int NUM_ROWS_FIELD_NUMBER = 1; + private long numRows_ = 0L; + /** + * int64 num_rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * @return The numRows. + */ + @java.lang.Override + public long getNumRows() { + return numRows_; + } + + public static final int ENCODING_FIELD_NUMBER = 2; + private long encoding_ = 0L; + /** + * int64 encoding = 2 [(.google.api.field_behavior) = REQUIRED]; + * @return The encoding. + */ + @java.lang.Override + public long getEncoding() { + return encoding_; + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + if (numRows_ != 0L) { + output.writeInt64(1, numRows_); + } + if (encoding_ != 0L) { + output.writeInt64(2, encoding_); + } + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + if (numRows_ != 0L) { + size += com.google.protobuf.CodedOutputStream + .computeInt64Size(1, numRows_); + } + if (encoding_ != 0L) { + size += com.google.protobuf.CodedOutputStream + .computeInt64Size(2, encoding_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof com.google.cloud.spannerlib.v1.FetchOptions)) { + return super.equals(obj); + } + com.google.cloud.spannerlib.v1.FetchOptions other = (com.google.cloud.spannerlib.v1.FetchOptions) obj; + + if (getNumRows() + != other.getNumRows()) return false; + if (getEncoding() + != other.getEncoding()) return false; + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + hash = (37 * hash) + NUM_ROWS_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashLong( + getNumRows()); + hash = (37 * hash) + ENCODING_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashLong( + getEncoding()); + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static com.google.cloud.spannerlib.v1.FetchOptions parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static com.google.cloud.spannerlib.v1.FetchOptions parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static com.google.cloud.spannerlib.v1.FetchOptions parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static com.google.cloud.spannerlib.v1.FetchOptions parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static com.google.cloud.spannerlib.v1.FetchOptions parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static com.google.cloud.spannerlib.v1.FetchOptions parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static com.google.cloud.spannerlib.v1.FetchOptions parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static com.google.cloud.spannerlib.v1.FetchOptions parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static com.google.cloud.spannerlib.v1.FetchOptions parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input); + } + + public static com.google.cloud.spannerlib.v1.FetchOptions parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static com.google.cloud.spannerlib.v1.FetchOptions parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static com.google.cloud.spannerlib.v1.FetchOptions parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(com.google.cloud.spannerlib.v1.FetchOptions prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code google.spannerlib.v1.FetchOptions} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder implements + // @@protoc_insertion_point(builder_implements:google.spannerlib.v1.FetchOptions) + com.google.cloud.spannerlib.v1.FetchOptionsOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_FetchOptions_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_FetchOptions_fieldAccessorTable + .ensureFieldAccessorsInitialized( + com.google.cloud.spannerlib.v1.FetchOptions.class, com.google.cloud.spannerlib.v1.FetchOptions.Builder.class); + } + + // Construct using com.google.cloud.spannerlib.v1.FetchOptions.newBuilder() + private Builder() { + + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + + } + @java.lang.Override + public Builder clear() { + super.clear(); + bitField0_ = 0; + numRows_ = 0L; + encoding_ = 0L; + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_FetchOptions_descriptor; + } + + @java.lang.Override + public com.google.cloud.spannerlib.v1.FetchOptions getDefaultInstanceForType() { + return com.google.cloud.spannerlib.v1.FetchOptions.getDefaultInstance(); + } + + @java.lang.Override + public com.google.cloud.spannerlib.v1.FetchOptions build() { + com.google.cloud.spannerlib.v1.FetchOptions result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public com.google.cloud.spannerlib.v1.FetchOptions buildPartial() { + com.google.cloud.spannerlib.v1.FetchOptions result = new com.google.cloud.spannerlib.v1.FetchOptions(this); + if (bitField0_ != 0) { buildPartial0(result); } + onBuilt(); + return result; + } + + private void buildPartial0(com.google.cloud.spannerlib.v1.FetchOptions result) { + int from_bitField0_ = bitField0_; + if (((from_bitField0_ & 0x00000001) != 0)) { + result.numRows_ = numRows_; + } + if (((from_bitField0_ & 0x00000002) != 0)) { + result.encoding_ = encoding_; + } + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof com.google.cloud.spannerlib.v1.FetchOptions) { + return mergeFrom((com.google.cloud.spannerlib.v1.FetchOptions)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(com.google.cloud.spannerlib.v1.FetchOptions other) { + if (other == com.google.cloud.spannerlib.v1.FetchOptions.getDefaultInstance()) return this; + if (other.getNumRows() != 0L) { + setNumRows(other.getNumRows()); + } + if (other.getEncoding() != 0L) { + setEncoding(other.getEncoding()); + } + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 8: { + numRows_ = input.readInt64(); + bitField0_ |= 0x00000001; + break; + } // case 8 + case 16: { + encoding_ = input.readInt64(); + bitField0_ |= 0x00000002; + break; + } // case 16 + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + private int bitField0_; + + private long numRows_ ; + /** + * int64 num_rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * @return The numRows. + */ + @java.lang.Override + public long getNumRows() { + return numRows_; + } + /** + * int64 num_rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * @param value The numRows to set. + * @return This builder for chaining. + */ + public Builder setNumRows(long value) { + + numRows_ = value; + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + * int64 num_rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * @return This builder for chaining. + */ + public Builder clearNumRows() { + bitField0_ = (bitField0_ & ~0x00000001); + numRows_ = 0L; + onChanged(); + return this; + } + + private long encoding_ ; + /** + * int64 encoding = 2 [(.google.api.field_behavior) = REQUIRED]; + * @return The encoding. + */ + @java.lang.Override + public long getEncoding() { + return encoding_; + } + /** + * int64 encoding = 2 [(.google.api.field_behavior) = REQUIRED]; + * @param value The encoding to set. + * @return This builder for chaining. + */ + public Builder setEncoding(long value) { + + encoding_ = value; + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + /** + * int64 encoding = 2 [(.google.api.field_behavior) = REQUIRED]; + * @return This builder for chaining. + */ + public Builder clearEncoding() { + bitField0_ = (bitField0_ & ~0x00000002); + encoding_ = 0L; + onChanged(); + return this; + } + + // @@protoc_insertion_point(builder_scope:google.spannerlib.v1.FetchOptions) + } + + // @@protoc_insertion_point(class_scope:google.spannerlib.v1.FetchOptions) + private static final com.google.cloud.spannerlib.v1.FetchOptions DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new com.google.cloud.spannerlib.v1.FetchOptions(); + } + + public static com.google.cloud.spannerlib.v1.FetchOptions getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public FetchOptions parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public com.google.cloud.spannerlib.v1.FetchOptions getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + +} + diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/FetchOptionsOrBuilder.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/FetchOptionsOrBuilder.java new file mode 100644 index 00000000..23844684 --- /dev/null +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/FetchOptionsOrBuilder.java @@ -0,0 +1,24 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// NO CHECKED-IN PROTOBUF GENCODE +// source: google/spannerlib/v1/spannerlib.proto +// Protobuf Java Version: 4.32.1 + +package com.google.cloud.spannerlib.v1; + +@com.google.protobuf.Generated +public interface FetchOptionsOrBuilder extends + // @@protoc_insertion_point(interface_extends:google.spannerlib.v1.FetchOptions) + com.google.protobuf.MessageOrBuilder { + + /** + * int64 num_rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * @return The numRows. + */ + long getNumRows(); + + /** + * int64 encoding = 2 [(.google.api.field_behavior) = REQUIRED]; + * @return The encoding. + */ + long getEncoding(); +} diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextRequest.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextRequest.java index 9decc52c..3af8e7ba 100644 --- a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextRequest.java +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextRequest.java @@ -70,26 +70,30 @@ public com.google.cloud.spannerlib.v1.RowsOrBuilder getRowsOrBuilder() { return rows_ == null ? com.google.cloud.spannerlib.v1.Rows.getDefaultInstance() : rows_; } - public static final int NUM_ROWS_FIELD_NUMBER = 2; - private long numRows_ = 0L; + public static final int FETCH_OPTIONS_FIELD_NUMBER = 2; + private com.google.cloud.spannerlib.v1.FetchOptions fetchOptions_; /** - * int64 num_rows = 2 [(.google.api.field_behavior) = REQUIRED]; - * @return The numRows. + * .google.spannerlib.v1.FetchOptions fetch_options = 2 [(.google.api.field_behavior) = REQUIRED]; + * @return Whether the fetchOptions field is set. */ @java.lang.Override - public long getNumRows() { - return numRows_; + public boolean hasFetchOptions() { + return ((bitField0_ & 0x00000002) != 0); + } + /** + * .google.spannerlib.v1.FetchOptions fetch_options = 2 [(.google.api.field_behavior) = REQUIRED]; + * @return The fetchOptions. + */ + @java.lang.Override + public com.google.cloud.spannerlib.v1.FetchOptions getFetchOptions() { + return fetchOptions_ == null ? com.google.cloud.spannerlib.v1.FetchOptions.getDefaultInstance() : fetchOptions_; } - - public static final int ENCODING_FIELD_NUMBER = 3; - private long encoding_ = 0L; /** - * int64 encoding = 3 [(.google.api.field_behavior) = REQUIRED]; - * @return The encoding. + * .google.spannerlib.v1.FetchOptions fetch_options = 2 [(.google.api.field_behavior) = REQUIRED]; */ @java.lang.Override - public long getEncoding() { - return encoding_; + public com.google.cloud.spannerlib.v1.FetchOptionsOrBuilder getFetchOptionsOrBuilder() { + return fetchOptions_ == null ? com.google.cloud.spannerlib.v1.FetchOptions.getDefaultInstance() : fetchOptions_; } private byte memoizedIsInitialized = -1; @@ -109,11 +113,8 @@ public void writeTo(com.google.protobuf.CodedOutputStream output) if (((bitField0_ & 0x00000001) != 0)) { output.writeMessage(1, getRows()); } - if (numRows_ != 0L) { - output.writeInt64(2, numRows_); - } - if (encoding_ != 0L) { - output.writeInt64(3, encoding_); + if (((bitField0_ & 0x00000002) != 0)) { + output.writeMessage(2, getFetchOptions()); } getUnknownFields().writeTo(output); } @@ -128,13 +129,9 @@ public int getSerializedSize() { size += com.google.protobuf.CodedOutputStream .computeMessageSize(1, getRows()); } - if (numRows_ != 0L) { - size += com.google.protobuf.CodedOutputStream - .computeInt64Size(2, numRows_); - } - if (encoding_ != 0L) { + if (((bitField0_ & 0x00000002) != 0)) { size += com.google.protobuf.CodedOutputStream - .computeInt64Size(3, encoding_); + .computeMessageSize(2, getFetchOptions()); } size += getUnknownFields().getSerializedSize(); memoizedSize = size; @@ -156,10 +153,11 @@ public boolean equals(final java.lang.Object obj) { if (!getRows() .equals(other.getRows())) return false; } - if (getNumRows() - != other.getNumRows()) return false; - if (getEncoding() - != other.getEncoding()) return false; + if (hasFetchOptions() != other.hasFetchOptions()) return false; + if (hasFetchOptions()) { + if (!getFetchOptions() + .equals(other.getFetchOptions())) return false; + } if (!getUnknownFields().equals(other.getUnknownFields())) return false; return true; } @@ -175,12 +173,10 @@ public int hashCode() { hash = (37 * hash) + ROWS_FIELD_NUMBER; hash = (53 * hash) + getRows().hashCode(); } - hash = (37 * hash) + NUM_ROWS_FIELD_NUMBER; - hash = (53 * hash) + com.google.protobuf.Internal.hashLong( - getNumRows()); - hash = (37 * hash) + ENCODING_FIELD_NUMBER; - hash = (53 * hash) + com.google.protobuf.Internal.hashLong( - getEncoding()); + if (hasFetchOptions()) { + hash = (37 * hash) + FETCH_OPTIONS_FIELD_NUMBER; + hash = (53 * hash) + getFetchOptions().hashCode(); + } hash = (29 * hash) + getUnknownFields().hashCode(); memoizedHashCode = hash; return hash; @@ -312,6 +308,7 @@ private void maybeForceBuilderInitialization() { if (com.google.protobuf.GeneratedMessage .alwaysUseFieldBuilders) { internalGetRowsFieldBuilder(); + internalGetFetchOptionsFieldBuilder(); } } @java.lang.Override @@ -323,8 +320,11 @@ public Builder clear() { rowsBuilder_.dispose(); rowsBuilder_ = null; } - numRows_ = 0L; - encoding_ = 0L; + fetchOptions_ = null; + if (fetchOptionsBuilder_ != null) { + fetchOptionsBuilder_.dispose(); + fetchOptionsBuilder_ = null; + } return this; } @@ -366,10 +366,10 @@ private void buildPartial0(com.google.cloud.spannerlib.v1.NextRequest result) { to_bitField0_ |= 0x00000001; } if (((from_bitField0_ & 0x00000002) != 0)) { - result.numRows_ = numRows_; - } - if (((from_bitField0_ & 0x00000004) != 0)) { - result.encoding_ = encoding_; + result.fetchOptions_ = fetchOptionsBuilder_ == null + ? fetchOptions_ + : fetchOptionsBuilder_.build(); + to_bitField0_ |= 0x00000002; } result.bitField0_ |= to_bitField0_; } @@ -389,11 +389,8 @@ public Builder mergeFrom(com.google.cloud.spannerlib.v1.NextRequest other) { if (other.hasRows()) { mergeRows(other.getRows()); } - if (other.getNumRows() != 0L) { - setNumRows(other.getNumRows()); - } - if (other.getEncoding() != 0L) { - setEncoding(other.getEncoding()); + if (other.hasFetchOptions()) { + mergeFetchOptions(other.getFetchOptions()); } this.mergeUnknownFields(other.getUnknownFields()); onChanged(); @@ -428,16 +425,13 @@ public Builder mergeFrom( bitField0_ |= 0x00000001; break; } // case 10 - case 16: { - numRows_ = input.readInt64(); + case 18: { + input.readMessage( + internalGetFetchOptionsFieldBuilder().getBuilder(), + extensionRegistry); bitField0_ |= 0x00000002; break; - } // case 16 - case 24: { - encoding_ = input.readInt64(); - bitField0_ |= 0x00000004; - break; - } // case 24 + } // case 18 default: { if (!super.parseUnknownField(input, extensionRegistry, tag)) { done = true; // was an endgroup tag @@ -576,68 +570,125 @@ public com.google.cloud.spannerlib.v1.RowsOrBuilder getRowsOrBuilder() { return rowsBuilder_; } - private long numRows_ ; + private com.google.cloud.spannerlib.v1.FetchOptions fetchOptions_; + private com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.FetchOptions, com.google.cloud.spannerlib.v1.FetchOptions.Builder, com.google.cloud.spannerlib.v1.FetchOptionsOrBuilder> fetchOptionsBuilder_; /** - * int64 num_rows = 2 [(.google.api.field_behavior) = REQUIRED]; - * @return The numRows. + * .google.spannerlib.v1.FetchOptions fetch_options = 2 [(.google.api.field_behavior) = REQUIRED]; + * @return Whether the fetchOptions field is set. */ - @java.lang.Override - public long getNumRows() { - return numRows_; + public boolean hasFetchOptions() { + return ((bitField0_ & 0x00000002) != 0); } /** - * int64 num_rows = 2 [(.google.api.field_behavior) = REQUIRED]; - * @param value The numRows to set. - * @return This builder for chaining. + * .google.spannerlib.v1.FetchOptions fetch_options = 2 [(.google.api.field_behavior) = REQUIRED]; + * @return The fetchOptions. */ - public Builder setNumRows(long value) { - - numRows_ = value; + public com.google.cloud.spannerlib.v1.FetchOptions getFetchOptions() { + if (fetchOptionsBuilder_ == null) { + return fetchOptions_ == null ? com.google.cloud.spannerlib.v1.FetchOptions.getDefaultInstance() : fetchOptions_; + } else { + return fetchOptionsBuilder_.getMessage(); + } + } + /** + * .google.spannerlib.v1.FetchOptions fetch_options = 2 [(.google.api.field_behavior) = REQUIRED]; + */ + public Builder setFetchOptions(com.google.cloud.spannerlib.v1.FetchOptions value) { + if (fetchOptionsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + fetchOptions_ = value; + } else { + fetchOptionsBuilder_.setMessage(value); + } bitField0_ |= 0x00000002; onChanged(); return this; } /** - * int64 num_rows = 2 [(.google.api.field_behavior) = REQUIRED]; - * @return This builder for chaining. + * .google.spannerlib.v1.FetchOptions fetch_options = 2 [(.google.api.field_behavior) = REQUIRED]; */ - public Builder clearNumRows() { - bitField0_ = (bitField0_ & ~0x00000002); - numRows_ = 0L; + public Builder setFetchOptions( + com.google.cloud.spannerlib.v1.FetchOptions.Builder builderForValue) { + if (fetchOptionsBuilder_ == null) { + fetchOptions_ = builderForValue.build(); + } else { + fetchOptionsBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000002; onChanged(); return this; } - - private long encoding_ ; /** - * int64 encoding = 3 [(.google.api.field_behavior) = REQUIRED]; - * @return The encoding. + * .google.spannerlib.v1.FetchOptions fetch_options = 2 [(.google.api.field_behavior) = REQUIRED]; */ - @java.lang.Override - public long getEncoding() { - return encoding_; + public Builder mergeFetchOptions(com.google.cloud.spannerlib.v1.FetchOptions value) { + if (fetchOptionsBuilder_ == null) { + if (((bitField0_ & 0x00000002) != 0) && + fetchOptions_ != null && + fetchOptions_ != com.google.cloud.spannerlib.v1.FetchOptions.getDefaultInstance()) { + getFetchOptionsBuilder().mergeFrom(value); + } else { + fetchOptions_ = value; + } + } else { + fetchOptionsBuilder_.mergeFrom(value); + } + if (fetchOptions_ != null) { + bitField0_ |= 0x00000002; + onChanged(); + } + return this; } /** - * int64 encoding = 3 [(.google.api.field_behavior) = REQUIRED]; - * @param value The encoding to set. - * @return This builder for chaining. + * .google.spannerlib.v1.FetchOptions fetch_options = 2 [(.google.api.field_behavior) = REQUIRED]; */ - public Builder setEncoding(long value) { - - encoding_ = value; - bitField0_ |= 0x00000004; + public Builder clearFetchOptions() { + bitField0_ = (bitField0_ & ~0x00000002); + fetchOptions_ = null; + if (fetchOptionsBuilder_ != null) { + fetchOptionsBuilder_.dispose(); + fetchOptionsBuilder_ = null; + } onChanged(); return this; } /** - * int64 encoding = 3 [(.google.api.field_behavior) = REQUIRED]; - * @return This builder for chaining. + * .google.spannerlib.v1.FetchOptions fetch_options = 2 [(.google.api.field_behavior) = REQUIRED]; */ - public Builder clearEncoding() { - bitField0_ = (bitField0_ & ~0x00000004); - encoding_ = 0L; + public com.google.cloud.spannerlib.v1.FetchOptions.Builder getFetchOptionsBuilder() { + bitField0_ |= 0x00000002; onChanged(); - return this; + return internalGetFetchOptionsFieldBuilder().getBuilder(); + } + /** + * .google.spannerlib.v1.FetchOptions fetch_options = 2 [(.google.api.field_behavior) = REQUIRED]; + */ + public com.google.cloud.spannerlib.v1.FetchOptionsOrBuilder getFetchOptionsOrBuilder() { + if (fetchOptionsBuilder_ != null) { + return fetchOptionsBuilder_.getMessageOrBuilder(); + } else { + return fetchOptions_ == null ? + com.google.cloud.spannerlib.v1.FetchOptions.getDefaultInstance() : fetchOptions_; + } + } + /** + * .google.spannerlib.v1.FetchOptions fetch_options = 2 [(.google.api.field_behavior) = REQUIRED]; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.FetchOptions, com.google.cloud.spannerlib.v1.FetchOptions.Builder, com.google.cloud.spannerlib.v1.FetchOptionsOrBuilder> + internalGetFetchOptionsFieldBuilder() { + if (fetchOptionsBuilder_ == null) { + fetchOptionsBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.cloud.spannerlib.v1.FetchOptions, com.google.cloud.spannerlib.v1.FetchOptions.Builder, com.google.cloud.spannerlib.v1.FetchOptionsOrBuilder>( + getFetchOptions(), + getParentForChildren(), + isClean()); + fetchOptions_ = null; + } + return fetchOptionsBuilder_; } // @@protoc_insertion_point(builder_scope:google.spannerlib.v1.NextRequest) diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextRequestOrBuilder.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextRequestOrBuilder.java index 664bed22..567fc52a 100644 --- a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextRequestOrBuilder.java +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextRequestOrBuilder.java @@ -26,14 +26,17 @@ public interface NextRequestOrBuilder extends com.google.cloud.spannerlib.v1.RowsOrBuilder getRowsOrBuilder(); /** - * int64 num_rows = 2 [(.google.api.field_behavior) = REQUIRED]; - * @return The numRows. + * .google.spannerlib.v1.FetchOptions fetch_options = 2 [(.google.api.field_behavior) = REQUIRED]; + * @return Whether the fetchOptions field is set. */ - long getNumRows(); - + boolean hasFetchOptions(); + /** + * .google.spannerlib.v1.FetchOptions fetch_options = 2 [(.google.api.field_behavior) = REQUIRED]; + * @return The fetchOptions. + */ + com.google.cloud.spannerlib.v1.FetchOptions getFetchOptions(); /** - * int64 encoding = 3 [(.google.api.field_behavior) = REQUIRED]; - * @return The encoding. + * .google.spannerlib.v1.FetchOptions fetch_options = 2 [(.google.api.field_behavior) = REQUIRED]; */ - long getEncoding(); + com.google.cloud.spannerlib.v1.FetchOptionsOrBuilder getFetchOptionsOrBuilder(); } diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextResultSetResponse.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextResultSetResponse.java new file mode 100644 index 00000000..7d8d65bc --- /dev/null +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextResultSetResponse.java @@ -0,0 +1,626 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// NO CHECKED-IN PROTOBUF GENCODE +// source: google/spannerlib/v1/spannerlib.proto +// Protobuf Java Version: 4.32.1 + +package com.google.cloud.spannerlib.v1; + +/** + * Protobuf type {@code google.spannerlib.v1.NextResultSetResponse} + */ +@com.google.protobuf.Generated +public final class NextResultSetResponse extends + com.google.protobuf.GeneratedMessage implements + // @@protoc_insertion_point(message_implements:google.spannerlib.v1.NextResultSetResponse) + NextResultSetResponseOrBuilder { +private static final long serialVersionUID = 0L; + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 32, + /* patch= */ 1, + /* suffix= */ "", + NextResultSetResponse.class.getName()); + } + // Use NextResultSetResponse.newBuilder() to construct. + private NextResultSetResponse(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + } + private NextResultSetResponse() { + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_NextResultSetResponse_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_NextResultSetResponse_fieldAccessorTable + .ensureFieldAccessorsInitialized( + com.google.cloud.spannerlib.v1.NextResultSetResponse.class, com.google.cloud.spannerlib.v1.NextResultSetResponse.Builder.class); + } + + private int bitField0_; + public static final int HAS_MORE_RESULTS_FIELD_NUMBER = 1; + private boolean hasMoreResults_ = false; + /** + * bool has_more_results = 1; + * @return The hasMoreResults. + */ + @java.lang.Override + public boolean getHasMoreResults() { + return hasMoreResults_; + } + + public static final int METADATA_FIELD_NUMBER = 2; + private com.google.spanner.v1.ResultSetMetadata metadata_; + /** + * .google.spanner.v1.ResultSetMetadata metadata = 2; + * @return Whether the metadata field is set. + */ + @java.lang.Override + public boolean hasMetadata() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + * .google.spanner.v1.ResultSetMetadata metadata = 2; + * @return The metadata. + */ + @java.lang.Override + public com.google.spanner.v1.ResultSetMetadata getMetadata() { + return metadata_ == null ? com.google.spanner.v1.ResultSetMetadata.getDefaultInstance() : metadata_; + } + /** + * .google.spanner.v1.ResultSetMetadata metadata = 2; + */ + @java.lang.Override + public com.google.spanner.v1.ResultSetMetadataOrBuilder getMetadataOrBuilder() { + return metadata_ == null ? com.google.spanner.v1.ResultSetMetadata.getDefaultInstance() : metadata_; + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + if (hasMoreResults_ != false) { + output.writeBool(1, hasMoreResults_); + } + if (((bitField0_ & 0x00000001) != 0)) { + output.writeMessage(2, getMetadata()); + } + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + if (hasMoreResults_ != false) { + size += com.google.protobuf.CodedOutputStream + .computeBoolSize(1, hasMoreResults_); + } + if (((bitField0_ & 0x00000001) != 0)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(2, getMetadata()); + } + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof com.google.cloud.spannerlib.v1.NextResultSetResponse)) { + return super.equals(obj); + } + com.google.cloud.spannerlib.v1.NextResultSetResponse other = (com.google.cloud.spannerlib.v1.NextResultSetResponse) obj; + + if (getHasMoreResults() + != other.getHasMoreResults()) return false; + if (hasMetadata() != other.hasMetadata()) return false; + if (hasMetadata()) { + if (!getMetadata() + .equals(other.getMetadata())) return false; + } + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + hash = (37 * hash) + HAS_MORE_RESULTS_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashBoolean( + getHasMoreResults()); + if (hasMetadata()) { + hash = (37 * hash) + METADATA_FIELD_NUMBER; + hash = (53 * hash) + getMetadata().hashCode(); + } + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input); + } + + public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(com.google.cloud.spannerlib.v1.NextResultSetResponse prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code google.spannerlib.v1.NextResultSetResponse} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder implements + // @@protoc_insertion_point(builder_implements:google.spannerlib.v1.NextResultSetResponse) + com.google.cloud.spannerlib.v1.NextResultSetResponseOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_NextResultSetResponse_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_NextResultSetResponse_fieldAccessorTable + .ensureFieldAccessorsInitialized( + com.google.cloud.spannerlib.v1.NextResultSetResponse.class, com.google.cloud.spannerlib.v1.NextResultSetResponse.Builder.class); + } + + // Construct using com.google.cloud.spannerlib.v1.NextResultSetResponse.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage + .alwaysUseFieldBuilders) { + internalGetMetadataFieldBuilder(); + } + } + @java.lang.Override + public Builder clear() { + super.clear(); + bitField0_ = 0; + hasMoreResults_ = false; + metadata_ = null; + if (metadataBuilder_ != null) { + metadataBuilder_.dispose(); + metadataBuilder_ = null; + } + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_NextResultSetResponse_descriptor; + } + + @java.lang.Override + public com.google.cloud.spannerlib.v1.NextResultSetResponse getDefaultInstanceForType() { + return com.google.cloud.spannerlib.v1.NextResultSetResponse.getDefaultInstance(); + } + + @java.lang.Override + public com.google.cloud.spannerlib.v1.NextResultSetResponse build() { + com.google.cloud.spannerlib.v1.NextResultSetResponse result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public com.google.cloud.spannerlib.v1.NextResultSetResponse buildPartial() { + com.google.cloud.spannerlib.v1.NextResultSetResponse result = new com.google.cloud.spannerlib.v1.NextResultSetResponse(this); + if (bitField0_ != 0) { buildPartial0(result); } + onBuilt(); + return result; + } + + private void buildPartial0(com.google.cloud.spannerlib.v1.NextResultSetResponse result) { + int from_bitField0_ = bitField0_; + if (((from_bitField0_ & 0x00000001) != 0)) { + result.hasMoreResults_ = hasMoreResults_; + } + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000002) != 0)) { + result.metadata_ = metadataBuilder_ == null + ? metadata_ + : metadataBuilder_.build(); + to_bitField0_ |= 0x00000001; + } + result.bitField0_ |= to_bitField0_; + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof com.google.cloud.spannerlib.v1.NextResultSetResponse) { + return mergeFrom((com.google.cloud.spannerlib.v1.NextResultSetResponse)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(com.google.cloud.spannerlib.v1.NextResultSetResponse other) { + if (other == com.google.cloud.spannerlib.v1.NextResultSetResponse.getDefaultInstance()) return this; + if (other.getHasMoreResults() != false) { + setHasMoreResults(other.getHasMoreResults()); + } + if (other.hasMetadata()) { + mergeMetadata(other.getMetadata()); + } + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 8: { + hasMoreResults_ = input.readBool(); + bitField0_ |= 0x00000001; + break; + } // case 8 + case 18: { + input.readMessage( + internalGetMetadataFieldBuilder().getBuilder(), + extensionRegistry); + bitField0_ |= 0x00000002; + break; + } // case 18 + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + private int bitField0_; + + private boolean hasMoreResults_ ; + /** + * bool has_more_results = 1; + * @return The hasMoreResults. + */ + @java.lang.Override + public boolean getHasMoreResults() { + return hasMoreResults_; + } + /** + * bool has_more_results = 1; + * @param value The hasMoreResults to set. + * @return This builder for chaining. + */ + public Builder setHasMoreResults(boolean value) { + + hasMoreResults_ = value; + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + * bool has_more_results = 1; + * @return This builder for chaining. + */ + public Builder clearHasMoreResults() { + bitField0_ = (bitField0_ & ~0x00000001); + hasMoreResults_ = false; + onChanged(); + return this; + } + + private com.google.spanner.v1.ResultSetMetadata metadata_; + private com.google.protobuf.SingleFieldBuilder< + com.google.spanner.v1.ResultSetMetadata, com.google.spanner.v1.ResultSetMetadata.Builder, com.google.spanner.v1.ResultSetMetadataOrBuilder> metadataBuilder_; + /** + * .google.spanner.v1.ResultSetMetadata metadata = 2; + * @return Whether the metadata field is set. + */ + public boolean hasMetadata() { + return ((bitField0_ & 0x00000002) != 0); + } + /** + * .google.spanner.v1.ResultSetMetadata metadata = 2; + * @return The metadata. + */ + public com.google.spanner.v1.ResultSetMetadata getMetadata() { + if (metadataBuilder_ == null) { + return metadata_ == null ? com.google.spanner.v1.ResultSetMetadata.getDefaultInstance() : metadata_; + } else { + return metadataBuilder_.getMessage(); + } + } + /** + * .google.spanner.v1.ResultSetMetadata metadata = 2; + */ + public Builder setMetadata(com.google.spanner.v1.ResultSetMetadata value) { + if (metadataBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + metadata_ = value; + } else { + metadataBuilder_.setMessage(value); + } + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + /** + * .google.spanner.v1.ResultSetMetadata metadata = 2; + */ + public Builder setMetadata( + com.google.spanner.v1.ResultSetMetadata.Builder builderForValue) { + if (metadataBuilder_ == null) { + metadata_ = builderForValue.build(); + } else { + metadataBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + /** + * .google.spanner.v1.ResultSetMetadata metadata = 2; + */ + public Builder mergeMetadata(com.google.spanner.v1.ResultSetMetadata value) { + if (metadataBuilder_ == null) { + if (((bitField0_ & 0x00000002) != 0) && + metadata_ != null && + metadata_ != com.google.spanner.v1.ResultSetMetadata.getDefaultInstance()) { + getMetadataBuilder().mergeFrom(value); + } else { + metadata_ = value; + } + } else { + metadataBuilder_.mergeFrom(value); + } + if (metadata_ != null) { + bitField0_ |= 0x00000002; + onChanged(); + } + return this; + } + /** + * .google.spanner.v1.ResultSetMetadata metadata = 2; + */ + public Builder clearMetadata() { + bitField0_ = (bitField0_ & ~0x00000002); + metadata_ = null; + if (metadataBuilder_ != null) { + metadataBuilder_.dispose(); + metadataBuilder_ = null; + } + onChanged(); + return this; + } + /** + * .google.spanner.v1.ResultSetMetadata metadata = 2; + */ + public com.google.spanner.v1.ResultSetMetadata.Builder getMetadataBuilder() { + bitField0_ |= 0x00000002; + onChanged(); + return internalGetMetadataFieldBuilder().getBuilder(); + } + /** + * .google.spanner.v1.ResultSetMetadata metadata = 2; + */ + public com.google.spanner.v1.ResultSetMetadataOrBuilder getMetadataOrBuilder() { + if (metadataBuilder_ != null) { + return metadataBuilder_.getMessageOrBuilder(); + } else { + return metadata_ == null ? + com.google.spanner.v1.ResultSetMetadata.getDefaultInstance() : metadata_; + } + } + /** + * .google.spanner.v1.ResultSetMetadata metadata = 2; + */ + private com.google.protobuf.SingleFieldBuilder< + com.google.spanner.v1.ResultSetMetadata, com.google.spanner.v1.ResultSetMetadata.Builder, com.google.spanner.v1.ResultSetMetadataOrBuilder> + internalGetMetadataFieldBuilder() { + if (metadataBuilder_ == null) { + metadataBuilder_ = new com.google.protobuf.SingleFieldBuilder< + com.google.spanner.v1.ResultSetMetadata, com.google.spanner.v1.ResultSetMetadata.Builder, com.google.spanner.v1.ResultSetMetadataOrBuilder>( + getMetadata(), + getParentForChildren(), + isClean()); + metadata_ = null; + } + return metadataBuilder_; + } + + // @@protoc_insertion_point(builder_scope:google.spannerlib.v1.NextResultSetResponse) + } + + // @@protoc_insertion_point(class_scope:google.spannerlib.v1.NextResultSetResponse) + private static final com.google.cloud.spannerlib.v1.NextResultSetResponse DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new com.google.cloud.spannerlib.v1.NextResultSetResponse(); + } + + public static com.google.cloud.spannerlib.v1.NextResultSetResponse getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public NextResultSetResponse parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public com.google.cloud.spannerlib.v1.NextResultSetResponse getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + +} + diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextResultSetResponseOrBuilder.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextResultSetResponseOrBuilder.java new file mode 100644 index 00000000..e9bdc409 --- /dev/null +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextResultSetResponseOrBuilder.java @@ -0,0 +1,33 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// NO CHECKED-IN PROTOBUF GENCODE +// source: google/spannerlib/v1/spannerlib.proto +// Protobuf Java Version: 4.32.1 + +package com.google.cloud.spannerlib.v1; + +@com.google.protobuf.Generated +public interface NextResultSetResponseOrBuilder extends + // @@protoc_insertion_point(interface_extends:google.spannerlib.v1.NextResultSetResponse) + com.google.protobuf.MessageOrBuilder { + + /** + * bool has_more_results = 1; + * @return The hasMoreResults. + */ + boolean getHasMoreResults(); + + /** + * .google.spanner.v1.ResultSetMetadata metadata = 2; + * @return Whether the metadata field is set. + */ + boolean hasMetadata(); + /** + * .google.spanner.v1.ResultSetMetadata metadata = 2; + * @return The metadata. + */ + com.google.spanner.v1.ResultSetMetadata getMetadata(); + /** + * .google.spanner.v1.ResultSetMetadata metadata = 2; + */ + com.google.spanner.v1.ResultSetMetadataOrBuilder getMetadataOrBuilder(); +} diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/RowData.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/RowData.java index 974c3f93..8f699345 100644 --- a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/RowData.java +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/RowData.java @@ -28,6 +28,7 @@ private RowData(com.google.protobuf.GeneratedMessage.Builder builder) { super(builder); } private RowData() { + requestId_ = ""; data_ = java.util.Collections.emptyList(); } @@ -45,10 +46,49 @@ private RowData() { } private int bitField0_; - public static final int ROWS_FIELD_NUMBER = 1; + public static final int REQUEST_ID_FIELD_NUMBER = 1; + @SuppressWarnings("serial") + private volatile java.lang.Object requestId_ = ""; + /** + * string request_id = 1; + * @return The requestId. + */ + @java.lang.Override + public java.lang.String getRequestId() { + java.lang.Object ref = requestId_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + requestId_ = s; + return s; + } + } + /** + * string request_id = 1; + * @return The bytes for requestId. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getRequestIdBytes() { + java.lang.Object ref = requestId_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + requestId_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + public static final int ROWS_FIELD_NUMBER = 2; private com.google.cloud.spannerlib.v1.Rows rows_; /** - * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * .google.spannerlib.v1.Rows rows = 2 [(.google.api.field_behavior) = REQUIRED]; * @return Whether the rows field is set. */ @java.lang.Override @@ -56,7 +96,7 @@ public boolean hasRows() { return ((bitField0_ & 0x00000001) != 0); } /** - * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * .google.spannerlib.v1.Rows rows = 2 [(.google.api.field_behavior) = REQUIRED]; * @return The rows. */ @java.lang.Override @@ -64,17 +104,17 @@ public com.google.cloud.spannerlib.v1.Rows getRows() { return rows_ == null ? com.google.cloud.spannerlib.v1.Rows.getDefaultInstance() : rows_; } /** - * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * .google.spannerlib.v1.Rows rows = 2 [(.google.api.field_behavior) = REQUIRED]; */ @java.lang.Override public com.google.cloud.spannerlib.v1.RowsOrBuilder getRowsOrBuilder() { return rows_ == null ? com.google.cloud.spannerlib.v1.Rows.getDefaultInstance() : rows_; } - public static final int METADATA_FIELD_NUMBER = 2; + public static final int METADATA_FIELD_NUMBER = 3; private com.google.spanner.v1.ResultSetMetadata metadata_; /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; + * .google.spanner.v1.ResultSetMetadata metadata = 3; * @return Whether the metadata field is set. */ @java.lang.Override @@ -82,7 +122,7 @@ public boolean hasMetadata() { return ((bitField0_ & 0x00000002) != 0); } /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; + * .google.spanner.v1.ResultSetMetadata metadata = 3; * @return The metadata. */ @java.lang.Override @@ -90,25 +130,25 @@ public com.google.spanner.v1.ResultSetMetadata getMetadata() { return metadata_ == null ? com.google.spanner.v1.ResultSetMetadata.getDefaultInstance() : metadata_; } /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; + * .google.spanner.v1.ResultSetMetadata metadata = 3; */ @java.lang.Override public com.google.spanner.v1.ResultSetMetadataOrBuilder getMetadataOrBuilder() { return metadata_ == null ? com.google.spanner.v1.ResultSetMetadata.getDefaultInstance() : metadata_; } - public static final int DATA_FIELD_NUMBER = 3; + public static final int DATA_FIELD_NUMBER = 4; @SuppressWarnings("serial") private java.util.List data_; /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ @java.lang.Override public java.util.List getDataList() { return data_; } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ @java.lang.Override public java.util.List @@ -116,21 +156,21 @@ public java.util.List getDataList() { return data_; } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ @java.lang.Override public int getDataCount() { return data_.size(); } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ @java.lang.Override public com.google.protobuf.ListValue getData(int index) { return data_.get(index); } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ @java.lang.Override public com.google.protobuf.ListValueOrBuilder getDataOrBuilder( @@ -138,10 +178,10 @@ public com.google.protobuf.ListValueOrBuilder getDataOrBuilder( return data_.get(index); } - public static final int STATS_FIELD_NUMBER = 4; + public static final int STATS_FIELD_NUMBER = 5; private com.google.spanner.v1.ResultSetStats stats_; /** - * .google.spanner.v1.ResultSetStats stats = 4; + * .google.spanner.v1.ResultSetStats stats = 5; * @return Whether the stats field is set. */ @java.lang.Override @@ -149,7 +189,7 @@ public boolean hasStats() { return ((bitField0_ & 0x00000004) != 0); } /** - * .google.spanner.v1.ResultSetStats stats = 4; + * .google.spanner.v1.ResultSetStats stats = 5; * @return The stats. */ @java.lang.Override @@ -157,17 +197,17 @@ public com.google.spanner.v1.ResultSetStats getStats() { return stats_ == null ? com.google.spanner.v1.ResultSetStats.getDefaultInstance() : stats_; } /** - * .google.spanner.v1.ResultSetStats stats = 4; + * .google.spanner.v1.ResultSetStats stats = 5; */ @java.lang.Override public com.google.spanner.v1.ResultSetStatsOrBuilder getStatsOrBuilder() { return stats_ == null ? com.google.spanner.v1.ResultSetStats.getDefaultInstance() : stats_; } - public static final int HAS_MORE_RESULTS_FIELD_NUMBER = 5; + public static final int HAS_MORE_RESULTS_FIELD_NUMBER = 6; private boolean hasMoreResults_ = false; /** - * bool has_more_results = 5; + * bool has_more_results = 6; * @return The hasMoreResults. */ @java.lang.Override @@ -189,20 +229,23 @@ public final boolean isInitialized() { @java.lang.Override public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io.IOException { + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(requestId_)) { + com.google.protobuf.GeneratedMessage.writeString(output, 1, requestId_); + } if (((bitField0_ & 0x00000001) != 0)) { - output.writeMessage(1, getRows()); + output.writeMessage(2, getRows()); } if (((bitField0_ & 0x00000002) != 0)) { - output.writeMessage(2, getMetadata()); + output.writeMessage(3, getMetadata()); } for (int i = 0; i < data_.size(); i++) { - output.writeMessage(3, data_.get(i)); + output.writeMessage(4, data_.get(i)); } if (((bitField0_ & 0x00000004) != 0)) { - output.writeMessage(4, getStats()); + output.writeMessage(5, getStats()); } if (hasMoreResults_ != false) { - output.writeBool(5, hasMoreResults_); + output.writeBool(6, hasMoreResults_); } getUnknownFields().writeTo(output); } @@ -213,25 +256,28 @@ public int getSerializedSize() { if (size != -1) return size; size = 0; + if (!com.google.protobuf.GeneratedMessage.isStringEmpty(requestId_)) { + size += com.google.protobuf.GeneratedMessage.computeStringSize(1, requestId_); + } if (((bitField0_ & 0x00000001) != 0)) { size += com.google.protobuf.CodedOutputStream - .computeMessageSize(1, getRows()); + .computeMessageSize(2, getRows()); } if (((bitField0_ & 0x00000002) != 0)) { size += com.google.protobuf.CodedOutputStream - .computeMessageSize(2, getMetadata()); + .computeMessageSize(3, getMetadata()); } for (int i = 0; i < data_.size(); i++) { size += com.google.protobuf.CodedOutputStream - .computeMessageSize(3, data_.get(i)); + .computeMessageSize(4, data_.get(i)); } if (((bitField0_ & 0x00000004) != 0)) { size += com.google.protobuf.CodedOutputStream - .computeMessageSize(4, getStats()); + .computeMessageSize(5, getStats()); } if (hasMoreResults_ != false) { size += com.google.protobuf.CodedOutputStream - .computeBoolSize(5, hasMoreResults_); + .computeBoolSize(6, hasMoreResults_); } size += getUnknownFields().getSerializedSize(); memoizedSize = size; @@ -248,6 +294,8 @@ public boolean equals(final java.lang.Object obj) { } com.google.cloud.spannerlib.v1.RowData other = (com.google.cloud.spannerlib.v1.RowData) obj; + if (!getRequestId() + .equals(other.getRequestId())) return false; if (hasRows() != other.hasRows()) return false; if (hasRows()) { if (!getRows() @@ -278,6 +326,8 @@ public int hashCode() { } int hash = 41; hash = (19 * hash) + getDescriptor().hashCode(); + hash = (37 * hash) + REQUEST_ID_FIELD_NUMBER; + hash = (53 * hash) + getRequestId().hashCode(); if (hasRows()) { hash = (37 * hash) + ROWS_FIELD_NUMBER; hash = (53 * hash) + getRows().hashCode(); @@ -437,6 +487,7 @@ private void maybeForceBuilderInitialization() { public Builder clear() { super.clear(); bitField0_ = 0; + requestId_ = ""; rows_ = null; if (rowsBuilder_ != null) { rowsBuilder_.dispose(); @@ -453,7 +504,7 @@ public Builder clear() { data_ = null; dataBuilder_.clear(); } - bitField0_ = (bitField0_ & ~0x00000004); + bitField0_ = (bitField0_ & ~0x00000008); stats_ = null; if (statsBuilder_ != null) { statsBuilder_.dispose(); @@ -494,9 +545,9 @@ public com.google.cloud.spannerlib.v1.RowData buildPartial() { private void buildPartialRepeatedFields(com.google.cloud.spannerlib.v1.RowData result) { if (dataBuilder_ == null) { - if (((bitField0_ & 0x00000004) != 0)) { + if (((bitField0_ & 0x00000008) != 0)) { data_ = java.util.Collections.unmodifiableList(data_); - bitField0_ = (bitField0_ & ~0x00000004); + bitField0_ = (bitField0_ & ~0x00000008); } result.data_ = data_; } else { @@ -506,26 +557,29 @@ private void buildPartialRepeatedFields(com.google.cloud.spannerlib.v1.RowData r private void buildPartial0(com.google.cloud.spannerlib.v1.RowData result) { int from_bitField0_ = bitField0_; - int to_bitField0_ = 0; if (((from_bitField0_ & 0x00000001) != 0)) { + result.requestId_ = requestId_; + } + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000002) != 0)) { result.rows_ = rowsBuilder_ == null ? rows_ : rowsBuilder_.build(); to_bitField0_ |= 0x00000001; } - if (((from_bitField0_ & 0x00000002) != 0)) { + if (((from_bitField0_ & 0x00000004) != 0)) { result.metadata_ = metadataBuilder_ == null ? metadata_ : metadataBuilder_.build(); to_bitField0_ |= 0x00000002; } - if (((from_bitField0_ & 0x00000008) != 0)) { + if (((from_bitField0_ & 0x00000010) != 0)) { result.stats_ = statsBuilder_ == null ? stats_ : statsBuilder_.build(); to_bitField0_ |= 0x00000004; } - if (((from_bitField0_ & 0x00000010) != 0)) { + if (((from_bitField0_ & 0x00000020) != 0)) { result.hasMoreResults_ = hasMoreResults_; } result.bitField0_ |= to_bitField0_; @@ -543,6 +597,11 @@ public Builder mergeFrom(com.google.protobuf.Message other) { public Builder mergeFrom(com.google.cloud.spannerlib.v1.RowData other) { if (other == com.google.cloud.spannerlib.v1.RowData.getDefaultInstance()) return this; + if (!other.getRequestId().isEmpty()) { + requestId_ = other.requestId_; + bitField0_ |= 0x00000001; + onChanged(); + } if (other.hasRows()) { mergeRows(other.getRows()); } @@ -553,7 +612,7 @@ public Builder mergeFrom(com.google.cloud.spannerlib.v1.RowData other) { if (!other.data_.isEmpty()) { if (data_.isEmpty()) { data_ = other.data_; - bitField0_ = (bitField0_ & ~0x00000004); + bitField0_ = (bitField0_ & ~0x00000008); } else { ensureDataIsMutable(); data_.addAll(other.data_); @@ -566,7 +625,7 @@ public Builder mergeFrom(com.google.cloud.spannerlib.v1.RowData other) { dataBuilder_.dispose(); dataBuilder_ = null; data_ = other.data_; - bitField0_ = (bitField0_ & ~0x00000004); + bitField0_ = (bitField0_ & ~0x00000008); dataBuilder_ = com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ? internalGetDataFieldBuilder() : null; @@ -608,20 +667,25 @@ public Builder mergeFrom( done = true; break; case 10: { - input.readMessage( - internalGetRowsFieldBuilder().getBuilder(), - extensionRegistry); + requestId_ = input.readStringRequireUtf8(); bitField0_ |= 0x00000001; break; } // case 10 case 18: { input.readMessage( - internalGetMetadataFieldBuilder().getBuilder(), + internalGetRowsFieldBuilder().getBuilder(), extensionRegistry); bitField0_ |= 0x00000002; break; } // case 18 case 26: { + input.readMessage( + internalGetMetadataFieldBuilder().getBuilder(), + extensionRegistry); + bitField0_ |= 0x00000004; + break; + } // case 26 + case 34: { com.google.protobuf.ListValue m = input.readMessage( com.google.protobuf.ListValue.parser(), @@ -633,19 +697,19 @@ public Builder mergeFrom( dataBuilder_.addMessage(m); } break; - } // case 26 - case 34: { + } // case 34 + case 42: { input.readMessage( internalGetStatsFieldBuilder().getBuilder(), extensionRegistry); - bitField0_ |= 0x00000008; + bitField0_ |= 0x00000010; break; - } // case 34 - case 40: { + } // case 42 + case 48: { hasMoreResults_ = input.readBool(); - bitField0_ |= 0x00000010; + bitField0_ |= 0x00000020; break; - } // case 40 + } // case 48 default: { if (!super.parseUnknownField(input, extensionRegistry, tag)) { done = true; // was an endgroup tag @@ -663,18 +727,90 @@ public Builder mergeFrom( } private int bitField0_; + private java.lang.Object requestId_ = ""; + /** + * string request_id = 1; + * @return The requestId. + */ + public java.lang.String getRequestId() { + java.lang.Object ref = requestId_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + requestId_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * string request_id = 1; + * @return The bytes for requestId. + */ + public com.google.protobuf.ByteString + getRequestIdBytes() { + java.lang.Object ref = requestId_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + requestId_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * string request_id = 1; + * @param value The requestId to set. + * @return This builder for chaining. + */ + public Builder setRequestId( + java.lang.String value) { + if (value == null) { throw new NullPointerException(); } + requestId_ = value; + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + * string request_id = 1; + * @return This builder for chaining. + */ + public Builder clearRequestId() { + requestId_ = getDefaultInstance().getRequestId(); + bitField0_ = (bitField0_ & ~0x00000001); + onChanged(); + return this; + } + /** + * string request_id = 1; + * @param value The bytes for requestId to set. + * @return This builder for chaining. + */ + public Builder setRequestIdBytes( + com.google.protobuf.ByteString value) { + if (value == null) { throw new NullPointerException(); } + checkByteStringIsUtf8(value); + requestId_ = value; + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + private com.google.cloud.spannerlib.v1.Rows rows_; private com.google.protobuf.SingleFieldBuilder< com.google.cloud.spannerlib.v1.Rows, com.google.cloud.spannerlib.v1.Rows.Builder, com.google.cloud.spannerlib.v1.RowsOrBuilder> rowsBuilder_; /** - * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * .google.spannerlib.v1.Rows rows = 2 [(.google.api.field_behavior) = REQUIRED]; * @return Whether the rows field is set. */ public boolean hasRows() { - return ((bitField0_ & 0x00000001) != 0); + return ((bitField0_ & 0x00000002) != 0); } /** - * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * .google.spannerlib.v1.Rows rows = 2 [(.google.api.field_behavior) = REQUIRED]; * @return The rows. */ public com.google.cloud.spannerlib.v1.Rows getRows() { @@ -685,7 +821,7 @@ public com.google.cloud.spannerlib.v1.Rows getRows() { } } /** - * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * .google.spannerlib.v1.Rows rows = 2 [(.google.api.field_behavior) = REQUIRED]; */ public Builder setRows(com.google.cloud.spannerlib.v1.Rows value) { if (rowsBuilder_ == null) { @@ -696,12 +832,12 @@ public Builder setRows(com.google.cloud.spannerlib.v1.Rows value) { } else { rowsBuilder_.setMessage(value); } - bitField0_ |= 0x00000001; + bitField0_ |= 0x00000002; onChanged(); return this; } /** - * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * .google.spannerlib.v1.Rows rows = 2 [(.google.api.field_behavior) = REQUIRED]; */ public Builder setRows( com.google.cloud.spannerlib.v1.Rows.Builder builderForValue) { @@ -710,16 +846,16 @@ public Builder setRows( } else { rowsBuilder_.setMessage(builderForValue.build()); } - bitField0_ |= 0x00000001; + bitField0_ |= 0x00000002; onChanged(); return this; } /** - * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * .google.spannerlib.v1.Rows rows = 2 [(.google.api.field_behavior) = REQUIRED]; */ public Builder mergeRows(com.google.cloud.spannerlib.v1.Rows value) { if (rowsBuilder_ == null) { - if (((bitField0_ & 0x00000001) != 0) && + if (((bitField0_ & 0x00000002) != 0) && rows_ != null && rows_ != com.google.cloud.spannerlib.v1.Rows.getDefaultInstance()) { getRowsBuilder().mergeFrom(value); @@ -730,16 +866,16 @@ public Builder mergeRows(com.google.cloud.spannerlib.v1.Rows value) { rowsBuilder_.mergeFrom(value); } if (rows_ != null) { - bitField0_ |= 0x00000001; + bitField0_ |= 0x00000002; onChanged(); } return this; } /** - * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * .google.spannerlib.v1.Rows rows = 2 [(.google.api.field_behavior) = REQUIRED]; */ public Builder clearRows() { - bitField0_ = (bitField0_ & ~0x00000001); + bitField0_ = (bitField0_ & ~0x00000002); rows_ = null; if (rowsBuilder_ != null) { rowsBuilder_.dispose(); @@ -749,15 +885,15 @@ public Builder clearRows() { return this; } /** - * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * .google.spannerlib.v1.Rows rows = 2 [(.google.api.field_behavior) = REQUIRED]; */ public com.google.cloud.spannerlib.v1.Rows.Builder getRowsBuilder() { - bitField0_ |= 0x00000001; + bitField0_ |= 0x00000002; onChanged(); return internalGetRowsFieldBuilder().getBuilder(); } /** - * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * .google.spannerlib.v1.Rows rows = 2 [(.google.api.field_behavior) = REQUIRED]; */ public com.google.cloud.spannerlib.v1.RowsOrBuilder getRowsOrBuilder() { if (rowsBuilder_ != null) { @@ -768,7 +904,7 @@ public com.google.cloud.spannerlib.v1.RowsOrBuilder getRowsOrBuilder() { } } /** - * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * .google.spannerlib.v1.Rows rows = 2 [(.google.api.field_behavior) = REQUIRED]; */ private com.google.protobuf.SingleFieldBuilder< com.google.cloud.spannerlib.v1.Rows, com.google.cloud.spannerlib.v1.Rows.Builder, com.google.cloud.spannerlib.v1.RowsOrBuilder> @@ -788,14 +924,14 @@ public com.google.cloud.spannerlib.v1.RowsOrBuilder getRowsOrBuilder() { private com.google.protobuf.SingleFieldBuilder< com.google.spanner.v1.ResultSetMetadata, com.google.spanner.v1.ResultSetMetadata.Builder, com.google.spanner.v1.ResultSetMetadataOrBuilder> metadataBuilder_; /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; + * .google.spanner.v1.ResultSetMetadata metadata = 3; * @return Whether the metadata field is set. */ public boolean hasMetadata() { - return ((bitField0_ & 0x00000002) != 0); + return ((bitField0_ & 0x00000004) != 0); } /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; + * .google.spanner.v1.ResultSetMetadata metadata = 3; * @return The metadata. */ public com.google.spanner.v1.ResultSetMetadata getMetadata() { @@ -806,7 +942,7 @@ public com.google.spanner.v1.ResultSetMetadata getMetadata() { } } /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; + * .google.spanner.v1.ResultSetMetadata metadata = 3; */ public Builder setMetadata(com.google.spanner.v1.ResultSetMetadata value) { if (metadataBuilder_ == null) { @@ -817,12 +953,12 @@ public Builder setMetadata(com.google.spanner.v1.ResultSetMetadata value) { } else { metadataBuilder_.setMessage(value); } - bitField0_ |= 0x00000002; + bitField0_ |= 0x00000004; onChanged(); return this; } /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; + * .google.spanner.v1.ResultSetMetadata metadata = 3; */ public Builder setMetadata( com.google.spanner.v1.ResultSetMetadata.Builder builderForValue) { @@ -831,16 +967,16 @@ public Builder setMetadata( } else { metadataBuilder_.setMessage(builderForValue.build()); } - bitField0_ |= 0x00000002; + bitField0_ |= 0x00000004; onChanged(); return this; } /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; + * .google.spanner.v1.ResultSetMetadata metadata = 3; */ public Builder mergeMetadata(com.google.spanner.v1.ResultSetMetadata value) { if (metadataBuilder_ == null) { - if (((bitField0_ & 0x00000002) != 0) && + if (((bitField0_ & 0x00000004) != 0) && metadata_ != null && metadata_ != com.google.spanner.v1.ResultSetMetadata.getDefaultInstance()) { getMetadataBuilder().mergeFrom(value); @@ -851,16 +987,16 @@ public Builder mergeMetadata(com.google.spanner.v1.ResultSetMetadata value) { metadataBuilder_.mergeFrom(value); } if (metadata_ != null) { - bitField0_ |= 0x00000002; + bitField0_ |= 0x00000004; onChanged(); } return this; } /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; + * .google.spanner.v1.ResultSetMetadata metadata = 3; */ public Builder clearMetadata() { - bitField0_ = (bitField0_ & ~0x00000002); + bitField0_ = (bitField0_ & ~0x00000004); metadata_ = null; if (metadataBuilder_ != null) { metadataBuilder_.dispose(); @@ -870,15 +1006,15 @@ public Builder clearMetadata() { return this; } /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; + * .google.spanner.v1.ResultSetMetadata metadata = 3; */ public com.google.spanner.v1.ResultSetMetadata.Builder getMetadataBuilder() { - bitField0_ |= 0x00000002; + bitField0_ |= 0x00000004; onChanged(); return internalGetMetadataFieldBuilder().getBuilder(); } /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; + * .google.spanner.v1.ResultSetMetadata metadata = 3; */ public com.google.spanner.v1.ResultSetMetadataOrBuilder getMetadataOrBuilder() { if (metadataBuilder_ != null) { @@ -889,7 +1025,7 @@ public com.google.spanner.v1.ResultSetMetadataOrBuilder getMetadataOrBuilder() { } } /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; + * .google.spanner.v1.ResultSetMetadata metadata = 3; */ private com.google.protobuf.SingleFieldBuilder< com.google.spanner.v1.ResultSetMetadata, com.google.spanner.v1.ResultSetMetadata.Builder, com.google.spanner.v1.ResultSetMetadataOrBuilder> @@ -908,9 +1044,9 @@ public com.google.spanner.v1.ResultSetMetadataOrBuilder getMetadataOrBuilder() { private java.util.List data_ = java.util.Collections.emptyList(); private void ensureDataIsMutable() { - if (!((bitField0_ & 0x00000004) != 0)) { + if (!((bitField0_ & 0x00000008) != 0)) { data_ = new java.util.ArrayList(data_); - bitField0_ |= 0x00000004; + bitField0_ |= 0x00000008; } } @@ -918,7 +1054,7 @@ private void ensureDataIsMutable() { com.google.protobuf.ListValue, com.google.protobuf.ListValue.Builder, com.google.protobuf.ListValueOrBuilder> dataBuilder_; /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ public java.util.List getDataList() { if (dataBuilder_ == null) { @@ -928,7 +1064,7 @@ public java.util.List getDataList() { } } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ public int getDataCount() { if (dataBuilder_ == null) { @@ -938,7 +1074,7 @@ public int getDataCount() { } } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ public com.google.protobuf.ListValue getData(int index) { if (dataBuilder_ == null) { @@ -948,7 +1084,7 @@ public com.google.protobuf.ListValue getData(int index) { } } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ public Builder setData( int index, com.google.protobuf.ListValue value) { @@ -965,7 +1101,7 @@ public Builder setData( return this; } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ public Builder setData( int index, com.google.protobuf.ListValue.Builder builderForValue) { @@ -979,7 +1115,7 @@ public Builder setData( return this; } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ public Builder addData(com.google.protobuf.ListValue value) { if (dataBuilder_ == null) { @@ -995,7 +1131,7 @@ public Builder addData(com.google.protobuf.ListValue value) { return this; } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ public Builder addData( int index, com.google.protobuf.ListValue value) { @@ -1012,7 +1148,7 @@ public Builder addData( return this; } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ public Builder addData( com.google.protobuf.ListValue.Builder builderForValue) { @@ -1026,7 +1162,7 @@ public Builder addData( return this; } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ public Builder addData( int index, com.google.protobuf.ListValue.Builder builderForValue) { @@ -1040,7 +1176,7 @@ public Builder addData( return this; } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ public Builder addAllData( java.lang.Iterable values) { @@ -1055,12 +1191,12 @@ public Builder addAllData( return this; } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ public Builder clearData() { if (dataBuilder_ == null) { data_ = java.util.Collections.emptyList(); - bitField0_ = (bitField0_ & ~0x00000004); + bitField0_ = (bitField0_ & ~0x00000008); onChanged(); } else { dataBuilder_.clear(); @@ -1068,7 +1204,7 @@ public Builder clearData() { return this; } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ public Builder removeData(int index) { if (dataBuilder_ == null) { @@ -1081,14 +1217,14 @@ public Builder removeData(int index) { return this; } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ public com.google.protobuf.ListValue.Builder getDataBuilder( int index) { return internalGetDataFieldBuilder().getBuilder(index); } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ public com.google.protobuf.ListValueOrBuilder getDataOrBuilder( int index) { @@ -1098,7 +1234,7 @@ public com.google.protobuf.ListValueOrBuilder getDataOrBuilder( } } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ public java.util.List getDataOrBuilderList() { @@ -1109,14 +1245,14 @@ public com.google.protobuf.ListValueOrBuilder getDataOrBuilder( } } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ public com.google.protobuf.ListValue.Builder addDataBuilder() { return internalGetDataFieldBuilder().addBuilder( com.google.protobuf.ListValue.getDefaultInstance()); } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ public com.google.protobuf.ListValue.Builder addDataBuilder( int index) { @@ -1124,7 +1260,7 @@ public com.google.protobuf.ListValue.Builder addDataBuilder( index, com.google.protobuf.ListValue.getDefaultInstance()); } /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ public java.util.List getDataBuilderList() { @@ -1137,7 +1273,7 @@ public com.google.protobuf.ListValue.Builder addDataBuilder( dataBuilder_ = new com.google.protobuf.RepeatedFieldBuilder< com.google.protobuf.ListValue, com.google.protobuf.ListValue.Builder, com.google.protobuf.ListValueOrBuilder>( data_, - ((bitField0_ & 0x00000004) != 0), + ((bitField0_ & 0x00000008) != 0), getParentForChildren(), isClean()); data_ = null; @@ -1149,14 +1285,14 @@ public com.google.protobuf.ListValue.Builder addDataBuilder( private com.google.protobuf.SingleFieldBuilder< com.google.spanner.v1.ResultSetStats, com.google.spanner.v1.ResultSetStats.Builder, com.google.spanner.v1.ResultSetStatsOrBuilder> statsBuilder_; /** - * .google.spanner.v1.ResultSetStats stats = 4; + * .google.spanner.v1.ResultSetStats stats = 5; * @return Whether the stats field is set. */ public boolean hasStats() { - return ((bitField0_ & 0x00000008) != 0); + return ((bitField0_ & 0x00000010) != 0); } /** - * .google.spanner.v1.ResultSetStats stats = 4; + * .google.spanner.v1.ResultSetStats stats = 5; * @return The stats. */ public com.google.spanner.v1.ResultSetStats getStats() { @@ -1167,7 +1303,7 @@ public com.google.spanner.v1.ResultSetStats getStats() { } } /** - * .google.spanner.v1.ResultSetStats stats = 4; + * .google.spanner.v1.ResultSetStats stats = 5; */ public Builder setStats(com.google.spanner.v1.ResultSetStats value) { if (statsBuilder_ == null) { @@ -1178,12 +1314,12 @@ public Builder setStats(com.google.spanner.v1.ResultSetStats value) { } else { statsBuilder_.setMessage(value); } - bitField0_ |= 0x00000008; + bitField0_ |= 0x00000010; onChanged(); return this; } /** - * .google.spanner.v1.ResultSetStats stats = 4; + * .google.spanner.v1.ResultSetStats stats = 5; */ public Builder setStats( com.google.spanner.v1.ResultSetStats.Builder builderForValue) { @@ -1192,16 +1328,16 @@ public Builder setStats( } else { statsBuilder_.setMessage(builderForValue.build()); } - bitField0_ |= 0x00000008; + bitField0_ |= 0x00000010; onChanged(); return this; } /** - * .google.spanner.v1.ResultSetStats stats = 4; + * .google.spanner.v1.ResultSetStats stats = 5; */ public Builder mergeStats(com.google.spanner.v1.ResultSetStats value) { if (statsBuilder_ == null) { - if (((bitField0_ & 0x00000008) != 0) && + if (((bitField0_ & 0x00000010) != 0) && stats_ != null && stats_ != com.google.spanner.v1.ResultSetStats.getDefaultInstance()) { getStatsBuilder().mergeFrom(value); @@ -1212,16 +1348,16 @@ public Builder mergeStats(com.google.spanner.v1.ResultSetStats value) { statsBuilder_.mergeFrom(value); } if (stats_ != null) { - bitField0_ |= 0x00000008; + bitField0_ |= 0x00000010; onChanged(); } return this; } /** - * .google.spanner.v1.ResultSetStats stats = 4; + * .google.spanner.v1.ResultSetStats stats = 5; */ public Builder clearStats() { - bitField0_ = (bitField0_ & ~0x00000008); + bitField0_ = (bitField0_ & ~0x00000010); stats_ = null; if (statsBuilder_ != null) { statsBuilder_.dispose(); @@ -1231,15 +1367,15 @@ public Builder clearStats() { return this; } /** - * .google.spanner.v1.ResultSetStats stats = 4; + * .google.spanner.v1.ResultSetStats stats = 5; */ public com.google.spanner.v1.ResultSetStats.Builder getStatsBuilder() { - bitField0_ |= 0x00000008; + bitField0_ |= 0x00000010; onChanged(); return internalGetStatsFieldBuilder().getBuilder(); } /** - * .google.spanner.v1.ResultSetStats stats = 4; + * .google.spanner.v1.ResultSetStats stats = 5; */ public com.google.spanner.v1.ResultSetStatsOrBuilder getStatsOrBuilder() { if (statsBuilder_ != null) { @@ -1250,7 +1386,7 @@ public com.google.spanner.v1.ResultSetStatsOrBuilder getStatsOrBuilder() { } } /** - * .google.spanner.v1.ResultSetStats stats = 4; + * .google.spanner.v1.ResultSetStats stats = 5; */ private com.google.protobuf.SingleFieldBuilder< com.google.spanner.v1.ResultSetStats, com.google.spanner.v1.ResultSetStats.Builder, com.google.spanner.v1.ResultSetStatsOrBuilder> @@ -1268,7 +1404,7 @@ public com.google.spanner.v1.ResultSetStatsOrBuilder getStatsOrBuilder() { private boolean hasMoreResults_ ; /** - * bool has_more_results = 5; + * bool has_more_results = 6; * @return The hasMoreResults. */ @java.lang.Override @@ -1276,23 +1412,23 @@ public boolean getHasMoreResults() { return hasMoreResults_; } /** - * bool has_more_results = 5; + * bool has_more_results = 6; * @param value The hasMoreResults to set. * @return This builder for chaining. */ public Builder setHasMoreResults(boolean value) { hasMoreResults_ = value; - bitField0_ |= 0x00000010; + bitField0_ |= 0x00000020; onChanged(); return this; } /** - * bool has_more_results = 5; + * bool has_more_results = 6; * @return This builder for chaining. */ public Builder clearHasMoreResults() { - bitField0_ = (bitField0_ & ~0x00000010); + bitField0_ = (bitField0_ & ~0x00000020); hasMoreResults_ = false; onChanged(); return this; diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/RowDataOrBuilder.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/RowDataOrBuilder.java index 255b566f..7dd00184 100644 --- a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/RowDataOrBuilder.java +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/RowDataOrBuilder.java @@ -11,76 +11,88 @@ public interface RowDataOrBuilder extends com.google.protobuf.MessageOrBuilder { /** - * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * string request_id = 1; + * @return The requestId. + */ + java.lang.String getRequestId(); + /** + * string request_id = 1; + * @return The bytes for requestId. + */ + com.google.protobuf.ByteString + getRequestIdBytes(); + + /** + * .google.spannerlib.v1.Rows rows = 2 [(.google.api.field_behavior) = REQUIRED]; * @return Whether the rows field is set. */ boolean hasRows(); /** - * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * .google.spannerlib.v1.Rows rows = 2 [(.google.api.field_behavior) = REQUIRED]; * @return The rows. */ com.google.cloud.spannerlib.v1.Rows getRows(); /** - * .google.spannerlib.v1.Rows rows = 1 [(.google.api.field_behavior) = REQUIRED]; + * .google.spannerlib.v1.Rows rows = 2 [(.google.api.field_behavior) = REQUIRED]; */ com.google.cloud.spannerlib.v1.RowsOrBuilder getRowsOrBuilder(); /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; + * .google.spanner.v1.ResultSetMetadata metadata = 3; * @return Whether the metadata field is set. */ boolean hasMetadata(); /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; + * .google.spanner.v1.ResultSetMetadata metadata = 3; * @return The metadata. */ com.google.spanner.v1.ResultSetMetadata getMetadata(); /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; + * .google.spanner.v1.ResultSetMetadata metadata = 3; */ com.google.spanner.v1.ResultSetMetadataOrBuilder getMetadataOrBuilder(); /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ java.util.List getDataList(); /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ com.google.protobuf.ListValue getData(int index); /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ int getDataCount(); /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ java.util.List getDataOrBuilderList(); /** - * repeated .google.protobuf.ListValue data = 3 [(.google.api.field_behavior) = REQUIRED]; + * repeated .google.protobuf.ListValue data = 4 [(.google.api.field_behavior) = REQUIRED]; */ com.google.protobuf.ListValueOrBuilder getDataOrBuilder( int index); /** - * .google.spanner.v1.ResultSetStats stats = 4; + * .google.spanner.v1.ResultSetStats stats = 5; * @return Whether the stats field is set. */ boolean hasStats(); /** - * .google.spanner.v1.ResultSetStats stats = 4; + * .google.spanner.v1.ResultSetStats stats = 5; * @return The stats. */ com.google.spanner.v1.ResultSetStats getStats(); /** - * .google.spanner.v1.ResultSetStats stats = 4; + * .google.spanner.v1.ResultSetStats stats = 5; */ com.google.spanner.v1.ResultSetStatsOrBuilder getStatsOrBuilder(); /** - * bool has_more_results = 5; + * bool has_more_results = 6; * @return The hasMoreResults. */ boolean getHasMoreResults(); diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/SpannerLibGrpc.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/SpannerLibGrpc.java index fbfb5da6..796700e9 100644 --- a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/SpannerLibGrpc.java +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/SpannerLibGrpc.java @@ -570,6 +570,37 @@ com.google.cloud.spannerlib.v1.ConnectionStreamResponse> getConnectionStreamMeth return getConnectionStreamMethod; } + private static volatile io.grpc.MethodDescriptor getContinueStreamingMethod; + + @io.grpc.stub.annotations.RpcMethod( + fullMethodName = SERVICE_NAME + '/' + "ContinueStreaming", + requestType = com.google.cloud.spannerlib.v1.Rows.class, + responseType = com.google.cloud.spannerlib.v1.RowData.class, + methodType = io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING) + public static io.grpc.MethodDescriptor getContinueStreamingMethod() { + io.grpc.MethodDescriptor getContinueStreamingMethod; + if ((getContinueStreamingMethod = SpannerLibGrpc.getContinueStreamingMethod) == null) { + synchronized (SpannerLibGrpc.class) { + if ((getContinueStreamingMethod = SpannerLibGrpc.getContinueStreamingMethod) == null) { + SpannerLibGrpc.getContinueStreamingMethod = getContinueStreamingMethod = + io.grpc.MethodDescriptor.newBuilder() + .setType(io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING) + .setFullMethodName(generateFullMethodName(SERVICE_NAME, "ContinueStreaming")) + .setSampledToLocalTracing(true) + .setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + com.google.cloud.spannerlib.v1.Rows.getDefaultInstance())) + .setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + com.google.cloud.spannerlib.v1.RowData.getDefaultInstance())) + .setSchemaDescriptor(new SpannerLibMethodDescriptorSupplier("ContinueStreaming")) + .build(); + } + } + } + return getContinueStreamingMethod; + } + /** * Creates a new async stub that supports all call types for the service */ @@ -758,6 +789,13 @@ default io.grpc.stub.StreamObserver responseObserver) { return io.grpc.stub.ServerCalls.asyncUnimplementedStreamingCall(getConnectionStreamMethod(), responseObserver); } + + /** + */ + default void continueStreaming(com.google.cloud.spannerlib.v1.Rows request, + io.grpc.stub.StreamObserver responseObserver) { + io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getContinueStreamingMethod(), responseObserver); + } } /** @@ -930,6 +968,14 @@ public io.grpc.stub.StreamObserver responseObserver) { + io.grpc.stub.ClientCalls.asyncServerStreamingCall( + getChannel().newCall(getContinueStreamingMethod(), getCallOptions()), request, responseObserver); + } } /** @@ -1077,6 +1123,15 @@ public com.google.spanner.v1.CommitResponse writeMutations(com.google.cloud.span return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( getChannel(), getConnectionStreamMethod(), getCallOptions()); } + + /** + */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + continueStreaming(com.google.cloud.spannerlib.v1.Rows request) { + return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall( + getChannel(), getContinueStreamingMethod(), getCallOptions(), request); + } } /** @@ -1214,6 +1269,14 @@ public com.google.spanner.v1.CommitResponse writeMutations(com.google.cloud.span return io.grpc.stub.ClientCalls.blockingUnaryCall( getChannel(), getWriteMutationsMethod(), getCallOptions(), request); } + + /** + */ + public java.util.Iterator continueStreaming( + com.google.cloud.spannerlib.v1.Rows request) { + return io.grpc.stub.ClientCalls.blockingServerStreamingCall( + getChannel(), getContinueStreamingMethod(), getCallOptions(), request); + } } /** @@ -1378,7 +1441,8 @@ public com.google.common.util.concurrent.ListenableFuture implements io.grpc.stub.ServerCalls.UnaryMethod, @@ -1465,6 +1529,10 @@ public void invoke(Req request, io.grpc.stub.StreamObserver responseObserv serviceImpl.writeMutations((com.google.cloud.spannerlib.v1.WriteMutationsRequest) request, (io.grpc.stub.StreamObserver) responseObserver); break; + case METHODID_CONTINUE_STREAMING: + serviceImpl.continueStreaming((com.google.cloud.spannerlib.v1.Rows) request, + (io.grpc.stub.StreamObserver) responseObserver); + break; default: throw new AssertionError(); } @@ -1612,6 +1680,13 @@ public static final io.grpc.ServerServiceDefinition bindService(AsyncService ser com.google.cloud.spannerlib.v1.ConnectionStreamRequest, com.google.cloud.spannerlib.v1.ConnectionStreamResponse>( service, METHODID_CONNECTION_STREAM))) + .addMethod( + getContinueStreamingMethod(), + io.grpc.stub.ServerCalls.asyncServerStreamingCall( + new MethodHandlers< + com.google.cloud.spannerlib.v1.Rows, + com.google.cloud.spannerlib.v1.RowData>( + service, METHODID_CONTINUE_STREAMING))) .build(); } @@ -1678,6 +1753,7 @@ public static io.grpc.ServiceDescriptor getServiceDescriptor() { .addMethod(getRollbackMethod()) .addMethod(getWriteMutationsMethod()) .addMethod(getConnectionStreamMethod()) + .addMethod(getContinueStreamingMethod()) .build(); } } diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/SpannerLibProto.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/SpannerLibProto.java index b782bddb..dc595690 100644 --- a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/SpannerLibProto.java +++ b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/SpannerLibProto.java @@ -86,6 +86,11 @@ public static void registerAllExtensions( static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_google_spannerlib_v1_NextRequest_fieldAccessorTable; + static final com.google.protobuf.Descriptors.Descriptor + internal_static_google_spannerlib_v1_FetchOptions_descriptor; + static final + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_google_spannerlib_v1_FetchOptions_fieldAccessorTable; static final com.google.protobuf.Descriptors.Descriptor internal_static_google_spannerlib_v1_RowData_descriptor; static final @@ -106,6 +111,11 @@ public static void registerAllExtensions( static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_google_spannerlib_v1_ConnectionStreamRequest_fieldAccessorTable; + static final com.google.protobuf.Descriptors.Descriptor + internal_static_google_spannerlib_v1_ExecuteResponse_descriptor; + static final + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_google_spannerlib_v1_ExecuteResponse_fieldAccessorTable; static final com.google.protobuf.Descriptors.Descriptor internal_static_google_spannerlib_v1_ConnectionStreamResponse_descriptor; static final @@ -123,96 +133,126 @@ public static void registerAllExtensions( "\n%google/spannerlib/v1/spannerlib.proto\022" + "\024google.spannerlib.v1\032\037google/api/field_" + "behavior.proto\032\033google/protobuf/empty.pr" + - "oto\032\034google/protobuf/struct.proto\032\"googl" + - "e/spanner/v1/result_set.proto\032\037google/sp" + - "anner/v1/spanner.proto\032#google/spanner/v" + - "1/transaction.proto\"\r\n\013InfoRequest\"\037\n\014In" + - "foResponse\022\017\n\007version\030\001 \001(\t\"3\n\021CreatePoo" + - "lRequest\022\036\n\021connection_string\030\001 \001(\tB\003\340A\002" + - "\"H\n\027CreateConnectionRequest\022-\n\004pool\030\001 \001(" + - "\0132\032.google.spannerlib.v1.PoolB\003\340A\002\"\223\001\n\016E" + - "xecuteRequest\0229\n\nconnection\030\001 \001(\0132 .goog" + - "le.spannerlib.v1.ConnectionB\003\340A\002\022F\n\023exec" + - "ute_sql_request\030\002 \001(\0132$.google.spanner.v" + - "1.ExecuteSqlRequestB\003\340A\002\"\243\001\n\023ExecuteBatc" + - "hRequest\0229\n\nconnection\030\001 \001(\0132 .google.sp" + - "annerlib.v1.ConnectionB\003\340A\002\022Q\n\031execute_b" + - "atch_dml_request\030\002 \001(\0132).google.spanner." + - "v1.ExecuteBatchDmlRequestB\003\340A\002\"\235\001\n\027Begin" + - "TransactionRequest\0229\n\nconnection\030\001 \001(\0132 " + - ".google.spannerlib.v1.ConnectionB\003\340A\002\022G\n" + - "\023transaction_options\030\002 \001(\0132%.google.span" + - "ner.v1.TransactionOptionsB\003\340A\002\"\236\001\n\025Write" + - "MutationsRequest\0229\n\nconnection\030\001 \001(\0132 .g" + - "oogle.spannerlib.v1.ConnectionB\003\340A\002\022J\n\tm" + - "utations\030\002 \001(\01322.google.spanner.v1.Batch" + - "WriteRequest.MutationGroupB\003\340A\002\"\027\n\004Pool\022" + - "\017\n\002id\030\001 \001(\003B\003\340A\002\"L\n\nConnection\022-\n\004pool\030\001" + - " \001(\0132\032.google.spannerlib.v1.PoolB\003\340A\002\022\017\n" + - "\002id\030\002 \001(\003B\003\340A\002\"R\n\004Rows\0229\n\nconnection\030\001 \001" + + "oto\032\034google/protobuf/struct.proto\032\027googl" + + "e/rpc/status.proto\032\"google/spanner/v1/re" + + "sult_set.proto\032\037google/spanner/v1/spanne" + + "r.proto\032#google/spanner/v1/transaction.p" + + "roto\"\r\n\013InfoRequest\"\037\n\014InfoResponse\022\017\n\007v" + + "ersion\030\001 \001(\t\"3\n\021CreatePoolRequest\022\036\n\021con" + + "nection_string\030\001 \001(\tB\003\340A\002\"H\n\027CreateConne" + + "ctionRequest\022-\n\004pool\030\001 \001(\0132\032.google.span" + + "nerlib.v1.PoolB\003\340A\002\"\316\001\n\016ExecuteRequest\0229" + + "\n\nconnection\030\001 \001(\0132 .google.spannerlib.v" + + "1.ConnectionB\003\340A\002\022F\n\023execute_sql_request" + + "\030\002 \001(\0132$.google.spanner.v1.ExecuteSqlReq" + + "uestB\003\340A\002\0229\n\rfetch_options\030\003 \001(\0132\".googl" + + "e.spannerlib.v1.FetchOptions\"\243\001\n\023Execute" + + "BatchRequest\0229\n\nconnection\030\001 \001(\0132 .googl" + + "e.spannerlib.v1.ConnectionB\003\340A\002\022Q\n\031execu" + + "te_batch_dml_request\030\002 \001(\0132).google.span" + + "ner.v1.ExecuteBatchDmlRequestB\003\340A\002\"\235\001\n\027B" + + "eginTransactionRequest\0229\n\nconnection\030\001 \001" + "(\0132 .google.spannerlib.v1.ConnectionB\003\340A" + - "\002\022\017\n\002id\030\002 \001(\003B\003\340A\002\"j\n\013NextRequest\022-\n\004row" + - "s\030\001 \001(\0132\032.google.spannerlib.v1.RowsB\003\340A\002" + - "\022\025\n\010num_rows\030\002 \001(\003B\003\340A\002\022\025\n\010encoding\030\003 \001(" + - "\003B\003\340A\002\"\353\001\n\007RowData\022-\n\004rows\030\001 \001(\0132\032.googl" + - "e.spannerlib.v1.RowsB\003\340A\002\0226\n\010metadata\030\002 " + - "\001(\0132$.google.spanner.v1.ResultSetMetadat" + - "a\022-\n\004data\030\003 \003(\0132\032.google.protobuf.ListVa" + - "lueB\003\340A\002\0220\n\005stats\030\004 \001(\0132!.google.spanner" + - ".v1.ResultSetStats\022\030\n\020has_more_results\030\005" + - " \001(\010\"@\n\017MetadataRequest\022-\n\004rows\030\001 \001(\0132\032." + - "google.spannerlib.v1.RowsB\003\340A\002\"F\n\025Result" + - "SetStatsRequest\022-\n\004rows\030\001 \001(\0132\032.google.s" + - "pannerlib.v1.RowsB\003\340A\002\"e\n\027ConnectionStre" + - "amRequest\022?\n\017execute_request\030\001 \001(\0132$.goo" + - "gle.spannerlib.v1.ExecuteRequestH\000B\t\n\007re" + - "quest\"Z\n\030ConnectionStreamResponse\0222\n\003row" + - "\030\001 \001(\0132#.google.spanner.v1.PartialResult" + - "SetH\000B\n\n\010response2\227\014\n\nSpannerLib\022O\n\004Info" + - "\022!.google.spannerlib.v1.InfoRequest\032\".go" + - "ogle.spannerlib.v1.InfoResponse\"\000\022S\n\nCre" + - "atePool\022\'.google.spannerlib.v1.CreatePoo" + - "lRequest\032\032.google.spannerlib.v1.Pool\"\000\022A" + - "\n\tClosePool\022\032.google.spannerlib.v1.Pool\032" + - "\026.google.protobuf.Empty\"\000\022e\n\020CreateConne" + - "ction\022-.google.spannerlib.v1.CreateConne" + - "ctionRequest\032 .google.spannerlib.v1.Conn" + - "ection\"\000\022M\n\017CloseConnection\022 .google.spa" + - "nnerlib.v1.Connection\032\026.google.protobuf." + - "Empty\"\000\022M\n\007Execute\022$.google.spannerlib.v" + - "1.ExecuteRequest\032\032.google.spannerlib.v1." + - "Rows\"\000\022[\n\020ExecuteStreaming\022$.google.span" + - "nerlib.v1.ExecuteRequest\032\035.google.spanne" + - "rlib.v1.RowData\"\0000\001\022g\n\014ExecuteBatch\022).go" + - "ogle.spannerlib.v1.ExecuteBatchRequest\032*" + - ".google.spanner.v1.ExecuteBatchDmlRespon" + - "se\"\000\022N\n\010Metadata\022\032.google.spannerlib.v1." + - "Rows\032$.google.spanner.v1.ResultSetMetada" + - "ta\"\000\022G\n\004Next\022!.google.spannerlib.v1.Next" + - "Request\032\032.google.protobuf.ListValue\"\000\022Q\n" + - "\016ResultSetStats\022\032.google.spannerlib.v1.R" + - "ows\032!.google.spanner.v1.ResultSetStats\"\000" + - "\022S\n\rNextResultSet\022\032.google.spannerlib.v1" + - ".Rows\032$.google.spanner.v1.ResultSetMetad" + - "ata\"\000\022A\n\tCloseRows\022\032.google.spannerlib.v" + - "1.Rows\032\026.google.protobuf.Empty\"\000\022[\n\020Begi" + - "nTransaction\022-.google.spannerlib.v1.Begi" + - "nTransactionRequest\032\026.google.protobuf.Em" + - "pty\"\000\022O\n\006Commit\022 .google.spannerlib.v1.C" + - "onnection\032!.google.spanner.v1.CommitResp" + - "onse\"\000\022F\n\010Rollback\022 .google.spannerlib.v" + - "1.Connection\032\026.google.protobuf.Empty\"\000\022b" + - "\n\016WriteMutations\022+.google.spannerlib.v1." + - "WriteMutationsRequest\032!.google.spanner.v" + - "1.CommitResponse\"\000\022w\n\020ConnectionStream\022-" + - ".google.spannerlib.v1.ConnectionStreamRe" + - "quest\032..google.spannerlib.v1.ConnectionS" + - "treamResponse\"\000(\0010\001B\315\001\n\036com.google.cloud" + - ".spannerlib.v1B\017SpannerLibProtoP\001Z>cloud" + - ".google.com/go/spannerlib/apiv1/spannerl" + - "ibpb;spannerlibpb\252\002\032Google.Cloud.Spanner" + - "Lib.V1\312\002\032Google\\Cloud\\SpannerLib\\V1\352\002\035Go" + - "ogle::Cloud::SpannerLib::V1b\006proto3" + "\002\022G\n\023transaction_options\030\002 \001(\0132%.google." + + "spanner.v1.TransactionOptionsB\003\340A\002\"\236\001\n\025W" + + "riteMutationsRequest\0229\n\nconnection\030\001 \001(\013" + + "2 .google.spannerlib.v1.ConnectionB\003\340A\002\022" + + "J\n\tmutations\030\002 \001(\01322.google.spanner.v1.B" + + "atchWriteRequest.MutationGroupB\003\340A\002\"\027\n\004P" + + "ool\022\017\n\002id\030\001 \001(\003B\003\340A\002\"L\n\nConnection\022-\n\004po" + + "ol\030\001 \001(\0132\032.google.spannerlib.v1.PoolB\003\340A" + + "\002\022\017\n\002id\030\002 \001(\003B\003\340A\002\"R\n\004Rows\0229\n\nconnection" + + "\030\001 \001(\0132 .google.spannerlib.v1.Connection" + + "B\003\340A\002\022\017\n\002id\030\002 \001(\003B\003\340A\002\"|\n\013NextRequest\022-\n" + + "\004rows\030\001 \001(\0132\032.google.spannerlib.v1.RowsB" + + "\003\340A\002\022>\n\rfetch_options\030\002 \001(\0132\".google.spa" + + "nnerlib.v1.FetchOptionsB\003\340A\002\"<\n\014FetchOpt" + + "ions\022\025\n\010num_rows\030\001 \001(\003B\003\340A\002\022\025\n\010encoding\030" + + "\002 \001(\003B\003\340A\002\"\377\001\n\007RowData\022\022\n\nrequest_id\030\001 \001" + + "(\t\022-\n\004rows\030\002 \001(\0132\032.google.spannerlib.v1." + + "RowsB\003\340A\002\0226\n\010metadata\030\003 \001(\0132$.google.spa" + + "nner.v1.ResultSetMetadata\022-\n\004data\030\004 \003(\0132" + + "\032.google.protobuf.ListValueB\003\340A\002\0220\n\005stat" + + "s\030\005 \001(\0132!.google.spanner.v1.ResultSetSta" + + "ts\022\030\n\020has_more_results\030\006 \001(\010\"@\n\017Metadata" + + "Request\022-\n\004rows\030\001 \001(\0132\032.google.spannerli" + + "b.v1.RowsB\003\340A\002\"F\n\025ResultSetStatsRequest\022" + + "-\n\004rows\030\001 \001(\0132\032.google.spannerlib.v1.Row" + + "sB\003\340A\002\"\317\003\n\027ConnectionStreamRequest\022?\n\017ex" + + "ecute_request\030\001 \001(\0132$.google.spannerlib." + + "v1.ExecuteRequestH\000\022J\n\025execute_batch_req" + + "uest\030\002 \001(\0132).google.spannerlib.v1.Execut" + + "eBatchRequestH\000\022R\n\031begin_transaction_req" + + "uest\030\003 \001(\0132-.google.spannerlib.v1.BeginT" + + "ransactionRequestH\000\022:\n\016commit_request\030\004 " + + "\001(\0132 .google.spannerlib.v1.ConnectionH\000\022" + + "<\n\020rollback_request\030\005 \001(\0132 .google.spann" + + "erlib.v1.ConnectionH\000\022N\n\027write_mutations" + + "_request\030\006 \001(\0132+.google.spannerlib.v1.Wr" + + "iteMutationsRequestH\000B\t\n\007request\"\261\001\n\017Exe" + + "cuteResponse\022-\n\004rows\030\001 \001(\0132\032.google.span" + + "nerlib.v1.RowsB\003\340A\002\0221\n\013result_sets\030\002 \003(\013" + + "2\034.google.spanner.v1.ResultSet\022\"\n\006status" + + "\030\003 \001(\0132\022.google.rpc.Status\022\030\n\020has_more_r" + + "esults\030\004 \001(\010\"\323\003\n\030ConnectionStreamRespons" + + "e\022\"\n\006status\030\001 \001(\0132\022.google.rpc.Status\022A\n" + + "\020execute_response\030\002 \001(\0132%.google.spanner" + + "lib.v1.ExecuteResponseH\000\022L\n\026execute_batc" + + "h_response\030\003 \001(\0132*.google.spanner.v1.Exe" + + "cuteBatchDmlResponseH\000\022<\n\032begin_transact" + + "ion_response\030\004 \001(\0132\026.google.protobuf.Emp" + + "tyH\000\022<\n\017commit_response\030\005 \001(\0132!.google.s" + + "panner.v1.CommitResponseH\000\0223\n\021rollback_r" + + "esponse\030\006 \001(\0132\026.google.protobuf.EmptyH\000\022" + + "E\n\030write_mutations_response\030\007 \001(\0132!.goog" + + "le.spanner.v1.CommitResponseH\000B\n\n\010respon" + + "se2\353\014\n\nSpannerLib\022O\n\004Info\022!.google.spann" + + "erlib.v1.InfoRequest\032\".google.spannerlib" + + ".v1.InfoResponse\"\000\022S\n\nCreatePool\022\'.googl" + + "e.spannerlib.v1.CreatePoolRequest\032\032.goog" + + "le.spannerlib.v1.Pool\"\000\022A\n\tClosePool\022\032.g" + + "oogle.spannerlib.v1.Pool\032\026.google.protob" + + "uf.Empty\"\000\022e\n\020CreateConnection\022-.google." + + "spannerlib.v1.CreateConnectionRequest\032 ." + + "google.spannerlib.v1.Connection\"\000\022M\n\017Clo" + + "seConnection\022 .google.spannerlib.v1.Conn" + + "ection\032\026.google.protobuf.Empty\"\000\022M\n\007Exec" + + "ute\022$.google.spannerlib.v1.ExecuteReques" + + "t\032\032.google.spannerlib.v1.Rows\"\000\022[\n\020Execu" + + "teStreaming\022$.google.spannerlib.v1.Execu" + + "teRequest\032\035.google.spannerlib.v1.RowData" + + "\"\0000\001\022g\n\014ExecuteBatch\022).google.spannerlib" + + ".v1.ExecuteBatchRequest\032*.google.spanner" + + ".v1.ExecuteBatchDmlResponse\"\000\022N\n\010Metadat" + + "a\022\032.google.spannerlib.v1.Rows\032$.google.s" + + "panner.v1.ResultSetMetadata\"\000\022G\n\004Next\022!." + + "google.spannerlib.v1.NextRequest\032\032.googl" + + "e.protobuf.ListValue\"\000\022Q\n\016ResultSetStats" + + "\022\032.google.spannerlib.v1.Rows\032!.google.sp" + + "anner.v1.ResultSetStats\"\000\022S\n\rNextResultS" + + "et\022\032.google.spannerlib.v1.Rows\032$.google." + + "spanner.v1.ResultSetMetadata\"\000\022A\n\tCloseR" + + "ows\022\032.google.spannerlib.v1.Rows\032\026.google" + + ".protobuf.Empty\"\000\022[\n\020BeginTransaction\022-." + + "google.spannerlib.v1.BeginTransactionReq" + + "uest\032\026.google.protobuf.Empty\"\000\022O\n\006Commit" + + "\022 .google.spannerlib.v1.Connection\032!.goo" + + "gle.spanner.v1.CommitResponse\"\000\022F\n\010Rollb" + + "ack\022 .google.spannerlib.v1.Connection\032\026." + + "google.protobuf.Empty\"\000\022b\n\016WriteMutation" + + "s\022+.google.spannerlib.v1.WriteMutationsR" + + "equest\032!.google.spanner.v1.CommitRespons" + + "e\"\000\022w\n\020ConnectionStream\022-.google.spanner" + + "lib.v1.ConnectionStreamRequest\032..google." + + "spannerlib.v1.ConnectionStreamResponse\"\000" + + "(\0010\001\022R\n\021ContinueStreaming\022\032.google.spann" + + "erlib.v1.Rows\032\035.google.spannerlib.v1.Row" + + "Data\"\0000\001B\315\001\n\036com.google.cloud.spannerlib" + + ".v1B\017SpannerLibProtoP\001Z>cloud.google.com" + + "/go/spannerlib/apiv1/spannerlibpb;spanne" + + "rlibpb\252\002\032Google.Cloud.SpannerLib.V1\312\002\032Go" + + "ogle\\Cloud\\SpannerLib\\V1\352\002\035Google::Cloud" + + "::SpannerLib::V1b\006proto3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor .internalBuildGeneratedFileFrom(descriptorData, @@ -220,6 +260,7 @@ public static void registerAllExtensions( com.google.api.FieldBehaviorProto.getDescriptor(), com.google.protobuf.EmptyProto.getDescriptor(), com.google.protobuf.StructProto.getDescriptor(), + com.google.rpc.StatusProto.getDescriptor(), com.google.spanner.v1.ResultSetProto.getDescriptor(), com.google.spanner.v1.SpannerProto.getDescriptor(), com.google.spanner.v1.TransactionProto.getDescriptor(), @@ -253,7 +294,7 @@ public static void registerAllExtensions( internal_static_google_spannerlib_v1_ExecuteRequest_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_google_spannerlib_v1_ExecuteRequest_descriptor, - new java.lang.String[] { "Connection", "ExecuteSqlRequest", }); + new java.lang.String[] { "Connection", "ExecuteSqlRequest", "FetchOptions", }); internal_static_google_spannerlib_v1_ExecuteBatchRequest_descriptor = getDescriptor().getMessageTypes().get(5); internal_static_google_spannerlib_v1_ExecuteBatchRequest_fieldAccessorTable = new @@ -295,41 +336,54 @@ public static void registerAllExtensions( internal_static_google_spannerlib_v1_NextRequest_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_google_spannerlib_v1_NextRequest_descriptor, - new java.lang.String[] { "Rows", "NumRows", "Encoding", }); - internal_static_google_spannerlib_v1_RowData_descriptor = + new java.lang.String[] { "Rows", "FetchOptions", }); + internal_static_google_spannerlib_v1_FetchOptions_descriptor = getDescriptor().getMessageTypes().get(12); + internal_static_google_spannerlib_v1_FetchOptions_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_google_spannerlib_v1_FetchOptions_descriptor, + new java.lang.String[] { "NumRows", "Encoding", }); + internal_static_google_spannerlib_v1_RowData_descriptor = + getDescriptor().getMessageTypes().get(13); internal_static_google_spannerlib_v1_RowData_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_google_spannerlib_v1_RowData_descriptor, - new java.lang.String[] { "Rows", "Metadata", "Data", "Stats", "HasMoreResults", }); + new java.lang.String[] { "RequestId", "Rows", "Metadata", "Data", "Stats", "HasMoreResults", }); internal_static_google_spannerlib_v1_MetadataRequest_descriptor = - getDescriptor().getMessageTypes().get(13); + getDescriptor().getMessageTypes().get(14); internal_static_google_spannerlib_v1_MetadataRequest_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_google_spannerlib_v1_MetadataRequest_descriptor, new java.lang.String[] { "Rows", }); internal_static_google_spannerlib_v1_ResultSetStatsRequest_descriptor = - getDescriptor().getMessageTypes().get(14); + getDescriptor().getMessageTypes().get(15); internal_static_google_spannerlib_v1_ResultSetStatsRequest_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_google_spannerlib_v1_ResultSetStatsRequest_descriptor, new java.lang.String[] { "Rows", }); internal_static_google_spannerlib_v1_ConnectionStreamRequest_descriptor = - getDescriptor().getMessageTypes().get(15); + getDescriptor().getMessageTypes().get(16); internal_static_google_spannerlib_v1_ConnectionStreamRequest_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_google_spannerlib_v1_ConnectionStreamRequest_descriptor, - new java.lang.String[] { "ExecuteRequest", "Request", }); + new java.lang.String[] { "ExecuteRequest", "ExecuteBatchRequest", "BeginTransactionRequest", "CommitRequest", "RollbackRequest", "WriteMutationsRequest", "Request", }); + internal_static_google_spannerlib_v1_ExecuteResponse_descriptor = + getDescriptor().getMessageTypes().get(17); + internal_static_google_spannerlib_v1_ExecuteResponse_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_google_spannerlib_v1_ExecuteResponse_descriptor, + new java.lang.String[] { "Rows", "ResultSets", "Status", "HasMoreResults", }); internal_static_google_spannerlib_v1_ConnectionStreamResponse_descriptor = - getDescriptor().getMessageTypes().get(16); + getDescriptor().getMessageTypes().get(18); internal_static_google_spannerlib_v1_ConnectionStreamResponse_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_google_spannerlib_v1_ConnectionStreamResponse_descriptor, - new java.lang.String[] { "Row", "Response", }); + new java.lang.String[] { "Status", "ExecuteResponse", "ExecuteBatchResponse", "BeginTransactionResponse", "CommitResponse", "RollbackResponse", "WriteMutationsResponse", "Response", }); descriptor.resolveAllFeaturesImmutable(); com.google.api.FieldBehaviorProto.getDescriptor(); com.google.protobuf.EmptyProto.getDescriptor(); com.google.protobuf.StructProto.getDescriptor(); + com.google.rpc.StatusProto.getDescriptor(); com.google.spanner.v1.ResultSetProto.getDescriptor(); com.google.spanner.v1.SpannerProto.getDescriptor(); com.google.spanner.v1.TransactionProto.getDescriptor(); From 093aed2884389d3c577d3bbb732739cc0f3ba754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Mon, 8 Dec 2025 14:22:16 +0100 Subject: [PATCH 21/29] chore: option for LoadData --- .../spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt | 4 ++-- .../spanner-ado-net-benchmarks/tpcc/Program.cs | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt index d58f378e..dafe4745 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt @@ -109,7 +109,7 @@ gcloud run deploy spannerlib-dotnet-benchmark-read-write-tx \ --no-allow-unauthenticated --no-cpu-throttling \ --min-instances=1 --max-instances=1 \ --cpu=4 --memory=2Gi \ - --set-env-vars=NUM_WAREHOUSES=100,NUM_CLIENTS=50,TRANSACTION_TYPE=ReadWriteTx \ + --set-env-vars=NUM_WAREHOUSES=500,NUM_CLIENTS=50,TRANSACTION_TYPE=ReadWriteTx,LOAD_DATA=false \ --base-image dotnet8 \ --source . @@ -121,7 +121,7 @@ gcloud run deploy spannerlib-dotnet-benchmark-read-write-tx-client-lib \ --no-allow-unauthenticated --no-cpu-throttling \ --min-instances=1 --max-instances=1 \ --cpu=4 --memory=2Gi \ - --set-env-vars=NUM_WAREHOUSES=100,NUM_CLIENTS=50,TRANSACTION_TYPE=ReadWriteTx,CLIENT_TYPE=ClientLib \ + --set-env-vars=NUM_WAREHOUSES=500,NUM_CLIENTS=50,TRANSACTION_TYPE=ReadWriteTx,CLIENT_TYPE=ClientLib,LOAD_DATA=false \ --base-image dotnet8 \ --source . diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Program.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Program.cs index 7d151009..b295b664 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Program.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Program.cs @@ -61,6 +61,7 @@ public static async Task Main(string[] args) app.MapGet("/", () => { }); var webapp = app.RunAsync(url); + var loadData = bool.Parse(Environment.GetEnvironmentVariable("LOAD_DATA") ?? "true"); var exportStats = bool.Parse(Environment.GetEnvironmentVariable("EXPORT_STATS") ?? "true"); var logWaitTime = int.Parse(Environment.GetEnvironmentVariable("LOG_WAIT_TIME") ?? "60"); var database = Environment.GetEnvironmentVariable("DATABASE") ?? "projects/appdev-soda-spanner-staging/instances/knut-test-ycsb/databases/dotnet-tpcc"; @@ -136,13 +137,16 @@ public static async Task Main(string[] args) { connectionString += ";retryAbortsInternally=false"; } - await using (var connection = new SpannerConnection()) + + if (loadData) { + await using var connection = new SpannerConnection(); connection.ConnectionString = connectionString; await connection.OpenAsync(cancellationTokenSource.Token); Console.WriteLine("Creating schema..."); - await SchemaUtil.CreateSchemaAsync(connection, DatabaseDialect.Postgresql, cancellationTokenSource.Token); + await SchemaUtil.CreateSchemaAsync(connection, DatabaseDialect.Postgresql, + cancellationTokenSource.Token); Console.WriteLine("Loading data..."); var loader = new DataLoader(connection, numWarehouses); From 6a101b55a7f609d8aec00eb386308d94e27b6736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Tue, 9 Dec 2025 11:43:13 +0100 Subject: [PATCH 22/29] chore: optimize checksum calculation --- .gitignore | 1 + checksum_row_iterator.go | 91 ++++++++++----- checksum_row_iterator_test.go | 106 ++++++++++++++---- .../spanner-ado-net-benchmarks/deploy.txt | 27 +++-- .../spanner-ado-net-benchmarks.csproj | 4 +- .../tpcc/BasicsRunner.cs | 6 +- .../tpcc/Program.cs | 1 + .../spanner-ado-net-tests/TransactionTests.cs | 50 +++++---- .../spanner-ado-net/spanner-ado-net.csproj | 10 +- .../spannerlib-dotnet-grpc-impl.csproj | 4 +- .../spannerlib-dotnet-grpc-server.csproj | 2 +- .../spannerlib-dotnet-grpc-v1.csproj | 2 +- .../spannerlib-dotnet-mockserver.csproj | 2 +- .../spannerlib-dotnet-native-impl.csproj | 4 +- .../spannerlib-dotnet-native.csproj | 2 +- .../spannerlib-dotnet.csproj | 2 +- transaction.go | 5 - 17 files changed, 217 insertions(+), 102 deletions(-) diff --git a/.gitignore b/.gitignore index ad2f2b24..8818a661 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ gorm/ .idea +.DS_Store diff --git a/checksum_row_iterator.go b/checksum_row_iterator.go index c6980a27..0218d6a0 100644 --- a/checksum_row_iterator.go +++ b/checksum_row_iterator.go @@ -15,11 +15,14 @@ package spannerdriver import ( - "bytes" "context" "crypto/sha256" + "encoding/binary" "encoding/gob" + "hash" + "math" "reflect" + "sort" "cloud.google.com/go/spanner" sppb "cloud.google.com/go/spanner/apiv1/spannerpb" @@ -70,8 +73,6 @@ type checksumRowIterator struct { // seen. It is calculated as a SHA256 checksum over all rows that so far // have been returned. checksum *[32]byte - buffer *bytes.Buffer - enc *gob.Encoder // errIndex and err indicate any error and the index in the result set // where the error occurred. @@ -110,7 +111,7 @@ func (it *checksumRowIterator) Next() (row *spanner.Row, err error) { // checksum of the columns that are included in this result. This is // also used to detect the possible difference between two empty // result sets with a different set of columns. - it.checksum, err = createMetadataChecksum(it.enc, it.buffer, it.metadata) + it.checksum, err = createMetadataChecksum(it.metadata) if err != nil { return err } @@ -119,7 +120,7 @@ func (it *checksumRowIterator) Next() (row *spanner.Row, err error) { return it.err } // Update the current checksum. - it.checksum, err = updateChecksum(it.enc, it.buffer, it.checksum, row) + it.checksum, err = updateChecksum(it.checksum, row) return err }) return row, err @@ -127,37 +128,79 @@ func (it *checksumRowIterator) Next() (row *spanner.Row, err error) { // updateChecksum calculates the following checksum based on a current checksum // and a new row. -func updateChecksum(enc *gob.Encoder, buffer *bytes.Buffer, currentChecksum *[32]byte, row *spanner.Row) (*[32]byte, error) { - buffer.Reset() - buffer.Write(currentChecksum[:]) +func updateChecksum(currentChecksum *[32]byte, row *spanner.Row) (*[32]byte, error) { + hasher := sha256.New() + hasher.Write(currentChecksum[:]) for i := 0; i < row.Size(); i++ { var v spanner.GenericColumnValue err := row.Column(i, &v) if err != nil { return nil, err } - err = enc.Encode(v) - if err != nil { - return nil, err + hashValue(v.Value, hasher) + } + res := hasher.Sum(nil) + return (*[32]byte)(res), nil +} + +var int32Buf [4]byte +var float64Buf [8]byte + +func hashValue(value *structpb.Value, hasher hash.Hash) { + switch value.GetKind().(type) { + case *structpb.Value_StringValue: + hasher.Write(intToByte(int32Buf, len(value.GetStringValue()))) + hasher.Write([]byte(value.GetStringValue())) + case *structpb.Value_NullValue: + hasher.Write([]byte{0}) + case *structpb.Value_NumberValue: + hasher.Write(float64ToByte(float64Buf, value.GetNumberValue())) + case *structpb.Value_BoolValue: + if value.GetBoolValue() { + hasher.Write([]byte{1}) + } else { + hasher.Write([]byte{0}) + } + case *structpb.Value_StructValue: + fields := make([]string, 0, len(value.GetStructValue().Fields)) + for field, _ := range value.GetStructValue().Fields { + fields = append(fields, field) + } + sort.Strings(fields) + for _, field := range fields { + hasher.Write(intToByte(int32Buf, len(field))) + hasher.Write([]byte(field)) + hashValue(value.GetStructValue().Fields[field], hasher) + } + case *structpb.Value_ListValue: + for _, v := range value.GetListValue().GetValues() { + hashValue(v, hasher) } } - res := sha256.Sum256(buffer.Bytes()) - return &res, nil +} + +func intToByte(buf [4]byte, v int) []byte { + binary.BigEndian.PutUint32(buf[:], uint32(v)) + return buf[:] +} + +func float64ToByte(buf [8]byte, f float64) []byte { + binary.BigEndian.PutUint64(buf[:], math.Float64bits(f)) + return buf[:] } // createMetadataChecksum calculates the checksum of the metadata of a result. // Only the column names and types are included in the checksum. Any transaction // metadata is not included. -func createMetadataChecksum(enc *gob.Encoder, buffer *bytes.Buffer, metadata *sppb.ResultSetMetadata) (*[32]byte, error) { - buffer.Reset() +func createMetadataChecksum(metadata *sppb.ResultSetMetadata) (*[32]byte, error) { + hasher := sha256.New() for _, field := range metadata.RowType.Fields { - err := enc.Encode(field) - if err != nil { - return nil, err - } + hasher.Write(intToByte(int32Buf, len(field.Name))) + hasher.Write([]byte(field.Name)) + hasher.Write(intToByte(int32Buf, int(field.Type.Code.Number()))) } - res := sha256.Sum256(buffer.Bytes()) - return &res, nil + res := hasher.Sum(nil) + return (*[32]byte)(res), nil } // retry implements retriableStatement.retry for queries. It will execute the @@ -167,8 +210,6 @@ func createMetadataChecksum(enc *gob.Encoder, buffer *bytes.Buffer, metadata *sp // initial iterator was also returned by the new iterator, and that the errors // were returned by the same row index. func (it *checksumRowIterator) retry(ctx context.Context, tx *spanner.ReadWriteStmtBasedTransaction) error { - buffer := &bytes.Buffer{} - enc := gob.NewEncoder(buffer) retryIt := tx.QueryWithOptions(ctx, it.stmt, it.options) // If the original iterator had been stopped, we should also always stop the // new iterator. @@ -198,7 +239,7 @@ func (it *checksumRowIterator) retry(ctx context.Context, tx *spanner.ReadWriteS for n := int64(0); n < it.nc; n++ { row, err := retryIt.Next() if n == 0 && (err == nil || err == iterator.Done) { - newChecksum, checksumErr = createMetadataChecksum(enc, buffer, retryIt.Metadata) + newChecksum, checksumErr = createMetadataChecksum(retryIt.Metadata) if checksumErr != nil { return failRetry(checksumErr) } @@ -218,7 +259,7 @@ func (it *checksumRowIterator) retry(ctx context.Context, tx *spanner.ReadWriteS } return failRetry(ErrAbortedDueToConcurrentModification) } - newChecksum, err = updateChecksum(enc, buffer, newChecksum, row) + newChecksum, err = updateChecksum(newChecksum, row) if err != nil { return failRetry(err) } diff --git a/checksum_row_iterator_test.go b/checksum_row_iterator_test.go index cf31f36d..f9c659ec 100644 --- a/checksum_row_iterator_test.go +++ b/checksum_row_iterator_test.go @@ -15,8 +15,6 @@ package spannerdriver import ( - "bytes" - "encoding/gob" "math/big" "testing" "time" @@ -26,13 +24,6 @@ import ( ) func TestUpdateChecksum(t *testing.T) { - buffer1 := &bytes.Buffer{} - enc1 := gob.NewEncoder(buffer1) - buffer2 := &bytes.Buffer{} - enc2 := gob.NewEncoder(buffer2) - buffer3 := &bytes.Buffer{} - enc3 := gob.NewEncoder(buffer3) - row1, err := spanner.NewRow( []string{ "ColBool", "ColInt64", "ColFloat64", "ColNumeric", "ColString", "ColBytes", "ColDate", "ColTimestamp", "ColJson", @@ -111,17 +102,17 @@ func TestUpdateChecksum(t *testing.T) { t.Fatalf("could not create row 3: %v", err) } initial1 := new([32]byte) - checksum1, err := updateChecksum(enc1, buffer1, initial1, row1) + checksum1, err := updateChecksum(initial1, row1) if err != nil { t.Fatalf("could not calculate checksum 1: %v", err) } initial2 := new([32]byte) - checksum2, err := updateChecksum(enc2, buffer2, initial2, row2) + checksum2, err := updateChecksum(initial2, row2) if err != nil { t.Fatalf("could not calculate checksum 2: %v", err) } initial3 := new([32]byte) - checksum3, err := updateChecksum(enc3, buffer3, initial3, row3) + checksum3, err := updateChecksum(initial3, row3) if err != nil { t.Fatalf("could not calculate checksum 3: %v", err) } @@ -136,11 +127,11 @@ func TestUpdateChecksum(t *testing.T) { // Updating checksums 1 and 3 with the data from row 2 should also produce // the same checksum. - checksum1_2, err := updateChecksum(enc1, buffer1, checksum1, row2) + checksum1_2, err := updateChecksum(checksum1, row2) if err != nil { t.Fatalf("could not calculate checksum 1_2: %v", err) } - checksum3_2, err := updateChecksum(enc3, buffer3, checksum3, row2) + checksum3_2, err := updateChecksum(checksum3, row2) if err != nil { t.Fatalf("could not calculate checksum 1_2: %v", err) } @@ -150,7 +141,7 @@ func TestUpdateChecksum(t *testing.T) { // The combination of row 3 and 2 will produce a different checksum than the // combination 2 and 3, because they are in a different order. - checksum2_3, err := updateChecksum(enc2, buffer2, checksum2, row3) + checksum2_3, err := updateChecksum(checksum2, row3) if err != nil { t.Fatalf("could not calculate checksum 2_3: %v", err) } @@ -160,9 +151,6 @@ func TestUpdateChecksum(t *testing.T) { } func TestUpdateChecksumForNullValues(t *testing.T) { - buffer := &bytes.Buffer{} - enc := gob.NewEncoder(buffer) - row, err := spanner.NewRow( []string{ "ColBool", "ColInt64", "ColFloat64", "ColNumeric", "ColString", "ColBytes", "ColDate", "ColTimestamp", "ColJson", @@ -182,7 +170,7 @@ func TestUpdateChecksumForNullValues(t *testing.T) { } initial := new([32]byte) // Create the initial checksum. - checksum, err := updateChecksum(enc, buffer, initial, row) + checksum, err := updateChecksum(initial, row) if err != nil { t.Fatalf("could not calculate checksum 1: %v", err) } @@ -192,10 +180,8 @@ func TestUpdateChecksumForNullValues(t *testing.T) { t.Fatalf("checksum value should not be equal to the initial value") } // Calculating the same checksum again should yield the same result. - buffer2 := &bytes.Buffer{} - enc2 := gob.NewEncoder(buffer2) initial2 := new([32]byte) - checksum2, err := updateChecksum(enc2, buffer2, initial2, row) + checksum2, err := updateChecksum(initial2, row) if err != nil { t.Fatalf("failed to update checksum: %v", err) } @@ -203,3 +189,79 @@ func TestUpdateChecksumForNullValues(t *testing.T) { t.Fatalf("recalculated checksum does not match the initial calculation") } } + +func BenchmarkChecksumRowIterator(b *testing.B) { + row1, _ := spanner.NewRow( + []string{ + "ColBool", "ColInt64", "ColFloat64", "ColNumeric", "ColString", "ColBytes", "ColDate", "ColTimestamp", "ColJson", + "ArrBool", "ArrInt64", "ArrFloat64", "ArrNumeric", "ArrString", "ArrBytes", "ArrDate", "ArrTimestamp", "ArrJson", + }, + []interface{}{ + true, int64(1), 3.14, numeric("6.626"), "test", []byte("testbytes"), civil.Date{Year: 2021, Month: 8, Day: 5}, + time.Date(2021, 8, 5, 13, 19, 23, 123456789, time.UTC), + nullJson(true, `"key": "value", "other-key": ["value1", "value2"]}`), + []bool{true, false}, []int64{1, 2}, []float64{3.14, 6.626}, []big.Rat{numeric("3.14"), numeric("6.626")}, + []string{"test1", "test2"}, [][]byte{[]byte("testbytes1"), []byte("testbytes1")}, + []civil.Date{{Year: 2021, Month: 8, Day: 5}, {Year: 2021, Month: 8, Day: 6}}, + []time.Time{ + time.Date(2021, 8, 5, 13, 19, 23, 123456789, time.UTC), + time.Date(2021, 8, 6, 13, 19, 23, 123456789, time.UTC), + }, + []spanner.NullJSON{ + nullJson(true, `"key1": "value1", "other-key1": ["value1", "value2"]}`), + nullJson(true, `"key2": "value2", "other-key2": ["value1", "value2"]}`), + }, + }, + ) + row2, _ := spanner.NewRow( + []string{ + "ColBool", "ColInt64", "ColFloat64", "ColNumeric", "ColString", "ColBytes", "ColDate", "ColTimestamp", "ColJson", + "ArrBool", "ArrInt64", "ArrFloat64", "ArrNumeric", "ArrString", "ArrBytes", "ArrDate", "ArrTimestamp", "ArrJson", + }, + []interface{}{ + true, int64(2), 6.626, numeric("3.14"), "test2", []byte("testbytes2"), civil.Date{Year: 2020, Month: 8, Day: 5}, + time.Date(2020, 8, 5, 13, 19, 23, 123456789, time.UTC), + nullJson(true, `"key": "other-value", "other-key": ["other-value1", "other-value2"]}`), + []bool{true, false}, []int64{1, 2}, []float64{3.14, 6.626}, []big.Rat{numeric("3.14"), numeric("6.626")}, + []string{"test1_", "test2_"}, [][]byte{[]byte("testbytes1_"), []byte("testbytes1_")}, + []civil.Date{{Year: 2020, Month: 8, Day: 5}, {Year: 2020, Month: 8, Day: 6}}, + []time.Time{ + time.Date(2020, 8, 5, 13, 19, 23, 123456789, time.UTC), + time.Date(2020, 8, 6, 13, 19, 23, 123456789, time.UTC), + }, + []spanner.NullJSON{ + nullJson(true, `"key1": "other-value1", "other-key1": ["other-value1", "other-value2"]}`), + nullJson(true, `"key2": "other-value2", "other-key2": ["other-value1", "other-value2"]}`), + }, + }, + ) + row3, _ := spanner.NewRow( + []string{ + "ColBool", "ColInt64", "ColFloat64", "ColNumeric", "ColString", "ColBytes", "ColDate", "ColTimestamp", "ColJson", + "ArrBool", "ArrInt64", "ArrFloat64", "ArrNumeric", "ArrString", "ArrBytes", "ArrDate", "ArrTimestamp", "ArrJson", + }, + []interface{}{ + true, int64(1), 3.14, numeric("6.626"), "test", []byte("testbytes"), civil.Date{Year: 2021, Month: 8, Day: 5}, + time.Date(2021, 8, 5, 13, 19, 23, 123456789, time.UTC), + nullJson(true, `"key": "value", "other-key": ["value1", "value2"]}`), + []bool{true, false}, []int64{1, 2}, []float64{3.14, 6.626}, []big.Rat{numeric("3.14"), numeric("6.626")}, + []string{"test1", "test2"}, [][]byte{[]byte("testbytes1"), []byte("testbytes1")}, + []civil.Date{{Year: 2021, Month: 8, Day: 5}, {Year: 2021, Month: 8, Day: 6}}, + []time.Time{ + time.Date(2021, 8, 5, 13, 19, 23, 123456789, time.UTC), + time.Date(2021, 8, 6, 13, 19, 23, 123456789, time.UTC), + }, + []spanner.NullJSON{ + nullJson(true, `"key1": "value1", "other-key1": ["value1", "value2"]}`), + nullJson(true, `"key2": "value2", "other-key2": ["value1", "value2"]}`), + }, + }, + ) + + for b.Loop() { + initial := new([32]byte) + checksum, _ := updateChecksum(initial, row1) + checksum, _ = updateChecksum(checksum, row2) + checksum, _ = updateChecksum(checksum, row3) + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt index dafe4745..08729507 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt @@ -1,34 +1,34 @@ -# SpannerLib, 10 clients - TPCC +# SpannerLib, 4 clients - TPCC gcloud run deploy spannerlib-dotnet-benchmark-tpcc \ --region=europe-north1 \ --no-allow-unauthenticated --no-cpu-throttling \ --min-instances=1 --max-instances=1 \ --cpu=4 --memory=2Gi \ - --set-env-vars=NUM_WAREHOUSES=500,NUM_CLIENTS=10 \ + --set-env-vars=NUM_WAREHOUSES=500,NUM_CLIENTS=4 \ --base-image dotnet8 \ --source . -# SpannerLib, 10 clients - TPCC - No internal retries +# SpannerLib, 4 clients - TPCC - No internal retries gcloud run deploy spannerlib-dotnet-benchmark-tpcc-no-internal-retries \ --region=europe-north1 \ --no-allow-unauthenticated --no-cpu-throttling \ --min-instances=1 --max-instances=1 \ --cpu=4 --memory=2Gi \ - --set-env-vars=NUM_WAREHOUSES=500,NUM_CLIENTS=10,RETRY_ABORTS_INTERNALLY=false,CLIENT_TYPE=SpannerLibNoRetries \ + --set-env-vars=NUM_WAREHOUSES=500,NUM_CLIENTS=4,RETRY_ABORTS_INTERNALLY=false,CLIENT_TYPE=SpannerLibNoRetries \ --base-image dotnet8 \ --source . -# ClientLib, 10 clients - TPCC +# ClientLib, 4 clients - TPCC gcloud run deploy spannerlib-dotnet-benchmark-tpcc-client-lib \ --region=europe-north1 \ --no-allow-unauthenticated --no-cpu-throttling \ --min-instances=1 --max-instances=1 \ --cpu=4 --memory=2Gi \ - --set-env-vars=NUM_WAREHOUSES=500,NUM_CLIENTS=10,CLIENT_TYPE=ClientLib \ + --set-env-vars=NUM_WAREHOUSES=500,NUM_CLIENTS=4,CLIENT_TYPE=ClientLib \ --base-image dotnet8 \ --source . @@ -109,7 +109,7 @@ gcloud run deploy spannerlib-dotnet-benchmark-read-write-tx \ --no-allow-unauthenticated --no-cpu-throttling \ --min-instances=1 --max-instances=1 \ --cpu=4 --memory=2Gi \ - --set-env-vars=NUM_WAREHOUSES=500,NUM_CLIENTS=50,TRANSACTION_TYPE=ReadWriteTx,LOAD_DATA=false \ + --set-env-vars=NUM_WAREHOUSES=500,NUM_CLIENTS=10,TRANSACTION_TYPE=ReadWriteTx,LOAD_DATA=false \ --base-image dotnet8 \ --source . @@ -121,11 +121,22 @@ gcloud run deploy spannerlib-dotnet-benchmark-read-write-tx-client-lib \ --no-allow-unauthenticated --no-cpu-throttling \ --min-instances=1 --max-instances=1 \ --cpu=4 --memory=2Gi \ - --set-env-vars=NUM_WAREHOUSES=500,NUM_CLIENTS=50,TRANSACTION_TYPE=ReadWriteTx,CLIENT_TYPE=ClientLib,LOAD_DATA=false \ + --set-env-vars=NUM_WAREHOUSES=500,NUM_CLIENTS=10,TRANSACTION_TYPE=ReadWriteTx,CLIENT_TYPE=ClientLib,LOAD_DATA=false \ --base-image dotnet8 \ --source . +# SpannerLib, 50 clients - ReadWriteTxExp + +gcloud run deploy spannerlib-dotnet-benchmark-read-write-tx-exp \ + --region=europe-north1 \ + --no-allow-unauthenticated --no-cpu-throttling \ + --min-instances=1 --max-instances=1 \ + --cpu=4 --memory=2Gi \ + --set-env-vars=NUM_WAREHOUSES=500,NUM_CLIENTS=10,TRANSACTION_TYPE=ReadWriteTx,LOAD_DATA=false \ + --base-image dotnet8 \ + --source . + # SpannerLib, 50 clients - LargeQuery gcloud run deploy spannerlib-dotnet-benchmark-large-query \ diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/spanner-ado-net-benchmarks.csproj b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/spanner-ado-net-benchmarks.csproj index 86c91ee2..2e70b661 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/spanner-ado-net-benchmarks.csproj +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/spanner-ado-net-benchmarks.csproj @@ -11,9 +11,9 @@ - + - + diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/BasicsRunner.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/BasicsRunner.cs index 87ff7436..ea6f7187 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/BasicsRunner.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/BasicsRunner.cs @@ -139,7 +139,7 @@ private async Task ReadWriteTransactionAsync(CancellationToken cancellationToken else { await using var cmd = _connection.CreateCommand(); - cmd.CommandText = "set local transaction_tag = 'spanner-lib'"; + cmd.CommandText = "set local transaction_tag = 'spanner-lib-exp'"; await cmd.ExecuteNonQueryAsync(cancellationToken); } @@ -185,12 +185,12 @@ private async Task ReadWriteTransactionAsync(CancellationToken cancellationToken await transaction.CommitAsync(cancellationToken); watch.Stop(); - _stats.RegisterOperationLatency(Program.OperationType.ReadWriteTx, watch.Elapsed.TotalMilliseconds); + _stats.RegisterOperationLatency(Program.OperationType.ReadWriteTxExp, watch.Elapsed.TotalMilliseconds); break; } catch (Exception exception) { - _stats.RegisterFailedOperation(Program.OperationType.ReadWriteTx, watch.Elapsed, exception); + _stats.RegisterFailedOperation(Program.OperationType.ReadWriteTxExp, watch.Elapsed, exception); } } } diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Program.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Program.cs index b295b664..e4765c12 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Program.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Program.cs @@ -42,6 +42,7 @@ public enum OperationType Scalar, PointDml, ReadWriteTx, + ReadWriteTxExp, NewOrder, Payment, OrderStatus, diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/TransactionTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/TransactionTests.cs index 70bcedd5..2685a777 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/TransactionTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/TransactionTests.cs @@ -500,35 +500,39 @@ public async Task DbConnectionIsolationLevel() await using var cmd = dbConn.CreateCommand(); cmd.CommandText = "set local transaction_tag = 'spanner-lib'"; await cmd.ExecuteNonQueryAsync(cancellationToken); - - await using var selectCommand = dbConn.CreateCommand(); - selectCommand.CommandText = selectSql; - selectCommand.Transaction = transaction; - await using var reader = await selectCommand.ExecuteReaderAsync(cancellationToken); - var foundRows = 0; - while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) - { - foundRows++; - } - if (foundRows != 1) - { - throw new InvalidOperationException("Unexpected found rows: " + foundRows); - } - await using var updateCommand = dbConn.CreateCommand(); - updateCommand.CommandText = insertSql; - updateCommand.Transaction = transaction; - var updated = await updateCommand.ExecuteNonQueryAsync(cancellationToken); - if (updated != 1) + for (var i = 0; i < 3; i++) { - throw new InvalidOperationException("Unexpected affected rows: " + updated); + await using var selectCommand = dbConn.CreateCommand(); + selectCommand.CommandText = selectSql; + selectCommand.Transaction = transaction; + await using var reader = await selectCommand.ExecuteReaderAsync(cancellationToken); + var foundRows = 0; + while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + foundRows++; + } + + if (foundRows != 1) + { + throw new InvalidOperationException("Unexpected found rows: " + foundRows); + } + + await using var updateCommand = dbConn.CreateCommand(); + updateCommand.CommandText = insertSql; + updateCommand.Transaction = transaction; + var updated = await updateCommand.ExecuteNonQueryAsync(cancellationToken); + if (updated != 1) + { + throw new InvalidOperationException("Unexpected affected rows: " + updated); + } } await transaction.CommitAsync(cancellationToken); + var firstRequest = Fixture.SpannerMock.Requests.OfType().First(); + Assert.That(firstRequest.Transaction.Begin.IsolationLevel, Is.EqualTo(TransactionOptions.Types.IsolationLevel.RepeatableRead)); var requests = Fixture.SpannerMock.Requests; - Console.WriteLine(requests); - var request = Fixture.SpannerMock.Requests.OfType().First(); - Assert.That(request.Transaction.Begin.IsolationLevel, Is.EqualTo(TransactionOptions.Types.IsolationLevel.RepeatableRead)); + Assert.That(requests.OfType().Count(), Is.EqualTo(6)); } [Test] diff --git a/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj b/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj index ac614072..63c15528 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj +++ b/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj @@ -9,7 +9,7 @@ Alpha.Google.Cloud.Spanner.DataProvider .NET Data Provider for Spanner Google - 1.0.0-alpha.20251207191847 + 1.0.0-alpha.20251209081138 ADO.NET Data Provider. Alpha version: Not for production use @@ -19,10 +19,10 @@ Alpha version: Not for production use - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/spannerlib-dotnet-grpc-impl.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/spannerlib-dotnet-grpc-impl.csproj index 67fec147..8be5cd74 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/spannerlib-dotnet-grpc-impl.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/spannerlib-dotnet-grpc-impl.csproj @@ -6,13 +6,13 @@ enable default Alpha.Google.Cloud.SpannerLib.GrpcImpl - 1.0.0-alpha.20251207191144 + 1.0.0-alpha.20251208195113 Google - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-server/spannerlib-dotnet-grpc-server.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-server/spannerlib-dotnet-grpc-server.csproj index 53475b7a..364a0387 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-server/spannerlib-dotnet-grpc-server.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-server/spannerlib-dotnet-grpc-server.csproj @@ -8,7 +8,7 @@ Alpha.Google.Cloud.SpannerLib.GrpcServer SpannerLib Grpc Server Google - 1.0.0-alpha.20251207191144 + 1.0.0-alpha.20251208195113 diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-v1/spannerlib-dotnet-grpc-v1.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-v1/spannerlib-dotnet-grpc-v1.csproj index 26475e2c..f2128792 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-v1/spannerlib-dotnet-grpc-v1.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-v1/spannerlib-dotnet-grpc-v1.csproj @@ -6,7 +6,7 @@ enable default Alpha.Google.Cloud.SpannerLib.Grpc.V1 - 1.0.0-alpha.20251207191144 + 1.0.0-alpha.20251208195113 Google diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/spannerlib-dotnet-mockserver.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/spannerlib-dotnet-mockserver.csproj index b605f431..0a07b579 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/spannerlib-dotnet-mockserver.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/spannerlib-dotnet-mockserver.csproj @@ -6,7 +6,7 @@ enable enable Alpha.Google.Cloud.SpannerLib.MockServer - 1.0.0-alpha.20251207191144 + 1.0.0-alpha.20251208195113 Google default diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native-impl/spannerlib-dotnet-native-impl.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native-impl/spannerlib-dotnet-native-impl.csproj index 5d18cb1c..46f3f5b6 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native-impl/spannerlib-dotnet-native-impl.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native-impl/spannerlib-dotnet-native-impl.csproj @@ -6,7 +6,7 @@ enable default Alpha.Google.Cloud.SpannerLib.NativeImpl - 1.0.0-alpha.20251207191144 + 1.0.0-alpha.20251208195113 Google @@ -16,7 +16,7 @@ - + diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native/spannerlib-dotnet-native.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native/spannerlib-dotnet-native.csproj index d8be70f9..79ec180f 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native/spannerlib-dotnet-native.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native/spannerlib-dotnet-native.csproj @@ -9,7 +9,7 @@ Alpha.Google.Cloud.SpannerLib.Native .NET wrapper for the native SpannerLib shared library Google - 1.0.0-alpha.20251207191144 + 1.0.0-alpha.20251208195113 diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/spannerlib-dotnet.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/spannerlib-dotnet.csproj index bb038a3e..722f3e16 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/spannerlib-dotnet.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/spannerlib-dotnet.csproj @@ -6,7 +6,7 @@ enable default Alpha.Google.Cloud.SpannerLib - 1.0.0-alpha.20251207191144 + 1.0.0-alpha.20251208195113 Google diff --git a/transaction.go b/transaction.go index 041c84a3..37dc077d 100644 --- a/transaction.go +++ b/transaction.go @@ -15,10 +15,8 @@ package spannerdriver import ( - "bytes" "context" "database/sql/driver" - "encoding/gob" "fmt" "log/slog" "math/rand" @@ -625,7 +623,6 @@ func (tx *readWriteTransaction) Query(ctx context.Context, stmt spanner.Statemen // If retries are enabled, we need to use a row iterator that will keep // track of a running checksum of all the results that we see. - buffer := &bytes.Buffer{} it := &checksumRowIterator{ RowIterator: tx.rwTx.QueryWithOptions(ctx, stmt, execOptions.QueryOptions), ctx: ctx, @@ -633,8 +630,6 @@ func (tx *readWriteTransaction) Query(ctx context.Context, stmt spanner.Statemen stmt: stmt, stmtType: stmtType, options: execOptions.QueryOptions, - buffer: buffer, - enc: gob.NewEncoder(buffer), } tx.statements = append(tx.statements, it) return it, nil From 39c5c2567193db6b7f13979caedff4bc7ae19d0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Mon, 15 Dec 2025 14:02:04 +0100 Subject: [PATCH 23/29] chore: cleanup and fix tests --- conn.go | 7 - .../spanner-ado-net/SpannerConnection.cs | 2 +- spannerlib/api/connection.go | 33 +- spannerlib/api/rows.go | 1 - .../StreamingRows.cs | 7 +- .../spannerlib/v1/ExecuteAllResponse.java | 720 ----------------- .../v1/ExecuteAllResponseOrBuilder.java | 36 - .../v1/ExecuteStreamingRequest.java | 744 ------------------ .../v1/ExecuteStreamingRequestOrBuilder.java | 42 - .../spannerlib/v1/NextResultSetResponse.java | 626 --------------- .../v1/NextResultSetResponseOrBuilder.java | 33 - 11 files changed, 16 insertions(+), 2235 deletions(-) delete mode 100644 spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteAllResponse.java delete mode 100644 spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteAllResponseOrBuilder.java delete mode 100644 spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteStreamingRequest.java delete mode 100644 spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteStreamingRequestOrBuilder.java delete mode 100644 spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextResultSetResponse.java delete mode 100644 spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextResultSetResponseOrBuilder.java diff --git a/conn.go b/conn.go index f92d1c3b..7ea700a8 100644 --- a/conn.go +++ b/conn.go @@ -226,9 +226,6 @@ type SpannerConn interface { // DetectStatementType returns the type of SQL statement. DetectStatementType(query string) parser.StatementType - // Parser returns the parser.StatementParser that is used for this connection. - Parser() *parser.StatementParser - // resetTransactionForRetry resets the current transaction after it has // been aborted by Spanner. Calling this function on a transaction that // has not been aborted is not supported and will cause an error to be @@ -299,10 +296,6 @@ func (c *conn) DetectStatementType(query string) parser.StatementType { return info.StatementType } -func (c *conn) Parser() *parser.StatementParser { - return c.parser -} - func (c *conn) CommitTimestamp() (time.Time, error) { ts := propertyCommitTimestamp.GetValueOrDefault(c.state) if ts == nil { diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs index fee8bacb..4e552a22 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs @@ -430,7 +430,7 @@ protected override DbBatch CreateDbBatch() return new SpannerBatch(this); } - public long[] ExecuteBatch(IEnumerable statements) + public long[] ExecuteBatch(List statements) { EnsureOpen(); _transaction?.MarkUsed(); diff --git a/spannerlib/api/connection.go b/spannerlib/api/connection.go index 1e997691..43b56233 100644 --- a/spannerlib/api/connection.go +++ b/spannerlib/api/connection.go @@ -361,14 +361,11 @@ func executeBatch(ctx context.Context, conn *Connection, executor queryExecutor, } func executeBatchDdl(ctx context.Context, conn *Connection, executor queryExecutor, statements []*spannerpb.ExecuteBatchDmlRequest_Statement) (*spannerpb.ExecuteBatchDmlResponse, error) { - useExplicitBatch := len(statements) > 1 - if useExplicitBatch { - if err := conn.backend.Raw(func(driverConn any) error { - spannerConn, _ := driverConn.(spannerdriver.SpannerConn) - return spannerConn.StartBatchDDL() - }); err != nil { - return nil, err - } + if err := conn.backend.Raw(func(driverConn any) error { + spannerConn, _ := driverConn.(spannerdriver.SpannerConn) + return spannerConn.StartBatchDDL() + }); err != nil { + return nil, err } for _, statement := range statements { _, err := executor.ExecContext(ctx, statement.Sql) @@ -376,14 +373,12 @@ func executeBatchDdl(ctx context.Context, conn *Connection, executor queryExecut return nil, err } } - if useExplicitBatch { - // TODO: Add support for getting the actual Batch DDL response. - if err := conn.backend.Raw(func(driverConn any) (err error) { - spannerConn, _ := driverConn.(spannerdriver.SpannerConn) - return spannerConn.RunBatch(ctx) - }); err != nil { - return nil, err - } + // TODO: Add support for getting the actual Batch DDL response. + if err := conn.backend.Raw(func(driverConn any) (err error) { + spannerConn, _ := driverConn.(spannerdriver.SpannerConn) + return spannerConn.RunBatch(ctx) + }); err != nil { + return nil, err } response := spannerpb.ExecuteBatchDmlResponse{} @@ -513,12 +508,6 @@ func determineBatchType(conn *Connection, statements []*spannerpb.ExecuteBatchDm if err := conn.backend.Raw(func(driverConn any) error { spannerConn, _ := driverConn.(spannerdriver.SpannerConn) firstStatementType := spannerConn.DetectStatementType(statements[0].Sql) - // As a special case, we allow the first statement in a batch to be a CREATE DATABASE statement. This will - // then trigger a CreateDatabase operation with the remaining DDL statements in this batch to be used as the - // ExtraStatements for the CreateDatabase operation. - if spannerConn.Parser().IsCreateDatabaseStatement(statements[0].Sql) { - firstStatementType = parser.StatementTypeDdl - } if firstStatementType == parser.StatementTypeDml { batchType = parser.BatchTypeDml } else if firstStatementType == parser.StatementTypeDdl { diff --git a/spannerlib/api/rows.go b/spannerlib/api/rows.go index 2193b6b5..1935836e 100644 --- a/spannerlib/api/rows.go +++ b/spannerlib/api/rows.go @@ -116,7 +116,6 @@ func next(ctx context.Context, poolId, connId, rowsId int64, marshalResult, rese rows.buffer = nil } if !marshalResult || values == nil { - rows.buffer = nil return values, nil, nil } diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/StreamingRows.cs b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/StreamingRows.cs index 40257593..56e54a0e 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/StreamingRows.cs +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/StreamingRows.cs @@ -133,8 +133,9 @@ private void MarkDone() _done = true; } - private bool TryNextCached(out ListValue? result) + private bool TryNextCached(out ListValue? result, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); if (_pendingNextResultSetCall || _done) { result = null; @@ -156,7 +157,7 @@ private bool TryNextCached(out ListValue? result) public override ListValue? Next() { - if (TryNextCached(out var result)) + if (TryNextCached(out var result, CancellationToken.None)) { return result; } @@ -200,7 +201,7 @@ private bool TryNextCached(out ListValue? result) public override async Task NextAsync(CancellationToken cancellationToken = default) { - if (TryNextCached(out var result)) + if (TryNextCached(out var result, cancellationToken)) { return result; } diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteAllResponse.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteAllResponse.java deleted file mode 100644 index 1d55c691..00000000 --- a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteAllResponse.java +++ /dev/null @@ -1,720 +0,0 @@ -// Generated by the protocol buffer compiler. DO NOT EDIT! -// NO CHECKED-IN PROTOBUF GENCODE -// source: google/spannerlib/v1/spannerlib.proto -// Protobuf Java Version: 4.32.1 - -package com.google.cloud.spannerlib.v1; - -/** - * Protobuf type {@code google.spannerlib.v1.ExecuteAllResponse} - */ -@com.google.protobuf.Generated -public final class ExecuteAllResponse extends - com.google.protobuf.GeneratedMessage implements - // @@protoc_insertion_point(message_implements:google.spannerlib.v1.ExecuteAllResponse) - ExecuteAllResponseOrBuilder { -private static final long serialVersionUID = 0L; - static { - com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( - com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, - /* major= */ 4, - /* minor= */ 32, - /* patch= */ 1, - /* suffix= */ "", - ExecuteAllResponse.class.getName()); - } - // Use ExecuteAllResponse.newBuilder() to construct. - private ExecuteAllResponse(com.google.protobuf.GeneratedMessage.Builder builder) { - super(builder); - } - private ExecuteAllResponse() { - resultSets_ = java.util.Collections.emptyList(); - } - - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteAllResponse_descriptor; - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable - internalGetFieldAccessorTable() { - return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteAllResponse_fieldAccessorTable - .ensureFieldAccessorsInitialized( - com.google.cloud.spannerlib.v1.ExecuteAllResponse.class, com.google.cloud.spannerlib.v1.ExecuteAllResponse.Builder.class); - } - - public static final int RESULT_SETS_FIELD_NUMBER = 1; - @SuppressWarnings("serial") - private java.util.List resultSets_; - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - @java.lang.Override - public java.util.List getResultSetsList() { - return resultSets_; - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - @java.lang.Override - public java.util.List - getResultSetsOrBuilderList() { - return resultSets_; - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - @java.lang.Override - public int getResultSetsCount() { - return resultSets_.size(); - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - @java.lang.Override - public com.google.spanner.v1.ResultSet getResultSets(int index) { - return resultSets_.get(index); - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - @java.lang.Override - public com.google.spanner.v1.ResultSetOrBuilder getResultSetsOrBuilder( - int index) { - return resultSets_.get(index); - } - - private byte memoizedIsInitialized = -1; - @java.lang.Override - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) return true; - if (isInitialized == 0) return false; - - memoizedIsInitialized = 1; - return true; - } - - @java.lang.Override - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - for (int i = 0; i < resultSets_.size(); i++) { - output.writeMessage(1, resultSets_.get(i)); - } - getUnknownFields().writeTo(output); - } - - @java.lang.Override - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) return size; - - size = 0; - for (int i = 0; i < resultSets_.size(); i++) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(1, resultSets_.get(i)); - } - size += getUnknownFields().getSerializedSize(); - memoizedSize = size; - return size; - } - - @java.lang.Override - public boolean equals(final java.lang.Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof com.google.cloud.spannerlib.v1.ExecuteAllResponse)) { - return super.equals(obj); - } - com.google.cloud.spannerlib.v1.ExecuteAllResponse other = (com.google.cloud.spannerlib.v1.ExecuteAllResponse) obj; - - if (!getResultSetsList() - .equals(other.getResultSetsList())) return false; - if (!getUnknownFields().equals(other.getUnknownFields())) return false; - return true; - } - - @java.lang.Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptor().hashCode(); - if (getResultSetsCount() > 0) { - hash = (37 * hash) + RESULT_SETS_FIELD_NUMBER; - hash = (53 * hash) + getResultSetsList().hashCode(); - } - hash = (29 * hash) + getUnknownFields().hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseFrom( - java.nio.ByteBuffer data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseFrom( - java.nio.ByteBuffer data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseFrom( - com.google.protobuf.ByteString data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseFrom(byte[] data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseDelimitedFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input); - } - - public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseFrom( - com.google.protobuf.CodedInputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - public static com.google.cloud.spannerlib.v1.ExecuteAllResponse parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - @java.lang.Override - public Builder newBuilderForType() { return newBuilder(); } - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - public static Builder newBuilder(com.google.cloud.spannerlib.v1.ExecuteAllResponse prototype) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - @java.lang.Override - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() : new Builder().mergeFrom(this); - } - - @java.lang.Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessage.BuilderParent parent) { - Builder builder = new Builder(parent); - return builder; - } - /** - * Protobuf type {@code google.spannerlib.v1.ExecuteAllResponse} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessage.Builder implements - // @@protoc_insertion_point(builder_implements:google.spannerlib.v1.ExecuteAllResponse) - com.google.cloud.spannerlib.v1.ExecuteAllResponseOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteAllResponse_descriptor; - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable - internalGetFieldAccessorTable() { - return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteAllResponse_fieldAccessorTable - .ensureFieldAccessorsInitialized( - com.google.cloud.spannerlib.v1.ExecuteAllResponse.class, com.google.cloud.spannerlib.v1.ExecuteAllResponse.Builder.class); - } - - // Construct using com.google.cloud.spannerlib.v1.ExecuteAllResponse.newBuilder() - private Builder() { - - } - - private Builder( - com.google.protobuf.GeneratedMessage.BuilderParent parent) { - super(parent); - - } - @java.lang.Override - public Builder clear() { - super.clear(); - bitField0_ = 0; - if (resultSetsBuilder_ == null) { - resultSets_ = java.util.Collections.emptyList(); - } else { - resultSets_ = null; - resultSetsBuilder_.clear(); - } - bitField0_ = (bitField0_ & ~0x00000001); - return this; - } - - @java.lang.Override - public com.google.protobuf.Descriptors.Descriptor - getDescriptorForType() { - return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteAllResponse_descriptor; - } - - @java.lang.Override - public com.google.cloud.spannerlib.v1.ExecuteAllResponse getDefaultInstanceForType() { - return com.google.cloud.spannerlib.v1.ExecuteAllResponse.getDefaultInstance(); - } - - @java.lang.Override - public com.google.cloud.spannerlib.v1.ExecuteAllResponse build() { - com.google.cloud.spannerlib.v1.ExecuteAllResponse result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - @java.lang.Override - public com.google.cloud.spannerlib.v1.ExecuteAllResponse buildPartial() { - com.google.cloud.spannerlib.v1.ExecuteAllResponse result = new com.google.cloud.spannerlib.v1.ExecuteAllResponse(this); - buildPartialRepeatedFields(result); - if (bitField0_ != 0) { buildPartial0(result); } - onBuilt(); - return result; - } - - private void buildPartialRepeatedFields(com.google.cloud.spannerlib.v1.ExecuteAllResponse result) { - if (resultSetsBuilder_ == null) { - if (((bitField0_ & 0x00000001) != 0)) { - resultSets_ = java.util.Collections.unmodifiableList(resultSets_); - bitField0_ = (bitField0_ & ~0x00000001); - } - result.resultSets_ = resultSets_; - } else { - result.resultSets_ = resultSetsBuilder_.build(); - } - } - - private void buildPartial0(com.google.cloud.spannerlib.v1.ExecuteAllResponse result) { - int from_bitField0_ = bitField0_; - } - - @java.lang.Override - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof com.google.cloud.spannerlib.v1.ExecuteAllResponse) { - return mergeFrom((com.google.cloud.spannerlib.v1.ExecuteAllResponse)other); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom(com.google.cloud.spannerlib.v1.ExecuteAllResponse other) { - if (other == com.google.cloud.spannerlib.v1.ExecuteAllResponse.getDefaultInstance()) return this; - if (resultSetsBuilder_ == null) { - if (!other.resultSets_.isEmpty()) { - if (resultSets_.isEmpty()) { - resultSets_ = other.resultSets_; - bitField0_ = (bitField0_ & ~0x00000001); - } else { - ensureResultSetsIsMutable(); - resultSets_.addAll(other.resultSets_); - } - onChanged(); - } - } else { - if (!other.resultSets_.isEmpty()) { - if (resultSetsBuilder_.isEmpty()) { - resultSetsBuilder_.dispose(); - resultSetsBuilder_ = null; - resultSets_ = other.resultSets_; - bitField0_ = (bitField0_ & ~0x00000001); - resultSetsBuilder_ = - com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ? - internalGetResultSetsFieldBuilder() : null; - } else { - resultSetsBuilder_.addAllMessages(other.resultSets_); - } - } - } - this.mergeUnknownFields(other.getUnknownFields()); - onChanged(); - return this; - } - - @java.lang.Override - public final boolean isInitialized() { - return true; - } - - @java.lang.Override - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - if (extensionRegistry == null) { - throw new java.lang.NullPointerException(); - } - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - case 10: { - com.google.spanner.v1.ResultSet m = - input.readMessage( - com.google.spanner.v1.ResultSet.parser(), - extensionRegistry); - if (resultSetsBuilder_ == null) { - ensureResultSetsIsMutable(); - resultSets_.add(m); - } else { - resultSetsBuilder_.addMessage(m); - } - break; - } // case 10 - default: { - if (!super.parseUnknownField(input, extensionRegistry, tag)) { - done = true; // was an endgroup tag - } - break; - } // default: - } // switch (tag) - } // while (!done) - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.unwrapIOException(); - } finally { - onChanged(); - } // finally - return this; - } - private int bitField0_; - - private java.util.List resultSets_ = - java.util.Collections.emptyList(); - private void ensureResultSetsIsMutable() { - if (!((bitField0_ & 0x00000001) != 0)) { - resultSets_ = new java.util.ArrayList(resultSets_); - bitField0_ |= 0x00000001; - } - } - - private com.google.protobuf.RepeatedFieldBuilder< - com.google.spanner.v1.ResultSet, com.google.spanner.v1.ResultSet.Builder, com.google.spanner.v1.ResultSetOrBuilder> resultSetsBuilder_; - - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - public java.util.List getResultSetsList() { - if (resultSetsBuilder_ == null) { - return java.util.Collections.unmodifiableList(resultSets_); - } else { - return resultSetsBuilder_.getMessageList(); - } - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - public int getResultSetsCount() { - if (resultSetsBuilder_ == null) { - return resultSets_.size(); - } else { - return resultSetsBuilder_.getCount(); - } - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - public com.google.spanner.v1.ResultSet getResultSets(int index) { - if (resultSetsBuilder_ == null) { - return resultSets_.get(index); - } else { - return resultSetsBuilder_.getMessage(index); - } - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - public Builder setResultSets( - int index, com.google.spanner.v1.ResultSet value) { - if (resultSetsBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureResultSetsIsMutable(); - resultSets_.set(index, value); - onChanged(); - } else { - resultSetsBuilder_.setMessage(index, value); - } - return this; - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - public Builder setResultSets( - int index, com.google.spanner.v1.ResultSet.Builder builderForValue) { - if (resultSetsBuilder_ == null) { - ensureResultSetsIsMutable(); - resultSets_.set(index, builderForValue.build()); - onChanged(); - } else { - resultSetsBuilder_.setMessage(index, builderForValue.build()); - } - return this; - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - public Builder addResultSets(com.google.spanner.v1.ResultSet value) { - if (resultSetsBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureResultSetsIsMutable(); - resultSets_.add(value); - onChanged(); - } else { - resultSetsBuilder_.addMessage(value); - } - return this; - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - public Builder addResultSets( - int index, com.google.spanner.v1.ResultSet value) { - if (resultSetsBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureResultSetsIsMutable(); - resultSets_.add(index, value); - onChanged(); - } else { - resultSetsBuilder_.addMessage(index, value); - } - return this; - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - public Builder addResultSets( - com.google.spanner.v1.ResultSet.Builder builderForValue) { - if (resultSetsBuilder_ == null) { - ensureResultSetsIsMutable(); - resultSets_.add(builderForValue.build()); - onChanged(); - } else { - resultSetsBuilder_.addMessage(builderForValue.build()); - } - return this; - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - public Builder addResultSets( - int index, com.google.spanner.v1.ResultSet.Builder builderForValue) { - if (resultSetsBuilder_ == null) { - ensureResultSetsIsMutable(); - resultSets_.add(index, builderForValue.build()); - onChanged(); - } else { - resultSetsBuilder_.addMessage(index, builderForValue.build()); - } - return this; - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - public Builder addAllResultSets( - java.lang.Iterable values) { - if (resultSetsBuilder_ == null) { - ensureResultSetsIsMutable(); - com.google.protobuf.AbstractMessageLite.Builder.addAll( - values, resultSets_); - onChanged(); - } else { - resultSetsBuilder_.addAllMessages(values); - } - return this; - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - public Builder clearResultSets() { - if (resultSetsBuilder_ == null) { - resultSets_ = java.util.Collections.emptyList(); - bitField0_ = (bitField0_ & ~0x00000001); - onChanged(); - } else { - resultSetsBuilder_.clear(); - } - return this; - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - public Builder removeResultSets(int index) { - if (resultSetsBuilder_ == null) { - ensureResultSetsIsMutable(); - resultSets_.remove(index); - onChanged(); - } else { - resultSetsBuilder_.remove(index); - } - return this; - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - public com.google.spanner.v1.ResultSet.Builder getResultSetsBuilder( - int index) { - return internalGetResultSetsFieldBuilder().getBuilder(index); - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - public com.google.spanner.v1.ResultSetOrBuilder getResultSetsOrBuilder( - int index) { - if (resultSetsBuilder_ == null) { - return resultSets_.get(index); } else { - return resultSetsBuilder_.getMessageOrBuilder(index); - } - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - public java.util.List - getResultSetsOrBuilderList() { - if (resultSetsBuilder_ != null) { - return resultSetsBuilder_.getMessageOrBuilderList(); - } else { - return java.util.Collections.unmodifiableList(resultSets_); - } - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - public com.google.spanner.v1.ResultSet.Builder addResultSetsBuilder() { - return internalGetResultSetsFieldBuilder().addBuilder( - com.google.spanner.v1.ResultSet.getDefaultInstance()); - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - public com.google.spanner.v1.ResultSet.Builder addResultSetsBuilder( - int index) { - return internalGetResultSetsFieldBuilder().addBuilder( - index, com.google.spanner.v1.ResultSet.getDefaultInstance()); - } - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - public java.util.List - getResultSetsBuilderList() { - return internalGetResultSetsFieldBuilder().getBuilderList(); - } - private com.google.protobuf.RepeatedFieldBuilder< - com.google.spanner.v1.ResultSet, com.google.spanner.v1.ResultSet.Builder, com.google.spanner.v1.ResultSetOrBuilder> - internalGetResultSetsFieldBuilder() { - if (resultSetsBuilder_ == null) { - resultSetsBuilder_ = new com.google.protobuf.RepeatedFieldBuilder< - com.google.spanner.v1.ResultSet, com.google.spanner.v1.ResultSet.Builder, com.google.spanner.v1.ResultSetOrBuilder>( - resultSets_, - ((bitField0_ & 0x00000001) != 0), - getParentForChildren(), - isClean()); - resultSets_ = null; - } - return resultSetsBuilder_; - } - - // @@protoc_insertion_point(builder_scope:google.spannerlib.v1.ExecuteAllResponse) - } - - // @@protoc_insertion_point(class_scope:google.spannerlib.v1.ExecuteAllResponse) - private static final com.google.cloud.spannerlib.v1.ExecuteAllResponse DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new com.google.cloud.spannerlib.v1.ExecuteAllResponse(); - } - - public static com.google.cloud.spannerlib.v1.ExecuteAllResponse getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser - PARSER = new com.google.protobuf.AbstractParser() { - @java.lang.Override - public ExecuteAllResponse parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - Builder builder = newBuilder(); - try { - builder.mergeFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(builder.buildPartial()); - } catch (com.google.protobuf.UninitializedMessageException e) { - throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException(e) - .setUnfinishedMessage(builder.buildPartial()); - } - return builder.buildPartial(); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @java.lang.Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - @java.lang.Override - public com.google.cloud.spannerlib.v1.ExecuteAllResponse getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - -} - diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteAllResponseOrBuilder.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteAllResponseOrBuilder.java deleted file mode 100644 index 1369da1c..00000000 --- a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteAllResponseOrBuilder.java +++ /dev/null @@ -1,36 +0,0 @@ -// Generated by the protocol buffer compiler. DO NOT EDIT! -// NO CHECKED-IN PROTOBUF GENCODE -// source: google/spannerlib/v1/spannerlib.proto -// Protobuf Java Version: 4.32.1 - -package com.google.cloud.spannerlib.v1; - -@com.google.protobuf.Generated -public interface ExecuteAllResponseOrBuilder extends - // @@protoc_insertion_point(interface_extends:google.spannerlib.v1.ExecuteAllResponse) - com.google.protobuf.MessageOrBuilder { - - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - java.util.List - getResultSetsList(); - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - com.google.spanner.v1.ResultSet getResultSets(int index); - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - int getResultSetsCount(); - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - java.util.List - getResultSetsOrBuilderList(); - /** - * repeated .google.spanner.v1.ResultSet result_sets = 1; - */ - com.google.spanner.v1.ResultSetOrBuilder getResultSetsOrBuilder( - int index); -} diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteStreamingRequest.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteStreamingRequest.java deleted file mode 100644 index d8bb7d1a..00000000 --- a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteStreamingRequest.java +++ /dev/null @@ -1,744 +0,0 @@ -// Generated by the protocol buffer compiler. DO NOT EDIT! -// NO CHECKED-IN PROTOBUF GENCODE -// source: google/spannerlib/v1/spannerlib.proto -// Protobuf Java Version: 4.32.1 - -package com.google.cloud.spannerlib.v1; - -/** - * Protobuf type {@code google.spannerlib.v1.ExecuteStreamingRequest} - */ -@com.google.protobuf.Generated -public final class ExecuteStreamingRequest extends - com.google.protobuf.GeneratedMessage implements - // @@protoc_insertion_point(message_implements:google.spannerlib.v1.ExecuteStreamingRequest) - ExecuteStreamingRequestOrBuilder { -private static final long serialVersionUID = 0L; - static { - com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( - com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, - /* major= */ 4, - /* minor= */ 32, - /* patch= */ 1, - /* suffix= */ "", - ExecuteStreamingRequest.class.getName()); - } - // Use ExecuteStreamingRequest.newBuilder() to construct. - private ExecuteStreamingRequest(com.google.protobuf.GeneratedMessage.Builder builder) { - super(builder); - } - private ExecuteStreamingRequest() { - } - - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteStreamingRequest_descriptor; - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable - internalGetFieldAccessorTable() { - return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteStreamingRequest_fieldAccessorTable - .ensureFieldAccessorsInitialized( - com.google.cloud.spannerlib.v1.ExecuteStreamingRequest.class, com.google.cloud.spannerlib.v1.ExecuteStreamingRequest.Builder.class); - } - - private int bitField0_; - public static final int CONNECTION_FIELD_NUMBER = 1; - private com.google.cloud.spannerlib.v1.Connection connection_; - /** - * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; - * @return Whether the connection field is set. - */ - @java.lang.Override - public boolean hasConnection() { - return ((bitField0_ & 0x00000001) != 0); - } - /** - * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; - * @return The connection. - */ - @java.lang.Override - public com.google.cloud.spannerlib.v1.Connection getConnection() { - return connection_ == null ? com.google.cloud.spannerlib.v1.Connection.getDefaultInstance() : connection_; - } - /** - * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; - */ - @java.lang.Override - public com.google.cloud.spannerlib.v1.ConnectionOrBuilder getConnectionOrBuilder() { - return connection_ == null ? com.google.cloud.spannerlib.v1.Connection.getDefaultInstance() : connection_; - } - - public static final int EXECUTE_SQL_REQUEST_FIELD_NUMBER = 2; - private com.google.spanner.v1.ExecuteSqlRequest executeSqlRequest_; - /** - * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; - * @return Whether the executeSqlRequest field is set. - */ - @java.lang.Override - public boolean hasExecuteSqlRequest() { - return ((bitField0_ & 0x00000002) != 0); - } - /** - * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; - * @return The executeSqlRequest. - */ - @java.lang.Override - public com.google.spanner.v1.ExecuteSqlRequest getExecuteSqlRequest() { - return executeSqlRequest_ == null ? com.google.spanner.v1.ExecuteSqlRequest.getDefaultInstance() : executeSqlRequest_; - } - /** - * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; - */ - @java.lang.Override - public com.google.spanner.v1.ExecuteSqlRequestOrBuilder getExecuteSqlRequestOrBuilder() { - return executeSqlRequest_ == null ? com.google.spanner.v1.ExecuteSqlRequest.getDefaultInstance() : executeSqlRequest_; - } - - private byte memoizedIsInitialized = -1; - @java.lang.Override - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) return true; - if (isInitialized == 0) return false; - - memoizedIsInitialized = 1; - return true; - } - - @java.lang.Override - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (((bitField0_ & 0x00000001) != 0)) { - output.writeMessage(1, getConnection()); - } - if (((bitField0_ & 0x00000002) != 0)) { - output.writeMessage(2, getExecuteSqlRequest()); - } - getUnknownFields().writeTo(output); - } - - @java.lang.Override - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) return size; - - size = 0; - if (((bitField0_ & 0x00000001) != 0)) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(1, getConnection()); - } - if (((bitField0_ & 0x00000002) != 0)) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(2, getExecuteSqlRequest()); - } - size += getUnknownFields().getSerializedSize(); - memoizedSize = size; - return size; - } - - @java.lang.Override - public boolean equals(final java.lang.Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof com.google.cloud.spannerlib.v1.ExecuteStreamingRequest)) { - return super.equals(obj); - } - com.google.cloud.spannerlib.v1.ExecuteStreamingRequest other = (com.google.cloud.spannerlib.v1.ExecuteStreamingRequest) obj; - - if (hasConnection() != other.hasConnection()) return false; - if (hasConnection()) { - if (!getConnection() - .equals(other.getConnection())) return false; - } - if (hasExecuteSqlRequest() != other.hasExecuteSqlRequest()) return false; - if (hasExecuteSqlRequest()) { - if (!getExecuteSqlRequest() - .equals(other.getExecuteSqlRequest())) return false; - } - if (!getUnknownFields().equals(other.getUnknownFields())) return false; - return true; - } - - @java.lang.Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptor().hashCode(); - if (hasConnection()) { - hash = (37 * hash) + CONNECTION_FIELD_NUMBER; - hash = (53 * hash) + getConnection().hashCode(); - } - if (hasExecuteSqlRequest()) { - hash = (37 * hash) + EXECUTE_SQL_REQUEST_FIELD_NUMBER; - hash = (53 * hash) + getExecuteSqlRequest().hashCode(); - } - hash = (29 * hash) + getUnknownFields().hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseFrom( - java.nio.ByteBuffer data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseFrom( - java.nio.ByteBuffer data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseFrom( - com.google.protobuf.ByteString data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseFrom(byte[] data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseDelimitedFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input); - } - - public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseFrom( - com.google.protobuf.CodedInputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - @java.lang.Override - public Builder newBuilderForType() { return newBuilder(); } - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - public static Builder newBuilder(com.google.cloud.spannerlib.v1.ExecuteStreamingRequest prototype) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - @java.lang.Override - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() : new Builder().mergeFrom(this); - } - - @java.lang.Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessage.BuilderParent parent) { - Builder builder = new Builder(parent); - return builder; - } - /** - * Protobuf type {@code google.spannerlib.v1.ExecuteStreamingRequest} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessage.Builder implements - // @@protoc_insertion_point(builder_implements:google.spannerlib.v1.ExecuteStreamingRequest) - com.google.cloud.spannerlib.v1.ExecuteStreamingRequestOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteStreamingRequest_descriptor; - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable - internalGetFieldAccessorTable() { - return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteStreamingRequest_fieldAccessorTable - .ensureFieldAccessorsInitialized( - com.google.cloud.spannerlib.v1.ExecuteStreamingRequest.class, com.google.cloud.spannerlib.v1.ExecuteStreamingRequest.Builder.class); - } - - // Construct using com.google.cloud.spannerlib.v1.ExecuteStreamingRequest.newBuilder() - private Builder() { - maybeForceBuilderInitialization(); - } - - private Builder( - com.google.protobuf.GeneratedMessage.BuilderParent parent) { - super(parent); - maybeForceBuilderInitialization(); - } - private void maybeForceBuilderInitialization() { - if (com.google.protobuf.GeneratedMessage - .alwaysUseFieldBuilders) { - internalGetConnectionFieldBuilder(); - internalGetExecuteSqlRequestFieldBuilder(); - } - } - @java.lang.Override - public Builder clear() { - super.clear(); - bitField0_ = 0; - connection_ = null; - if (connectionBuilder_ != null) { - connectionBuilder_.dispose(); - connectionBuilder_ = null; - } - executeSqlRequest_ = null; - if (executeSqlRequestBuilder_ != null) { - executeSqlRequestBuilder_.dispose(); - executeSqlRequestBuilder_ = null; - } - return this; - } - - @java.lang.Override - public com.google.protobuf.Descriptors.Descriptor - getDescriptorForType() { - return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_ExecuteStreamingRequest_descriptor; - } - - @java.lang.Override - public com.google.cloud.spannerlib.v1.ExecuteStreamingRequest getDefaultInstanceForType() { - return com.google.cloud.spannerlib.v1.ExecuteStreamingRequest.getDefaultInstance(); - } - - @java.lang.Override - public com.google.cloud.spannerlib.v1.ExecuteStreamingRequest build() { - com.google.cloud.spannerlib.v1.ExecuteStreamingRequest result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - @java.lang.Override - public com.google.cloud.spannerlib.v1.ExecuteStreamingRequest buildPartial() { - com.google.cloud.spannerlib.v1.ExecuteStreamingRequest result = new com.google.cloud.spannerlib.v1.ExecuteStreamingRequest(this); - if (bitField0_ != 0) { buildPartial0(result); } - onBuilt(); - return result; - } - - private void buildPartial0(com.google.cloud.spannerlib.v1.ExecuteStreamingRequest result) { - int from_bitField0_ = bitField0_; - int to_bitField0_ = 0; - if (((from_bitField0_ & 0x00000001) != 0)) { - result.connection_ = connectionBuilder_ == null - ? connection_ - : connectionBuilder_.build(); - to_bitField0_ |= 0x00000001; - } - if (((from_bitField0_ & 0x00000002) != 0)) { - result.executeSqlRequest_ = executeSqlRequestBuilder_ == null - ? executeSqlRequest_ - : executeSqlRequestBuilder_.build(); - to_bitField0_ |= 0x00000002; - } - result.bitField0_ |= to_bitField0_; - } - - @java.lang.Override - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof com.google.cloud.spannerlib.v1.ExecuteStreamingRequest) { - return mergeFrom((com.google.cloud.spannerlib.v1.ExecuteStreamingRequest)other); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom(com.google.cloud.spannerlib.v1.ExecuteStreamingRequest other) { - if (other == com.google.cloud.spannerlib.v1.ExecuteStreamingRequest.getDefaultInstance()) return this; - if (other.hasConnection()) { - mergeConnection(other.getConnection()); - } - if (other.hasExecuteSqlRequest()) { - mergeExecuteSqlRequest(other.getExecuteSqlRequest()); - } - this.mergeUnknownFields(other.getUnknownFields()); - onChanged(); - return this; - } - - @java.lang.Override - public final boolean isInitialized() { - return true; - } - - @java.lang.Override - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - if (extensionRegistry == null) { - throw new java.lang.NullPointerException(); - } - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - case 10: { - input.readMessage( - internalGetConnectionFieldBuilder().getBuilder(), - extensionRegistry); - bitField0_ |= 0x00000001; - break; - } // case 10 - case 18: { - input.readMessage( - internalGetExecuteSqlRequestFieldBuilder().getBuilder(), - extensionRegistry); - bitField0_ |= 0x00000002; - break; - } // case 18 - default: { - if (!super.parseUnknownField(input, extensionRegistry, tag)) { - done = true; // was an endgroup tag - } - break; - } // default: - } // switch (tag) - } // while (!done) - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.unwrapIOException(); - } finally { - onChanged(); - } // finally - return this; - } - private int bitField0_; - - private com.google.cloud.spannerlib.v1.Connection connection_; - private com.google.protobuf.SingleFieldBuilder< - com.google.cloud.spannerlib.v1.Connection, com.google.cloud.spannerlib.v1.Connection.Builder, com.google.cloud.spannerlib.v1.ConnectionOrBuilder> connectionBuilder_; - /** - * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; - * @return Whether the connection field is set. - */ - public boolean hasConnection() { - return ((bitField0_ & 0x00000001) != 0); - } - /** - * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; - * @return The connection. - */ - public com.google.cloud.spannerlib.v1.Connection getConnection() { - if (connectionBuilder_ == null) { - return connection_ == null ? com.google.cloud.spannerlib.v1.Connection.getDefaultInstance() : connection_; - } else { - return connectionBuilder_.getMessage(); - } - } - /** - * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; - */ - public Builder setConnection(com.google.cloud.spannerlib.v1.Connection value) { - if (connectionBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - connection_ = value; - } else { - connectionBuilder_.setMessage(value); - } - bitField0_ |= 0x00000001; - onChanged(); - return this; - } - /** - * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; - */ - public Builder setConnection( - com.google.cloud.spannerlib.v1.Connection.Builder builderForValue) { - if (connectionBuilder_ == null) { - connection_ = builderForValue.build(); - } else { - connectionBuilder_.setMessage(builderForValue.build()); - } - bitField0_ |= 0x00000001; - onChanged(); - return this; - } - /** - * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; - */ - public Builder mergeConnection(com.google.cloud.spannerlib.v1.Connection value) { - if (connectionBuilder_ == null) { - if (((bitField0_ & 0x00000001) != 0) && - connection_ != null && - connection_ != com.google.cloud.spannerlib.v1.Connection.getDefaultInstance()) { - getConnectionBuilder().mergeFrom(value); - } else { - connection_ = value; - } - } else { - connectionBuilder_.mergeFrom(value); - } - if (connection_ != null) { - bitField0_ |= 0x00000001; - onChanged(); - } - return this; - } - /** - * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; - */ - public Builder clearConnection() { - bitField0_ = (bitField0_ & ~0x00000001); - connection_ = null; - if (connectionBuilder_ != null) { - connectionBuilder_.dispose(); - connectionBuilder_ = null; - } - onChanged(); - return this; - } - /** - * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; - */ - public com.google.cloud.spannerlib.v1.Connection.Builder getConnectionBuilder() { - bitField0_ |= 0x00000001; - onChanged(); - return internalGetConnectionFieldBuilder().getBuilder(); - } - /** - * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; - */ - public com.google.cloud.spannerlib.v1.ConnectionOrBuilder getConnectionOrBuilder() { - if (connectionBuilder_ != null) { - return connectionBuilder_.getMessageOrBuilder(); - } else { - return connection_ == null ? - com.google.cloud.spannerlib.v1.Connection.getDefaultInstance() : connection_; - } - } - /** - * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; - */ - private com.google.protobuf.SingleFieldBuilder< - com.google.cloud.spannerlib.v1.Connection, com.google.cloud.spannerlib.v1.Connection.Builder, com.google.cloud.spannerlib.v1.ConnectionOrBuilder> - internalGetConnectionFieldBuilder() { - if (connectionBuilder_ == null) { - connectionBuilder_ = new com.google.protobuf.SingleFieldBuilder< - com.google.cloud.spannerlib.v1.Connection, com.google.cloud.spannerlib.v1.Connection.Builder, com.google.cloud.spannerlib.v1.ConnectionOrBuilder>( - getConnection(), - getParentForChildren(), - isClean()); - connection_ = null; - } - return connectionBuilder_; - } - - private com.google.spanner.v1.ExecuteSqlRequest executeSqlRequest_; - private com.google.protobuf.SingleFieldBuilder< - com.google.spanner.v1.ExecuteSqlRequest, com.google.spanner.v1.ExecuteSqlRequest.Builder, com.google.spanner.v1.ExecuteSqlRequestOrBuilder> executeSqlRequestBuilder_; - /** - * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; - * @return Whether the executeSqlRequest field is set. - */ - public boolean hasExecuteSqlRequest() { - return ((bitField0_ & 0x00000002) != 0); - } - /** - * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; - * @return The executeSqlRequest. - */ - public com.google.spanner.v1.ExecuteSqlRequest getExecuteSqlRequest() { - if (executeSqlRequestBuilder_ == null) { - return executeSqlRequest_ == null ? com.google.spanner.v1.ExecuteSqlRequest.getDefaultInstance() : executeSqlRequest_; - } else { - return executeSqlRequestBuilder_.getMessage(); - } - } - /** - * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; - */ - public Builder setExecuteSqlRequest(com.google.spanner.v1.ExecuteSqlRequest value) { - if (executeSqlRequestBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - executeSqlRequest_ = value; - } else { - executeSqlRequestBuilder_.setMessage(value); - } - bitField0_ |= 0x00000002; - onChanged(); - return this; - } - /** - * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; - */ - public Builder setExecuteSqlRequest( - com.google.spanner.v1.ExecuteSqlRequest.Builder builderForValue) { - if (executeSqlRequestBuilder_ == null) { - executeSqlRequest_ = builderForValue.build(); - } else { - executeSqlRequestBuilder_.setMessage(builderForValue.build()); - } - bitField0_ |= 0x00000002; - onChanged(); - return this; - } - /** - * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; - */ - public Builder mergeExecuteSqlRequest(com.google.spanner.v1.ExecuteSqlRequest value) { - if (executeSqlRequestBuilder_ == null) { - if (((bitField0_ & 0x00000002) != 0) && - executeSqlRequest_ != null && - executeSqlRequest_ != com.google.spanner.v1.ExecuteSqlRequest.getDefaultInstance()) { - getExecuteSqlRequestBuilder().mergeFrom(value); - } else { - executeSqlRequest_ = value; - } - } else { - executeSqlRequestBuilder_.mergeFrom(value); - } - if (executeSqlRequest_ != null) { - bitField0_ |= 0x00000002; - onChanged(); - } - return this; - } - /** - * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; - */ - public Builder clearExecuteSqlRequest() { - bitField0_ = (bitField0_ & ~0x00000002); - executeSqlRequest_ = null; - if (executeSqlRequestBuilder_ != null) { - executeSqlRequestBuilder_.dispose(); - executeSqlRequestBuilder_ = null; - } - onChanged(); - return this; - } - /** - * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; - */ - public com.google.spanner.v1.ExecuteSqlRequest.Builder getExecuteSqlRequestBuilder() { - bitField0_ |= 0x00000002; - onChanged(); - return internalGetExecuteSqlRequestFieldBuilder().getBuilder(); - } - /** - * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; - */ - public com.google.spanner.v1.ExecuteSqlRequestOrBuilder getExecuteSqlRequestOrBuilder() { - if (executeSqlRequestBuilder_ != null) { - return executeSqlRequestBuilder_.getMessageOrBuilder(); - } else { - return executeSqlRequest_ == null ? - com.google.spanner.v1.ExecuteSqlRequest.getDefaultInstance() : executeSqlRequest_; - } - } - /** - * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; - */ - private com.google.protobuf.SingleFieldBuilder< - com.google.spanner.v1.ExecuteSqlRequest, com.google.spanner.v1.ExecuteSqlRequest.Builder, com.google.spanner.v1.ExecuteSqlRequestOrBuilder> - internalGetExecuteSqlRequestFieldBuilder() { - if (executeSqlRequestBuilder_ == null) { - executeSqlRequestBuilder_ = new com.google.protobuf.SingleFieldBuilder< - com.google.spanner.v1.ExecuteSqlRequest, com.google.spanner.v1.ExecuteSqlRequest.Builder, com.google.spanner.v1.ExecuteSqlRequestOrBuilder>( - getExecuteSqlRequest(), - getParentForChildren(), - isClean()); - executeSqlRequest_ = null; - } - return executeSqlRequestBuilder_; - } - - // @@protoc_insertion_point(builder_scope:google.spannerlib.v1.ExecuteStreamingRequest) - } - - // @@protoc_insertion_point(class_scope:google.spannerlib.v1.ExecuteStreamingRequest) - private static final com.google.cloud.spannerlib.v1.ExecuteStreamingRequest DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new com.google.cloud.spannerlib.v1.ExecuteStreamingRequest(); - } - - public static com.google.cloud.spannerlib.v1.ExecuteStreamingRequest getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser - PARSER = new com.google.protobuf.AbstractParser() { - @java.lang.Override - public ExecuteStreamingRequest parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - Builder builder = newBuilder(); - try { - builder.mergeFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(builder.buildPartial()); - } catch (com.google.protobuf.UninitializedMessageException e) { - throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException(e) - .setUnfinishedMessage(builder.buildPartial()); - } - return builder.buildPartial(); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @java.lang.Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - @java.lang.Override - public com.google.cloud.spannerlib.v1.ExecuteStreamingRequest getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - -} - diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteStreamingRequestOrBuilder.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteStreamingRequestOrBuilder.java deleted file mode 100644 index 50e68339..00000000 --- a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/ExecuteStreamingRequestOrBuilder.java +++ /dev/null @@ -1,42 +0,0 @@ -// Generated by the protocol buffer compiler. DO NOT EDIT! -// NO CHECKED-IN PROTOBUF GENCODE -// source: google/spannerlib/v1/spannerlib.proto -// Protobuf Java Version: 4.32.1 - -package com.google.cloud.spannerlib.v1; - -@com.google.protobuf.Generated -public interface ExecuteStreamingRequestOrBuilder extends - // @@protoc_insertion_point(interface_extends:google.spannerlib.v1.ExecuteStreamingRequest) - com.google.protobuf.MessageOrBuilder { - - /** - * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; - * @return Whether the connection field is set. - */ - boolean hasConnection(); - /** - * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; - * @return The connection. - */ - com.google.cloud.spannerlib.v1.Connection getConnection(); - /** - * .google.spannerlib.v1.Connection connection = 1 [(.google.api.field_behavior) = REQUIRED]; - */ - com.google.cloud.spannerlib.v1.ConnectionOrBuilder getConnectionOrBuilder(); - - /** - * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; - * @return Whether the executeSqlRequest field is set. - */ - boolean hasExecuteSqlRequest(); - /** - * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; - * @return The executeSqlRequest. - */ - com.google.spanner.v1.ExecuteSqlRequest getExecuteSqlRequest(); - /** - * .google.spanner.v1.ExecuteSqlRequest execute_sql_request = 2 [(.google.api.field_behavior) = REQUIRED]; - */ - com.google.spanner.v1.ExecuteSqlRequestOrBuilder getExecuteSqlRequestOrBuilder(); -} diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextResultSetResponse.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextResultSetResponse.java deleted file mode 100644 index 7d8d65bc..00000000 --- a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextResultSetResponse.java +++ /dev/null @@ -1,626 +0,0 @@ -// Generated by the protocol buffer compiler. DO NOT EDIT! -// NO CHECKED-IN PROTOBUF GENCODE -// source: google/spannerlib/v1/spannerlib.proto -// Protobuf Java Version: 4.32.1 - -package com.google.cloud.spannerlib.v1; - -/** - * Protobuf type {@code google.spannerlib.v1.NextResultSetResponse} - */ -@com.google.protobuf.Generated -public final class NextResultSetResponse extends - com.google.protobuf.GeneratedMessage implements - // @@protoc_insertion_point(message_implements:google.spannerlib.v1.NextResultSetResponse) - NextResultSetResponseOrBuilder { -private static final long serialVersionUID = 0L; - static { - com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( - com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, - /* major= */ 4, - /* minor= */ 32, - /* patch= */ 1, - /* suffix= */ "", - NextResultSetResponse.class.getName()); - } - // Use NextResultSetResponse.newBuilder() to construct. - private NextResultSetResponse(com.google.protobuf.GeneratedMessage.Builder builder) { - super(builder); - } - private NextResultSetResponse() { - } - - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_NextResultSetResponse_descriptor; - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable - internalGetFieldAccessorTable() { - return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_NextResultSetResponse_fieldAccessorTable - .ensureFieldAccessorsInitialized( - com.google.cloud.spannerlib.v1.NextResultSetResponse.class, com.google.cloud.spannerlib.v1.NextResultSetResponse.Builder.class); - } - - private int bitField0_; - public static final int HAS_MORE_RESULTS_FIELD_NUMBER = 1; - private boolean hasMoreResults_ = false; - /** - * bool has_more_results = 1; - * @return The hasMoreResults. - */ - @java.lang.Override - public boolean getHasMoreResults() { - return hasMoreResults_; - } - - public static final int METADATA_FIELD_NUMBER = 2; - private com.google.spanner.v1.ResultSetMetadata metadata_; - /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; - * @return Whether the metadata field is set. - */ - @java.lang.Override - public boolean hasMetadata() { - return ((bitField0_ & 0x00000001) != 0); - } - /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; - * @return The metadata. - */ - @java.lang.Override - public com.google.spanner.v1.ResultSetMetadata getMetadata() { - return metadata_ == null ? com.google.spanner.v1.ResultSetMetadata.getDefaultInstance() : metadata_; - } - /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; - */ - @java.lang.Override - public com.google.spanner.v1.ResultSetMetadataOrBuilder getMetadataOrBuilder() { - return metadata_ == null ? com.google.spanner.v1.ResultSetMetadata.getDefaultInstance() : metadata_; - } - - private byte memoizedIsInitialized = -1; - @java.lang.Override - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) return true; - if (isInitialized == 0) return false; - - memoizedIsInitialized = 1; - return true; - } - - @java.lang.Override - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (hasMoreResults_ != false) { - output.writeBool(1, hasMoreResults_); - } - if (((bitField0_ & 0x00000001) != 0)) { - output.writeMessage(2, getMetadata()); - } - getUnknownFields().writeTo(output); - } - - @java.lang.Override - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) return size; - - size = 0; - if (hasMoreResults_ != false) { - size += com.google.protobuf.CodedOutputStream - .computeBoolSize(1, hasMoreResults_); - } - if (((bitField0_ & 0x00000001) != 0)) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(2, getMetadata()); - } - size += getUnknownFields().getSerializedSize(); - memoizedSize = size; - return size; - } - - @java.lang.Override - public boolean equals(final java.lang.Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof com.google.cloud.spannerlib.v1.NextResultSetResponse)) { - return super.equals(obj); - } - com.google.cloud.spannerlib.v1.NextResultSetResponse other = (com.google.cloud.spannerlib.v1.NextResultSetResponse) obj; - - if (getHasMoreResults() - != other.getHasMoreResults()) return false; - if (hasMetadata() != other.hasMetadata()) return false; - if (hasMetadata()) { - if (!getMetadata() - .equals(other.getMetadata())) return false; - } - if (!getUnknownFields().equals(other.getUnknownFields())) return false; - return true; - } - - @java.lang.Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptor().hashCode(); - hash = (37 * hash) + HAS_MORE_RESULTS_FIELD_NUMBER; - hash = (53 * hash) + com.google.protobuf.Internal.hashBoolean( - getHasMoreResults()); - if (hasMetadata()) { - hash = (37 * hash) + METADATA_FIELD_NUMBER; - hash = (53 * hash) + getMetadata().hashCode(); - } - hash = (29 * hash) + getUnknownFields().hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseFrom( - java.nio.ByteBuffer data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseFrom( - java.nio.ByteBuffer data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseFrom( - com.google.protobuf.ByteString data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseFrom(byte[] data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseDelimitedFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input); - } - - public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseFrom( - com.google.protobuf.CodedInputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - public static com.google.cloud.spannerlib.v1.NextResultSetResponse parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - @java.lang.Override - public Builder newBuilderForType() { return newBuilder(); } - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - public static Builder newBuilder(com.google.cloud.spannerlib.v1.NextResultSetResponse prototype) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - @java.lang.Override - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() : new Builder().mergeFrom(this); - } - - @java.lang.Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessage.BuilderParent parent) { - Builder builder = new Builder(parent); - return builder; - } - /** - * Protobuf type {@code google.spannerlib.v1.NextResultSetResponse} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessage.Builder implements - // @@protoc_insertion_point(builder_implements:google.spannerlib.v1.NextResultSetResponse) - com.google.cloud.spannerlib.v1.NextResultSetResponseOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_NextResultSetResponse_descriptor; - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable - internalGetFieldAccessorTable() { - return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_NextResultSetResponse_fieldAccessorTable - .ensureFieldAccessorsInitialized( - com.google.cloud.spannerlib.v1.NextResultSetResponse.class, com.google.cloud.spannerlib.v1.NextResultSetResponse.Builder.class); - } - - // Construct using com.google.cloud.spannerlib.v1.NextResultSetResponse.newBuilder() - private Builder() { - maybeForceBuilderInitialization(); - } - - private Builder( - com.google.protobuf.GeneratedMessage.BuilderParent parent) { - super(parent); - maybeForceBuilderInitialization(); - } - private void maybeForceBuilderInitialization() { - if (com.google.protobuf.GeneratedMessage - .alwaysUseFieldBuilders) { - internalGetMetadataFieldBuilder(); - } - } - @java.lang.Override - public Builder clear() { - super.clear(); - bitField0_ = 0; - hasMoreResults_ = false; - metadata_ = null; - if (metadataBuilder_ != null) { - metadataBuilder_.dispose(); - metadataBuilder_ = null; - } - return this; - } - - @java.lang.Override - public com.google.protobuf.Descriptors.Descriptor - getDescriptorForType() { - return com.google.cloud.spannerlib.v1.SpannerLibProto.internal_static_google_spannerlib_v1_NextResultSetResponse_descriptor; - } - - @java.lang.Override - public com.google.cloud.spannerlib.v1.NextResultSetResponse getDefaultInstanceForType() { - return com.google.cloud.spannerlib.v1.NextResultSetResponse.getDefaultInstance(); - } - - @java.lang.Override - public com.google.cloud.spannerlib.v1.NextResultSetResponse build() { - com.google.cloud.spannerlib.v1.NextResultSetResponse result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - @java.lang.Override - public com.google.cloud.spannerlib.v1.NextResultSetResponse buildPartial() { - com.google.cloud.spannerlib.v1.NextResultSetResponse result = new com.google.cloud.spannerlib.v1.NextResultSetResponse(this); - if (bitField0_ != 0) { buildPartial0(result); } - onBuilt(); - return result; - } - - private void buildPartial0(com.google.cloud.spannerlib.v1.NextResultSetResponse result) { - int from_bitField0_ = bitField0_; - if (((from_bitField0_ & 0x00000001) != 0)) { - result.hasMoreResults_ = hasMoreResults_; - } - int to_bitField0_ = 0; - if (((from_bitField0_ & 0x00000002) != 0)) { - result.metadata_ = metadataBuilder_ == null - ? metadata_ - : metadataBuilder_.build(); - to_bitField0_ |= 0x00000001; - } - result.bitField0_ |= to_bitField0_; - } - - @java.lang.Override - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof com.google.cloud.spannerlib.v1.NextResultSetResponse) { - return mergeFrom((com.google.cloud.spannerlib.v1.NextResultSetResponse)other); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom(com.google.cloud.spannerlib.v1.NextResultSetResponse other) { - if (other == com.google.cloud.spannerlib.v1.NextResultSetResponse.getDefaultInstance()) return this; - if (other.getHasMoreResults() != false) { - setHasMoreResults(other.getHasMoreResults()); - } - if (other.hasMetadata()) { - mergeMetadata(other.getMetadata()); - } - this.mergeUnknownFields(other.getUnknownFields()); - onChanged(); - return this; - } - - @java.lang.Override - public final boolean isInitialized() { - return true; - } - - @java.lang.Override - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - if (extensionRegistry == null) { - throw new java.lang.NullPointerException(); - } - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - case 8: { - hasMoreResults_ = input.readBool(); - bitField0_ |= 0x00000001; - break; - } // case 8 - case 18: { - input.readMessage( - internalGetMetadataFieldBuilder().getBuilder(), - extensionRegistry); - bitField0_ |= 0x00000002; - break; - } // case 18 - default: { - if (!super.parseUnknownField(input, extensionRegistry, tag)) { - done = true; // was an endgroup tag - } - break; - } // default: - } // switch (tag) - } // while (!done) - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.unwrapIOException(); - } finally { - onChanged(); - } // finally - return this; - } - private int bitField0_; - - private boolean hasMoreResults_ ; - /** - * bool has_more_results = 1; - * @return The hasMoreResults. - */ - @java.lang.Override - public boolean getHasMoreResults() { - return hasMoreResults_; - } - /** - * bool has_more_results = 1; - * @param value The hasMoreResults to set. - * @return This builder for chaining. - */ - public Builder setHasMoreResults(boolean value) { - - hasMoreResults_ = value; - bitField0_ |= 0x00000001; - onChanged(); - return this; - } - /** - * bool has_more_results = 1; - * @return This builder for chaining. - */ - public Builder clearHasMoreResults() { - bitField0_ = (bitField0_ & ~0x00000001); - hasMoreResults_ = false; - onChanged(); - return this; - } - - private com.google.spanner.v1.ResultSetMetadata metadata_; - private com.google.protobuf.SingleFieldBuilder< - com.google.spanner.v1.ResultSetMetadata, com.google.spanner.v1.ResultSetMetadata.Builder, com.google.spanner.v1.ResultSetMetadataOrBuilder> metadataBuilder_; - /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; - * @return Whether the metadata field is set. - */ - public boolean hasMetadata() { - return ((bitField0_ & 0x00000002) != 0); - } - /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; - * @return The metadata. - */ - public com.google.spanner.v1.ResultSetMetadata getMetadata() { - if (metadataBuilder_ == null) { - return metadata_ == null ? com.google.spanner.v1.ResultSetMetadata.getDefaultInstance() : metadata_; - } else { - return metadataBuilder_.getMessage(); - } - } - /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; - */ - public Builder setMetadata(com.google.spanner.v1.ResultSetMetadata value) { - if (metadataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - metadata_ = value; - } else { - metadataBuilder_.setMessage(value); - } - bitField0_ |= 0x00000002; - onChanged(); - return this; - } - /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; - */ - public Builder setMetadata( - com.google.spanner.v1.ResultSetMetadata.Builder builderForValue) { - if (metadataBuilder_ == null) { - metadata_ = builderForValue.build(); - } else { - metadataBuilder_.setMessage(builderForValue.build()); - } - bitField0_ |= 0x00000002; - onChanged(); - return this; - } - /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; - */ - public Builder mergeMetadata(com.google.spanner.v1.ResultSetMetadata value) { - if (metadataBuilder_ == null) { - if (((bitField0_ & 0x00000002) != 0) && - metadata_ != null && - metadata_ != com.google.spanner.v1.ResultSetMetadata.getDefaultInstance()) { - getMetadataBuilder().mergeFrom(value); - } else { - metadata_ = value; - } - } else { - metadataBuilder_.mergeFrom(value); - } - if (metadata_ != null) { - bitField0_ |= 0x00000002; - onChanged(); - } - return this; - } - /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; - */ - public Builder clearMetadata() { - bitField0_ = (bitField0_ & ~0x00000002); - metadata_ = null; - if (metadataBuilder_ != null) { - metadataBuilder_.dispose(); - metadataBuilder_ = null; - } - onChanged(); - return this; - } - /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; - */ - public com.google.spanner.v1.ResultSetMetadata.Builder getMetadataBuilder() { - bitField0_ |= 0x00000002; - onChanged(); - return internalGetMetadataFieldBuilder().getBuilder(); - } - /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; - */ - public com.google.spanner.v1.ResultSetMetadataOrBuilder getMetadataOrBuilder() { - if (metadataBuilder_ != null) { - return metadataBuilder_.getMessageOrBuilder(); - } else { - return metadata_ == null ? - com.google.spanner.v1.ResultSetMetadata.getDefaultInstance() : metadata_; - } - } - /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; - */ - private com.google.protobuf.SingleFieldBuilder< - com.google.spanner.v1.ResultSetMetadata, com.google.spanner.v1.ResultSetMetadata.Builder, com.google.spanner.v1.ResultSetMetadataOrBuilder> - internalGetMetadataFieldBuilder() { - if (metadataBuilder_ == null) { - metadataBuilder_ = new com.google.protobuf.SingleFieldBuilder< - com.google.spanner.v1.ResultSetMetadata, com.google.spanner.v1.ResultSetMetadata.Builder, com.google.spanner.v1.ResultSetMetadataOrBuilder>( - getMetadata(), - getParentForChildren(), - isClean()); - metadata_ = null; - } - return metadataBuilder_; - } - - // @@protoc_insertion_point(builder_scope:google.spannerlib.v1.NextResultSetResponse) - } - - // @@protoc_insertion_point(class_scope:google.spannerlib.v1.NextResultSetResponse) - private static final com.google.cloud.spannerlib.v1.NextResultSetResponse DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new com.google.cloud.spannerlib.v1.NextResultSetResponse(); - } - - public static com.google.cloud.spannerlib.v1.NextResultSetResponse getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser - PARSER = new com.google.protobuf.AbstractParser() { - @java.lang.Override - public NextResultSetResponse parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - Builder builder = newBuilder(); - try { - builder.mergeFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(builder.buildPartial()); - } catch (com.google.protobuf.UninitializedMessageException e) { - throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException(e) - .setUnfinishedMessage(builder.buildPartial()); - } - return builder.buildPartial(); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @java.lang.Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - @java.lang.Override - public com.google.cloud.spannerlib.v1.NextResultSetResponse getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - -} - diff --git a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextResultSetResponseOrBuilder.java b/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextResultSetResponseOrBuilder.java deleted file mode 100644 index e9bdc409..00000000 --- a/spannerlib/wrappers/spannerlib-java/src/main/java/com/google/cloud/spannerlib/v1/NextResultSetResponseOrBuilder.java +++ /dev/null @@ -1,33 +0,0 @@ -// Generated by the protocol buffer compiler. DO NOT EDIT! -// NO CHECKED-IN PROTOBUF GENCODE -// source: google/spannerlib/v1/spannerlib.proto -// Protobuf Java Version: 4.32.1 - -package com.google.cloud.spannerlib.v1; - -@com.google.protobuf.Generated -public interface NextResultSetResponseOrBuilder extends - // @@protoc_insertion_point(interface_extends:google.spannerlib.v1.NextResultSetResponse) - com.google.protobuf.MessageOrBuilder { - - /** - * bool has_more_results = 1; - * @return The hasMoreResults. - */ - boolean getHasMoreResults(); - - /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; - * @return Whether the metadata field is set. - */ - boolean hasMetadata(); - /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; - * @return The metadata. - */ - com.google.spanner.v1.ResultSetMetadata getMetadata(); - /** - * .google.spanner.v1.ResultSetMetadata metadata = 2; - */ - com.google.spanner.v1.ResultSetMetadataOrBuilder getMetadataOrBuilder(); -} From 3879a02a53f5887fa7c8d3f2365a075eeab96a4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Mon, 15 Dec 2025 18:40:08 +0100 Subject: [PATCH 24/29] test: add sample tests --- .../AbstractMockServerTests.cs | 94 ++++++++++ .../SamplesMockServerTests.cs | 160 ++++++++++++++++++ .../spanner-ado-net-samples-tests.csproj | 36 ++++ .../Snippets/CommitTimestampSample.cs | 2 - drivers/spanner-ado-net/spanner-ado-net.sln | 6 + .../spanner-ado-net/AssemblyInfo.cs | 1 + 6 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples-tests/AbstractMockServerTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples-tests/SamplesMockServerTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples-tests/spanner-ado-net-samples-tests.csproj diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples-tests/AbstractMockServerTests.cs b/drivers/spanner-ado-net/spanner-ado-net-samples-tests/AbstractMockServerTests.cs new file mode 100644 index 00000000..b0edfc6b --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples-tests/AbstractMockServerTests.cs @@ -0,0 +1,94 @@ +// Copyright 2025 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 +// +// https://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. + +using Google.Cloud.SpannerLib.MockServer; + +namespace Google.Cloud.Spanner.DataProvider.Samples.Tests; + +public abstract class AbstractMockServerTests +{ + static AbstractMockServerTests() + { + AppDomain.CurrentDomain.ProcessExit += (_, _) => + { + SpannerPool.CloseSpannerLib(); + }; + } + + protected SpannerMockServerFixture Fixture; + + protected SpannerDataSource DataSource { get; private set; } + + + protected string ConnectionString => $"Host={Fixture.Host};Port={Fixture.Port};Data Source=projects/p1/instances/i1/databases/d1;UsePlainText=true"; + + [OneTimeSetUp] + public void Setup() + { + Fixture = new SpannerMockServerFixture(); + DataSource = SpannerDataSource.Create(ConnectionString); + } + + [OneTimeTearDown] + public void Teardown() + { + DataSource.Dispose(); + Fixture.Dispose(); + } + + [SetUp] + public void SetupResults() + { + Fixture.SpannerMock.AddOrUpdateStatementResult("SELECT 1", StatementResult.CreateSelect1ResultSet()); + } + + [TearDown] + public void Reset() + { + Fixture.SpannerMock.Reset(); + Fixture.DatabaseAdminMock.Reset(); + } + + protected SpannerConnection OpenConnection() + { + var connection = new SpannerConnection(ConnectionString); + connection.Open(); + return connection; + } + + protected async Task OpenConnectionAsync() + { + var connection = new SpannerConnection(ConnectionString); + await connection.OpenAsync(); + return connection; + } + + protected SpannerDataSource CreateDataSource() + { + return CreateDataSource(_ => { }); + } + + protected SpannerDataSource CreateDataSource(string connectionString) + { + return CreateDataSource(csb => { csb.ConnectionString = connectionString; }); + } + + protected SpannerDataSource CreateDataSource(Action connectionStringBuilderAction) + { + var connectionStringBuilder = new SpannerConnectionStringBuilder(ConnectionString); + connectionStringBuilderAction(connectionStringBuilder); + return SpannerDataSource.Create(connectionStringBuilder); + } + +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples-tests/SamplesMockServerTests.cs b/drivers/spanner-ado-net/spanner-ado-net-samples-tests/SamplesMockServerTests.cs new file mode 100644 index 00000000..49162fb0 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples-tests/SamplesMockServerTests.cs @@ -0,0 +1,160 @@ +// Copyright 2025 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 +// +// https://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. + +using Google.Cloud.Spanner.DataProvider.Samples.Snippets; +using Google.Cloud.Spanner.V1; +using Google.Cloud.SpannerLib.MockServer; +using TypeCode = Google.Cloud.Spanner.V1.TypeCode; + +namespace Google.Cloud.Spanner.DataProvider.Samples.Tests; + +public class SamplesMockServerTests : AbstractMockServerTests +{ + private StringWriter? _writer; + private TextWriter _output; + + private StringWriter Writer => _writer ??= new StringWriter(); + + [SetUp] + public void SetupOutput() + { + _output = Console.Out; + _writer = new StringWriter(); + Console.SetOut(_writer); + } + + [TearDown] + public void CleanupOutput() + { + _writer?.Dispose(); + Console.SetOut(_output); + } + + [Test] + public async Task TestHelloWorld() + { + const string sql = "SELECT 'Hello World' as Message"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateResultSet([Tuple.Create(TypeCode.String, "Message")], [["Hello World"]])); + await HelloWorldSample.Run(ConnectionString); + + Assert.That(Writer.ToString(), Is.EqualTo($"Greeting from Spanner: Hello World{Environment.NewLine}")); + var requests = Fixture.SpannerMock.Requests.OfType().ToList(); + Assert.That(requests, Has.Count.EqualTo(1)); + var request = requests[0]; + Assert.That(request.Transaction, Is.EqualTo(new TransactionSelector + { + SingleUse = new TransactionOptions + { + ReadOnly = new TransactionOptions.Types.ReadOnly + { + Strong = true, + ReturnReadTimestamp = true, + } + } + })); + } + + [Test] + public async Task TestCommitTimestamp() + { + const string sql = "INSERT INTO Singers (SingerId, FirstName, LastName) VALUES (@id, @first, @last)"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(1L)); + + await CommitTimestampSample.Run(ConnectionString); + + Assert.That(Writer.ToString(), Does.StartWith($"Inserted 1 singer(s){Environment.NewLine}Transaction committed at ")); + var requests = Fixture.SpannerMock.Requests.OfType().ToList(); + Assert.That(requests, Has.Count.EqualTo(1)); + var request = requests[0]; + Assert.That(request.Transaction, Is.EqualTo(new TransactionSelector + { + Begin = new TransactionOptions + { + ReadWrite = new TransactionOptions.Types.ReadWrite(), + } + })); + Assert.That(request.Params.Fields, Has.Count.EqualTo(3)); + Assert.That(request.ParamTypes, Has.Count.EqualTo(0)); + Assert.That(Fixture.SpannerMock.Requests.OfType().ToList(), Has.Count.EqualTo(1)); + } + + [Test] + public async Task TestCustomConfigurationSample() + { + const string sql = "SELECT @greeting as Message"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateResultSet([Tuple.Create(TypeCode.String, "Message")], [["Hello from Spanner"]])); + + await CustomConfigurationSample.Run(ConnectionString); + + Assert.That(Writer.ToString(), Is.EqualTo($"Greeting: Hello from Spanner{Environment.NewLine}")); + } + + [Test] + public async Task TestDataTypesSample() + { + const string insert = "insert or update into AllTypes " + + "(id, col_bool, col_bytes, col_date, col_float32, col_float64, col_int64, /*col_interval,*/ col_json, col_numeric, col_string, col_timestamp) " + + "values (@id, @bool, @bytes, @date, @float32, @float64, @int64, /*@interval,*/ @json, @numeric, @string, @timestamp)"; + Fixture.SpannerMock.AddOrUpdateStatementResult(insert, StatementResult.CreateUpdateCount(1L)); + const string query = "select * from AllTypes order by id"; + Fixture.SpannerMock.AddOrUpdateStatementResult(query, StatementResult.CreateResultSet( + [ + Tuple.Create(TypeCode.Int64, "id"), + Tuple.Create(TypeCode.Bool, "col_bool"), + Tuple.Create(TypeCode.Bytes, "col_bytes"), + Tuple.Create(TypeCode.Date, "col_date"), + Tuple.Create(TypeCode.Float32, "col_float32"), + Tuple.Create(TypeCode.Float64, "col_float64"), + Tuple.Create(TypeCode.Int64, "col_int64"), + Tuple.Create(TypeCode.Json, "col_json"), + Tuple.Create(TypeCode.Numeric, "col_numeric"), + Tuple.Create(TypeCode.String, "col_string"), + Tuple.Create(TypeCode.Timestamp, "col_timestamp"), + ], + [[ + 1L, + true, + Convert.ToBase64String(new byte[]{1,2,3}), + DateOnly.FromDateTime(DateTime.Now), + 3.14f, + 3.14d, + 100L, + "{\"key\":\"value\"}", + 3.14m, + "test-string", + DateTime.Now, + ]])); + + await DataTypesSample.Run(ConnectionString); + + Assert.That(Writer.ToString(), Does.StartWith($"Inserted: 1{Environment.NewLine}id: 1{Environment.NewLine}col_bool: True{Environment.NewLine}")); + var request = Fixture.SpannerMock.Requests.OfType().First(); + Assert.That(request.Sql, Is.EqualTo(insert)); + Assert.That(request.Params.Fields, Has.Count.EqualTo(11)); + Assert.That(request.ParamTypes, Has.Count.EqualTo(0)); + Assert.That(request.Params.Fields["id"].StringValue, Is.EqualTo("1")); + Assert.That(request.Params.Fields["bool"].BoolValue, Is.True); + Assert.That(request.Params.Fields["bytes"].StringValue, Is.EqualTo(Convert.ToBase64String(new byte[]{1,2,3}))); + Assert.That(request.Params.Fields["date"].StringValue, Is.Not.Null); + Assert.That(request.Params.Fields["float32"].NumberValue, Is.EqualTo(3.14f)); + Assert.That(request.Params.Fields["float64"].NumberValue, Is.EqualTo(3.14d)); + Assert.That(request.Params.Fields["int64"].StringValue, Is.EqualTo("100")); + Assert.That(request.Params.Fields["json"].StringValue, Is.EqualTo("{\"key\": \"value\"}")); + Assert.That(request.Params.Fields["numeric"].StringValue, Is.EqualTo("3.14")); + Assert.That(request.Params.Fields["string"].StringValue, Is.EqualTo("test-string")); + Assert.That(request.Params.Fields["timestamp"].StringValue, Is.Not.Null); + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples-tests/spanner-ado-net-samples-tests.csproj b/drivers/spanner-ado-net/spanner-ado-net-samples-tests/spanner-ado-net-samples-tests.csproj new file mode 100644 index 00000000..ed386981 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples-tests/spanner-ado-net-samples-tests.csproj @@ -0,0 +1,36 @@ + + + + net8.0 + Google.Cloud.Spanner.DataProvider.Samples.Tests + enable + enable + + false + true + Google.Cloud.Spanner.DataProvider.Samples.Tests + default + LatestMajor + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/CommitTimestampSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/CommitTimestampSample.cs index a0f09399..fcabb63e 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/CommitTimestampSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/CommitTimestampSample.cs @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Data; - namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; // Example for getting the commit timestamp of a read/write transaction. diff --git a/drivers/spanner-ado-net/spanner-ado-net.sln b/drivers/spanner-ado-net/spanner-ado-net.sln index 7a24c1a4..315ab09c 100644 --- a/drivers/spanner-ado-net/spanner-ado-net.sln +++ b/drivers/spanner-ado-net/spanner-ado-net.sln @@ -20,6 +20,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spannerlib-dotnet-native-im EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spannerlib-dotnet-grpc-v1", "..\..\spannerlib\wrappers\spannerlib-dotnet\spannerlib-dotnet-grpc-v1\spannerlib-dotnet-grpc-v1.csproj", "{DF3C6D80-EB58-4189-A15A-9D3FEA233AF0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spanner-ado-net-samples-tests", "spanner-ado-net-samples-tests\spanner-ado-net-samples-tests.csproj", "{17F5C0B7-7932-4536-9328-5BBB0EC02A3B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -66,5 +68,9 @@ Global {DF3C6D80-EB58-4189-A15A-9D3FEA233AF0}.Debug|Any CPU.Build.0 = Debug|Any CPU {DF3C6D80-EB58-4189-A15A-9D3FEA233AF0}.Release|Any CPU.ActiveCfg = Release|Any CPU {DF3C6D80-EB58-4189-A15A-9D3FEA233AF0}.Release|Any CPU.Build.0 = Release|Any CPU + {17F5C0B7-7932-4536-9328-5BBB0EC02A3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17F5C0B7-7932-4536-9328-5BBB0EC02A3B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17F5C0B7-7932-4536-9328-5BBB0EC02A3B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17F5C0B7-7932-4536-9328-5BBB0EC02A3B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/drivers/spanner-ado-net/spanner-ado-net/AssemblyInfo.cs b/drivers/spanner-ado-net/spanner-ado-net/AssemblyInfo.cs index 9c773122..e1f3d346 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/AssemblyInfo.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/AssemblyInfo.cs @@ -1,3 +1,4 @@ using System.Runtime.CompilerServices; [assembly:InternalsVisibleTo("Google.Cloud.Spanner.DataProvider.Tests")] +[assembly:InternalsVisibleTo("Google.Cloud.Spanner.DataProvider.Samples.Tests")] [assembly:InternalsVisibleTo("Google.Cloud.Spanner.DataProvider.SpecificationTests")] From afbfd6525c7105a164457992a5d3badcc5cf1f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Tue, 16 Dec 2025 14:10:33 +0100 Subject: [PATCH 25/29] test: add more sample tests --- .../spanner-ado-net-benchmarks/deploy.txt | 8 +- .../spanner-ado-net-benchmarks/run.txt | 81 +++++++++++++++++ .../SamplesMockServerTests.cs | 46 ++++++++++ .../Snippets/DdlBatchSample.cs | 3 +- spannerlib/grpc-server/server_test.go | 89 +++++++++++++++++++ 5 files changed, 221 insertions(+), 6 deletions(-) diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt index 08729507..57e8b0ba 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/deploy.txt @@ -102,7 +102,7 @@ gcloud run deploy spannerlib-dotnet-benchmark-point-dml-client-lib \ --source . -# SpannerLib, 50 clients - ReadWriteTx +# SpannerLib, 10 clients - ReadWriteTx gcloud run deploy spannerlib-dotnet-benchmark-read-write-tx \ --region=europe-north1 \ @@ -114,7 +114,7 @@ gcloud run deploy spannerlib-dotnet-benchmark-read-write-tx \ --source . -# ClientLib, 50 clients - ReadWriteTx +# ClientLib, 10 clients - ReadWriteTx gcloud run deploy spannerlib-dotnet-benchmark-read-write-tx-client-lib \ --region=europe-north1 \ @@ -137,7 +137,7 @@ gcloud run deploy spannerlib-dotnet-benchmark-read-write-tx-exp \ --base-image dotnet8 \ --source . -# SpannerLib, 50 clients - LargeQuery +# SpannerLib, 5 clients - LargeQuery gcloud run deploy spannerlib-dotnet-benchmark-large-query \ --region=europe-north1 \ @@ -148,7 +148,7 @@ gcloud run deploy spannerlib-dotnet-benchmark-large-query \ --base-image dotnet8 \ --source . -# ClientLib, 50 clients - LargeQuery +# ClientLib, 5 clients - LargeQuery gcloud run deploy spannerlib-dotnet-benchmark-large-query-client-lib \ --region=europe-north1 \ diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/run.txt b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/run.txt index 6990db0c..48be7559 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/run.txt +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/run.txt @@ -21,6 +21,29 @@ cd bin/Release/net8.0 ./Google.Cloud.Spanner.DataProvider.Benchmarks +# SpannerLib, 50 clients - Scalar + +export CREATE_ALIVENESS_SERVER=false +export NUM_WAREHOUSES=100 +export NUM_CLIENTS=50 +export TRANSACTION_TYPE=Scalar +dotnet build --configuration Release +cd bin/Release/net8.0 +./Google.Cloud.Spanner.DataProvider.Benchmarks + + +# ClientLib, 50 clients - Scalar + +export CREATE_ALIVENESS_SERVER=false +export NUM_WAREHOUSES=100 +export NUM_CLIENTS=50 +export TRANSACTION_TYPE=Scalar +export CLIENT_TYPE=ClientLib +dotnet build --configuration Release +cd bin/Release/net8.0 +./Google.Cloud.Spanner.DataProvider.Benchmarks + + # SpannerLib, 50 clients - PointDml export CREATE_ALIVENESS_SERVER=false @@ -32,3 +55,61 @@ cd bin/Release/net8.0 ./Google.Cloud.Spanner.DataProvider.Benchmarks +# ClientLib, 50 clients - PointDml + +export CREATE_ALIVENESS_SERVER=false +export NUM_WAREHOUSES=100 +export NUM_CLIENTS=50 +export TRANSACTION_TYPE=PointDml +export CLIENT_TYPE=ClientLib +dotnet build --configuration Release +cd bin/Release/net8.0 +./Google.Cloud.Spanner.DataProvider.Benchmarks + + +# SpannerLib, 10 clients - ReadWriteTx + +export CREATE_ALIVENESS_SERVER=false +export NUM_WAREHOUSES=100 +export NUM_CLIENTS=10 +export TRANSACTION_TYPE=ReadWriteTx +dotnet build --configuration Release +cd bin/Release/net8.0 +./Google.Cloud.Spanner.DataProvider.Benchmarks + + +# ClientLib, 10 clients - ReadWriteTx + +export CREATE_ALIVENESS_SERVER=false +export NUM_WAREHOUSES=100 +export NUM_CLIENTS=10 +export TRANSACTION_TYPE=ReadWriteTx +export CLIENT_TYPE=ClientLib +dotnet build --configuration Release +cd bin/Release/net8.0 +./Google.Cloud.Spanner.DataProvider.Benchmarks + + +# SpannerLib, 5 clients - LargeQuery + +export CREATE_ALIVENESS_SERVER=false +export NUM_WAREHOUSES=100 +export NUM_CLIENTS=5 +export TRANSACTION_TYPE=LargeQuery +dotnet build --configuration Release +cd bin/Release/net8.0 +./Google.Cloud.Spanner.DataProvider.Benchmarks + + +# ClientLib, 5 clients - LargeQuery + +export CREATE_ALIVENESS_SERVER=false +export NUM_WAREHOUSES=100 +export NUM_CLIENTS=5 +export TRANSACTION_TYPE=LargeQuery +export CLIENT_TYPE=ClientLib +dotnet build --configuration Release +cd bin/Release/net8.0 +./Google.Cloud.Spanner.DataProvider.Benchmarks + + diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples-tests/SamplesMockServerTests.cs b/drivers/spanner-ado-net/spanner-ado-net-samples-tests/SamplesMockServerTests.cs index 49162fb0..3981b146 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples-tests/SamplesMockServerTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples-tests/SamplesMockServerTests.cs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Google.Cloud.Spanner.Admin.Database.V1; using Google.Cloud.Spanner.DataProvider.Samples.Snippets; using Google.Cloud.Spanner.V1; using Google.Cloud.SpannerLib.MockServer; @@ -157,4 +158,49 @@ public async Task TestDataTypesSample() Assert.That(request.Params.Fields["string"].StringValue, Is.EqualTo("test-string")); Assert.That(request.Params.Fields["timestamp"].StringValue, Is.Not.Null); } + + [Test] + public async Task TestDdlBatchSample() + { + await DdlBatchSample.Run(ConnectionString); + + Assert.That(Writer.ToString(), Is.EqualTo( + $"Executed ADO.NET batch{Environment.NewLine}" + + $"Executed DDL batch{Environment.NewLine}" + + $"Executed a single SQL string with multiple DDL statements as one batch.{Environment.NewLine}")); + + var requests = Fixture.DatabaseAdminMock.Requests.OfType().ToList(); + Assert.That(requests.Count, Is.EqualTo(3)); + foreach (var request in requests) + { + Assert.That(request.Statements.Count, Is.EqualTo(2)); + } + } + + [Test] + public async Task TestDmlBatchSample() + { + const string insert = + "insert or update into Singers (SingerId, FirstName, LastName) values (@Id, @FirstName, @LastName)"; + Fixture.SpannerMock.AddOrUpdateStatementResult(insert, StatementResult.CreateUpdateCount(1L)); + const string update = "update Singers set BirthDate = NULL where BirthDate < DATE '1900-01-01'"; + Fixture.SpannerMock.AddOrUpdateStatementResult(update, StatementResult.CreateUpdateCount(10L)); + + await DmlBatchSample.Run(ConnectionString); + + Assert.That(Writer.ToString(), Is.EqualTo( + $"Executed ADO.NET batch{Environment.NewLine}" + + $"Affected: 13{Environment.NewLine}{Environment.NewLine}" + + $"Executed DML batch{Environment.NewLine}" + + $"Affected: -1{Environment.NewLine}{Environment.NewLine}")); + + var executeRequests = Fixture.SpannerMock.Requests.OfType().ToList(); + Assert.That(executeRequests, Is.Empty); + var requests = Fixture.SpannerMock.Requests.OfType().ToList(); + Assert.That(requests.Count, Is.EqualTo(2)); + foreach (var request in requests) + { + Assert.That(request.Statements.Count, Is.EqualTo(4)); + } + } } \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DdlBatchSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DdlBatchSample.cs index 287b5846..f87274ff 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DdlBatchSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DdlBatchSample.cs @@ -38,8 +38,7 @@ public static async Task Run(string connectionString) await ExecuteWithStartBatchDdl(connection, statements); // Execute a batch of DDL statements as a single command text. - // TODO: Enable when the library supports multi-statement SQL strings. - // await ExecuteAsSingleCommand(connection, statements); + await ExecuteAsSingleCommand(connection, statements); } private static async Task ExecuteAsSingleCommand(SpannerConnection connection, string[] statements) diff --git a/spannerlib/grpc-server/server_test.go b/spannerlib/grpc-server/server_test.go index 9224d394..2413e1e6 100644 --- a/spannerlib/grpc-server/server_test.go +++ b/spannerlib/grpc-server/server_test.go @@ -919,6 +919,95 @@ func TestExecuteBatchBidi(t *testing.T) { } } +func TestExecuteBatchDmlScriptBidi(t *testing.T) { + t.Parallel() + ctx := context.Background() + + server, teardown := setupMockSpannerServer(t) + defer teardown() + dsn := fmt.Sprintf("%s/projects/p/instances/i/databases/d?useplaintext=true", server.Address) + + client, cleanup := startTestSpannerLibServer(t) + defer cleanup() + + pool, err := client.CreatePool(ctx, &pb.CreatePoolRequest{ConnectionString: dsn}) + if err != nil { + t.Fatalf("failed to create pool: %v", err) + } + connection, err := client.CreateConnection(ctx, &pb.CreateConnectionRequest{Pool: pool}) + if err != nil { + t.Fatalf("failed to create connection: %v", err) + } + stream, err := client.ConnectionStream(ctx) + if err != nil { + t.Fatalf("failed to start stream: %v", err) + } + + if err := stream.Send(&pb.ConnectionStreamRequest{ + Request: &pb.ConnectionStreamRequest_ExecuteRequest{ + ExecuteRequest: &pb.ExecuteRequest{ + Connection: connection, + ExecuteSqlRequest: &sppb.ExecuteSqlRequest{ + Sql: "START BATCH DML", + }, + }, + }, + }); err != nil { + t.Fatalf("failed to send START BATCH DML request: %v", err) + } + _, err = stream.Recv() + if err != nil { + t.Fatalf("failed to start batch: %v", err) + } + for range 2 { + if err := stream.Send(&pb.ConnectionStreamRequest{ + Request: &pb.ConnectionStreamRequest_ExecuteRequest{ + ExecuteRequest: &pb.ExecuteRequest{ + Connection: connection, + ExecuteSqlRequest: &sppb.ExecuteSqlRequest{ + Sql: testutil.UpdateBarSetFoo, + }, + }, + }, + }); err != nil { + t.Fatalf("failed to send DML request: %v", err) + } + _, err := stream.Recv() + if err != nil { + t.Fatalf("failed to execute dml: %v", err) + } + } + if err := stream.Send(&pb.ConnectionStreamRequest{ + Request: &pb.ConnectionStreamRequest_ExecuteRequest{ + ExecuteRequest: &pb.ExecuteRequest{ + Connection: connection, + ExecuteSqlRequest: &sppb.ExecuteSqlRequest{ + Sql: "RUN BATCH", + }, + }, + }, + }); err != nil { + t.Fatalf("failed to send RUN BATCH request: %v", err) + } + _, err = stream.Recv() + if err != nil { + t.Fatalf("failed to execute batch: %v", err) + } + + if err := stream.CloseSend(); err != nil { + t.Fatalf("failed to close send: %v", err) + } + if _, err := client.ClosePool(ctx, pool); err != nil { + t.Fatalf("failed to close pool: %v", err) + } + + requests := server.TestSpanner.DrainRequestsFromServer() + executeRequests := testutil.RequestsOfType(requests, reflect.TypeOf(&sppb.ExecuteSqlRequest{})) + if g, w := len(executeRequests), 0; g != w { + t.Fatalf("num execute requests mismatch\n Got: %v\nWant: %v", g, w) + } +} + func TestTransaction(t *testing.T) { t.Parallel() ctx := context.Background() From 30dda1f9bce4e4a1647e1ce6435d66454e3a518c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Wed, 17 Dec 2025 10:24:48 +0100 Subject: [PATCH 26/29] chore: add more tests --- conn.go | 12 +- .../SamplesMockServerTests.cs | 37 ++++++ .../spanner-ado-net/spanner-ado-net.csproj | 7 ++ rows.go | 7 +- spannerlib/api/partitioned_dml_test.go | 109 ++++++++++++++++++ 5 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 spannerlib/api/partitioned_dml_test.go diff --git a/conn.go b/conn.go index 54cf5311..43043667 100644 --- a/conn.go +++ b/conn.go @@ -1055,11 +1055,19 @@ func (c *conn) queryContext(ctx context.Context, query string, execOptions *Exec return nil, err } statementInfo := c.parser.DetectStatementType(query) + + // TODO: Refactor queryContext and execContext into one unified method. // DDL statements are not supported in QueryContext so use the execContext method for the execution. // DML statements that are executed during a DML batch should also be re-routed to execContext for execution. - if statementInfo.StatementType == parser.StatementTypeDdl || (c.InDMLBatch() && + shouldUseDmlBatch := c.InDMLBatch() && + statementInfo.StatementType == parser.StatementTypeDml && + !statementInfo.HasThenReturn + // DML statements that should use a Partitioned DML transaction should also be re-routed to execContext. + shouldUsePDML := !c.inTransaction() && statementInfo.StatementType == parser.StatementTypeDml && - !statementInfo.HasThenReturn) { + !statementInfo.HasThenReturn && + c.AutocommitDMLMode() == PartitionedNonAtomic + if statementInfo.StatementType == parser.StatementTypeDdl || shouldUseDmlBatch || shouldUsePDML { res, err := c.execContext(ctx, query, execOptions, args) if err != nil { return nil, err diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples-tests/SamplesMockServerTests.cs b/drivers/spanner-ado-net/spanner-ado-net-samples-tests/SamplesMockServerTests.cs index 3981b146..f970c8f3 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples-tests/SamplesMockServerTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples-tests/SamplesMockServerTests.cs @@ -203,4 +203,41 @@ public async Task TestDmlBatchSample() Assert.That(request.Statements.Count, Is.EqualTo(4)); } } + + [Test] + public async Task TestMutationsSample() + { + await MutationsSample.Run(ConnectionString); + + Assert.That(Writer.ToString(), Is.EqualTo($"Inserted data using mutations. Affected: 1{Environment.NewLine}{Environment.NewLine}")); + var beginRequests = Fixture.SpannerMock.Requests.OfType().ToList(); + Assert.That(beginRequests, Has.Count.EqualTo(1)); + var beginRequest = beginRequests[0]; + Assert.That(beginRequest.MutationKey?.Insert?.Table, Is.Not.Null); + Assert.That(beginRequest.MutationKey.Insert.Table, Is.EqualTo("Singers")); + Assert.That(beginRequest.Options?.ReadWrite, Is.Not.Null); + var commitRequests = Fixture.SpannerMock.Requests.OfType().ToList(); + Assert.That(commitRequests, Has.Count.EqualTo(1)); + var commitRequest = commitRequests[0]; + Assert.That(commitRequest.Mutations, Has.Count.EqualTo(1)); + } + + [Test] + public async Task TestPartitionedDmlSample() + { + const string sql = "update Singers set BirthDate=date '1900-01-01' where BirthDate is null"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(100L)); + + await PartitionedDmlSample.Run(ConnectionString); + + Assert.That(Writer.ToString(), Is.EqualTo($"Executed a Partitioned DML statement. Affected: 100{Environment.NewLine}{Environment.NewLine}")); + // var beginRequests = Fixture.SpannerMock.Requests.OfType().ToList(); + // Assert.That(beginRequests, Has.Count.EqualTo(1)); + // var beginRequest = beginRequests[0]; + // Assert.That(beginRequest.Options?.PartitionedDml, Is.Not.Null); + var executeRequests = Fixture.SpannerMock.Requests.OfType().ToList(); + Assert.That(executeRequests, Has.Count.EqualTo(1)); + var executeRequest = executeRequests[0]; + Assert.That(executeRequest.Transaction?.Begin?.PartitionedDml, Is.Not.Null); + } } \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj b/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj index 7840e09d..3e160df4 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj +++ b/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj @@ -29,4 +29,11 @@ Alpha version: Not for production use + + + + + + + diff --git a/rows.go b/rows.go index 2c33ef6d..a6bb8244 100644 --- a/rows.go +++ b/rows.go @@ -499,11 +499,16 @@ type emptyRows struct { stats *sppb.ResultSetStats } -func createDriverResultRows(_ driver.Result, cancel context.CancelFunc, opts *ExecOptions) *emptyRows { +func createDriverResultRows(result driver.Result, cancel context.CancelFunc, opts *ExecOptions) *emptyRows { + stats := emptyRowsStats + if affected, err := result.RowsAffected(); err == nil { + stats = &sppb.ResultSetStats{RowCount: &sppb.ResultSetStats_RowCountExact{RowCountExact: affected}} + } res := &emptyRows{ cancel: cancel, returnResultSetMetadata: opts.ReturnResultSetMetadata, returnResultSetStats: opts.ReturnResultSetStats, + stats: stats, } if !opts.ReturnResultSetMetadata { res.currentResultSetType = resultSetTypeResults diff --git a/spannerlib/api/partitioned_dml_test.go b/spannerlib/api/partitioned_dml_test.go new file mode 100644 index 00000000..bb1d3b4a --- /dev/null +++ b/spannerlib/api/partitioned_dml_test.go @@ -0,0 +1,109 @@ +// Copyright 2025 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 +// +// https://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. + +package api + +import ( + "context" + "fmt" + "reflect" + "testing" + + "cloud.google.com/go/spanner/apiv1/spannerpb" + "github.com/googleapis/go-sql-spanner/testutil" +) + +func TestPartitionedDml(t *testing.T) { + t.Parallel() + + ctx := context.Background() + server, teardown := setupMockServer(t) + defer teardown() + dsn := fmt.Sprintf("%s/projects/p/instances/i/databases/d?useplaintext=true", server.Address) + + poolId, err := CreatePool(ctx, dsn) + if err != nil { + t.Fatalf("CreatePool returned unexpected error: %v", err) + } + connId, err := CreateConnection(ctx, poolId) + if err != nil { + t.Fatalf("CreateConnection returned unexpected error: %v", err) + } + + if err := executeAndDiscard(ctx, poolId, connId, "set autocommit_dml_mode='partitioned_non_atomic'"); err != nil { + t.Fatalf("set autocommit_dml_mode returned unexpected error: %v", err) + } + if affected, err := executeAndReturnUpdateCount(ctx, poolId, connId, testutil.UpdateBarSetFoo); err != nil { + t.Fatalf("execute returned unexpected error: %v", err) + } else { + if g, w := affected, int64(testutil.UpdateBarSetFooRowCount); g != w { + t.Fatalf("update count mismatch\n Got: %v\nWant: %v", g, w) + } + } + + // Verify that the statement used Partitioned DML. + requests := server.TestSpanner.DrainRequestsFromServer() + beginRequests := testutil.RequestsOfType(requests, reflect.TypeOf(&spannerpb.BeginTransactionRequest{})) + if g, w := len(beginRequests), 1; g != w { + t.Fatalf("BeginTransaction request count mismatch\n Got: %v\nWant: %v", g, w) + } + executeRequests := testutil.RequestsOfType(requests, reflect.TypeOf(&spannerpb.ExecuteSqlRequest{})) + if g, w := len(executeRequests), 1; g != w { + t.Fatalf("Execute request count mismatch\n Got: %v\nWant: %v", g, w) + } + executeRequest := executeRequests[0].(*spannerpb.ExecuteSqlRequest) + if executeRequest.GetTransaction() == nil || len(executeRequest.GetTransaction().GetId()) == 0 { + t.Fatalf("missing transaction on request: %v", executeRequest) + } + // Partitioned DML is not committed. + commitRequests := testutil.RequestsOfType(requests, reflect.TypeOf(&spannerpb.CommitRequest{})) + if g, w := len(commitRequests), 0; g != w { + t.Fatalf("Commit request count mismatch\n Got: %v\nWant: %v", g, w) + } + + if err := CloseConnection(ctx, poolId, connId); err != nil { + t.Fatalf("CloseConnection returned unexpected error: %v", err) + } + if err := ClosePool(ctx, poolId); err != nil { + t.Fatalf("ClosePool returned unexpected error: %v", err) + } +} + +func executeAndDiscard(ctx context.Context, poolId, connId int64, query string) error { + if rowsId, err := Execute(ctx, poolId, connId, &spannerpb.ExecuteSqlRequest{Sql: query}); err != nil { + return err + } else { + _ = CloseRows(ctx, poolId, connId, rowsId) + } + return nil +} + +func executeAndReturnUpdateCount(ctx context.Context, poolId, connId int64, query string) (int64, error) { + if rowsId, err := Execute(ctx, poolId, connId, &spannerpb.ExecuteSqlRequest{Sql: query}); err != nil { + return 0, err + } else { + defer func() { _ = CloseRows(ctx, poolId, connId, rowsId) }() + if stats, err := ResultSetStats(ctx, poolId, connId, rowsId); err != nil { + return 0, err + } else { + switch stats.GetRowCount().(type) { + case *spannerpb.ResultSetStats_RowCountExact: + return stats.GetRowCountExact(), nil + case *spannerpb.ResultSetStats_RowCountLowerBound: + return stats.GetRowCountLowerBound(), nil + } + return 0, fmt.Errorf("unexpected result set stats type: %v", stats.GetRowCount()) + } + } +} From e6c7ca38465a0cf8ca6cef38130acbadf4bdba2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Wed, 17 Dec 2025 15:41:05 +0100 Subject: [PATCH 27/29] chore: update benchmarks --- .../spanner-ado-net-benchmarks/run.txt | 55 +++++++++++++-- .../spanner-ado-net-benchmarks.csproj | 3 +- .../tpcc/BasicsRunner.cs | 68 +++++++++++++------ .../tpcc/Program.cs | 15 ++-- .../tpcc/TpccRunner.cs | 23 +++++++ .../spanner-ado-net-tests/BasicTests.cs | 7 +- .../spanner-ado-net/spanner-ado-net.csproj | 10 +-- .../spannerlib-dotnet-grpc-impl.csproj | 4 +- .../spannerlib-dotnet-grpc-server.csproj | 2 +- .../spannerlib-dotnet-grpc-v1.csproj | 2 +- .../spannerlib-dotnet-mockserver.csproj | 2 +- .../spannerlib-dotnet-native-impl.csproj | 4 +- .../spannerlib-dotnet-native.csproj | 2 +- .../spannerlib-dotnet.csproj | 2 +- 14 files changed, 152 insertions(+), 47 deletions(-) diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/run.txt b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/run.txt index 48be7559..f8735a79 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/run.txt +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/run.txt @@ -1,8 +1,10 @@ # SpannerLib, 50 clients - PointQuery +export GOOGLE_APPLICATION_CREDENTIALS=/home/loite/spanner-staging-testing.json export CREATE_ALIVENESS_SERVER=false export NUM_WAREHOUSES=100 export NUM_CLIENTS=50 +export TRANSACTIONS_PER_SECOND=10 export TRANSACTION_TYPE=PointQuery dotnet build --configuration Release cd bin/Release/net8.0 @@ -11,9 +13,11 @@ cd bin/Release/net8.0 # ClientLib, 50 clients - PointQuery +export GOOGLE_APPLICATION_CREDENTIALS=/home/loite/spanner-staging-testing.json export CREATE_ALIVENESS_SERVER=false export NUM_WAREHOUSES=100 export NUM_CLIENTS=50 +export TRANSACTIONS_PER_SECOND=10 export TRANSACTION_TYPE=PointQuery export CLIENT_TYPE=ClientLib dotnet build --configuration Release @@ -23,9 +27,11 @@ cd bin/Release/net8.0 # SpannerLib, 50 clients - Scalar +export GOOGLE_APPLICATION_CREDENTIALS=/home/loite/spanner-staging-testing.json export CREATE_ALIVENESS_SERVER=false export NUM_WAREHOUSES=100 export NUM_CLIENTS=50 +export TRANSACTIONS_PER_SECOND=10 export TRANSACTION_TYPE=Scalar dotnet build --configuration Release cd bin/Release/net8.0 @@ -34,9 +40,11 @@ cd bin/Release/net8.0 # ClientLib, 50 clients - Scalar +export GOOGLE_APPLICATION_CREDENTIALS=/home/loite/spanner-staging-testing.json export CREATE_ALIVENESS_SERVER=false export NUM_WAREHOUSES=100 export NUM_CLIENTS=50 +export TRANSACTIONS_PER_SECOND=10 export TRANSACTION_TYPE=Scalar export CLIENT_TYPE=ClientLib dotnet build --configuration Release @@ -46,9 +54,11 @@ cd bin/Release/net8.0 # SpannerLib, 50 clients - PointDml +export GOOGLE_APPLICATION_CREDENTIALS=/home/loite/spanner-staging-testing.json export CREATE_ALIVENESS_SERVER=false export NUM_WAREHOUSES=100 export NUM_CLIENTS=50 +export TRANSACTIONS_PER_SECOND=10 export TRANSACTION_TYPE=PointDml dotnet build --configuration Release cd bin/Release/net8.0 @@ -57,9 +67,11 @@ cd bin/Release/net8.0 # ClientLib, 50 clients - PointDml +export GOOGLE_APPLICATION_CREDENTIALS=/home/loite/spanner-staging-testing.json export CREATE_ALIVENESS_SERVER=false export NUM_WAREHOUSES=100 export NUM_CLIENTS=50 +export TRANSACTIONS_PER_SECOND=10 export TRANSACTION_TYPE=PointDml export CLIENT_TYPE=ClientLib dotnet build --configuration Release @@ -67,22 +79,26 @@ cd bin/Release/net8.0 ./Google.Cloud.Spanner.DataProvider.Benchmarks -# SpannerLib, 10 clients - ReadWriteTx +# SpannerLib, 50 clients - ReadWriteTx +export GOOGLE_APPLICATION_CREDENTIALS=/home/loite/spanner-staging-testing.json export CREATE_ALIVENESS_SERVER=false export NUM_WAREHOUSES=100 -export NUM_CLIENTS=10 +export NUM_CLIENTS=50 +export TRANSACTIONS_PER_SECOND=2 export TRANSACTION_TYPE=ReadWriteTx dotnet build --configuration Release cd bin/Release/net8.0 ./Google.Cloud.Spanner.DataProvider.Benchmarks -# ClientLib, 10 clients - ReadWriteTx +# ClientLib, 50 clients - ReadWriteTx +export GOOGLE_APPLICATION_CREDENTIALS=/home/loite/spanner-staging-testing.json export CREATE_ALIVENESS_SERVER=false export NUM_WAREHOUSES=100 -export NUM_CLIENTS=10 +export NUM_CLIENTS=50 +export TRANSACTIONS_PER_SECOND=2 export TRANSACTION_TYPE=ReadWriteTx export CLIENT_TYPE=ClientLib dotnet build --configuration Release @@ -92,9 +108,11 @@ cd bin/Release/net8.0 # SpannerLib, 5 clients - LargeQuery +export GOOGLE_APPLICATION_CREDENTIALS=/home/loite/spanner-staging-testing.json export CREATE_ALIVENESS_SERVER=false export NUM_WAREHOUSES=100 export NUM_CLIENTS=5 +export TRANSACTIONS_PER_SECOND=1 export TRANSACTION_TYPE=LargeQuery dotnet build --configuration Release cd bin/Release/net8.0 @@ -103,9 +121,11 @@ cd bin/Release/net8.0 # ClientLib, 5 clients - LargeQuery +export GOOGLE_APPLICATION_CREDENTIALS=/home/loite/spanner-staging-testing.json export CREATE_ALIVENESS_SERVER=false export NUM_WAREHOUSES=100 export NUM_CLIENTS=5 +export TRANSACTIONS_PER_SECOND=1 export TRANSACTION_TYPE=LargeQuery export CLIENT_TYPE=ClientLib dotnet build --configuration Release @@ -113,3 +133,30 @@ cd bin/Release/net8.0 ./Google.Cloud.Spanner.DataProvider.Benchmarks +# SpannerLib, 5 clients - TPC-C + +export GOOGLE_APPLICATION_CREDENTIALS=/home/loite/spanner-staging-testing.json +export CREATE_ALIVENESS_SERVER=false +export NUM_WAREHOUSES=100 +export NUM_CLIENTS=5 +export TRANSACTIONS_PER_SECOND=1 +export TRANSACTION_TYPE=Tpcc +dotnet build --configuration Release +cd bin/Release/net8.0 +./Google.Cloud.Spanner.DataProvider.Benchmarks + + +# ClientLib, 5 clients - TPC-C + +export GOOGLE_APPLICATION_CREDENTIALS=/home/loite/spanner-staging-testing.json +export CREATE_ALIVENESS_SERVER=false +export NUM_WAREHOUSES=100 +export NUM_CLIENTS=5 +export TRANSACTIONS_PER_SECOND=1 +export TRANSACTION_TYPE=Tpcc +export CLIENT_TYPE=ClientLib +dotnet build --configuration Release +cd bin/Release/net8.0 +./Google.Cloud.Spanner.DataProvider.Benchmarks + + diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/spanner-ado-net-benchmarks.csproj b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/spanner-ado-net-benchmarks.csproj index 6ecaab49..0ecc50a0 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/spanner-ado-net-benchmarks.csproj +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/spanner-ado-net-benchmarks.csproj @@ -8,10 +8,11 @@ enable Google.Cloud.Spanner.DataProvider.Benchmarks default + LatestMajor - + diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/BasicsRunner.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/BasicsRunner.cs index ea6f7187..147aabab 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/BasicsRunner.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/BasicsRunner.cs @@ -7,6 +7,7 @@ namespace Google.Cloud.Spanner.DataProvider.Benchmarks.tpcc; public class BasicsRunner : AbstractRunner { + private readonly double _delayBetweenTransactions; private readonly Stats _stats; private readonly DbConnection _connection; private readonly Program.BenchmarkType _benchmarkType; @@ -16,6 +17,7 @@ public class BasicsRunner : AbstractRunner private readonly int _numItems; internal BasicsRunner( + long transactionsPerSecond, Stats stats, DbConnection connection, Program.BenchmarkType benchmarkType, @@ -24,6 +26,14 @@ internal BasicsRunner( int numCustomersPerDistrict = 3000, int numItems = 100_000) { + if (transactionsPerSecond > 0) + { + _delayBetweenTransactions = 1000d / transactionsPerSecond; + } + else + { + _delayBetweenTransactions = 0; + } _stats = stats; _connection = connection; _benchmarkType = benchmarkType; @@ -37,8 +47,19 @@ public override async Task RunAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { + var stopwatch = Stopwatch.StartNew(); await RunTransactionAsync(cancellationToken); - var delay = Random.Shared.Next(10, 100); + stopwatch.Stop(); + int delay; + if (_delayBetweenTransactions > 0) + { + delay = (int) (_delayBetweenTransactions - stopwatch.ElapsedMilliseconds); + } + else + { + delay = Random.Shared.Next(10, 100); + } + delay = Math.Max(delay, 0); await Task.Delay(delay, cancellationToken); } } @@ -139,7 +160,7 @@ private async Task ReadWriteTransactionAsync(CancellationToken cancellationToken else { await using var cmd = _connection.CreateCommand(); - cmd.CommandText = "set local transaction_tag = 'spanner-lib-exp'"; + cmd.CommandText = "set local transaction_tag = 'spanner-lib'"; await cmd.ExecuteNonQueryAsync(cancellationToken); } @@ -185,37 +206,46 @@ private async Task ReadWriteTransactionAsync(CancellationToken cancellationToken await transaction.CommitAsync(cancellationToken); watch.Stop(); - _stats.RegisterOperationLatency(Program.OperationType.ReadWriteTxExp, watch.Elapsed.TotalMilliseconds); + _stats.RegisterOperationLatency(Program.OperationType.ReadWriteTx, watch.Elapsed.TotalMilliseconds); break; } catch (Exception exception) { - _stats.RegisterFailedOperation(Program.OperationType.ReadWriteTxExp, watch.Elapsed, exception); + _stats.RegisterFailedOperation(Program.OperationType.ReadWriteTx, watch.Elapsed, exception); } } } private async Task LargeQueryAsync(CancellationToken cancellationToken) { - await using var command = _connection.CreateCommand(); - command.CommandText = "select * from customer limit $1 offset $2"; - AddParameter(command, "p1", 100_000L); - AddParameter(command, "p2", Random.Shared.Next(1, 1001)); - await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); - // Fetch the first row outside the measurement to ensure a fair comparison between clients that delay the - // query execution to the first read, and those that don't. - await reader.ReadAsync(cancellationToken).ConfigureAwait(false); - - var watch = Stopwatch.StartNew(); - while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + Stopwatch? watch = null; + try { - for (var col = 0; col < reader.FieldCount; col++) + await using var command = _connection.CreateCommand(); + command.CommandText = "select * from customer limit $1 offset $2"; + AddParameter(command, "p1", 100_000L); + AddParameter(command, "p2", Random.Shared.Next(1, 1001)); + await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + // Fetch the first row outside the measurement to ensure a fair comparison between clients that delay the + // query execution to the first read, and those that don't. + await reader.ReadAsync(cancellationToken).ConfigureAwait(false); + + watch = Stopwatch.StartNew(); + while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) { - _ = reader.GetValue(col); + for (var col = 0; col < reader.FieldCount; col++) + { + _ = reader.GetValue(col); + } } + watch.Stop(); + _stats.RegisterOperationLatency(Program.OperationType.LargeQuery, watch.Elapsed.TotalMilliseconds); + } + catch (Exception exception) + { + Console.WriteLine(exception.Message); + _stats.RegisterFailedOperation(Program.OperationType.LargeQuery, watch?.Elapsed ?? TimeSpan.Zero, exception); } - watch.Stop(); - _stats.RegisterOperationLatency(Program.OperationType.LargeQuery, watch.Elapsed.TotalMilliseconds); } private DbCommand CreatePointReadCommand() diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Program.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Program.cs index 5a670f41..0d961e6a 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Program.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/Program.cs @@ -42,7 +42,6 @@ public enum OperationType Scalar, PointDml, ReadWriteTx, - ReadWriteTxExp, NewOrder, Payment, OrderStatus, @@ -163,10 +162,11 @@ await SchemaUtil.CreateSchemaAsync(connection, DatabaseDialect.Postgresql, Console.WriteLine($"Running benchmark {transactionType}..."); Console.WriteLine($"Client type: {clientType}"); Console.WriteLine($"Num clients: {numClients}"); + Console.WriteLine($"Target TPS: {targetTps}"); Console.WriteLine($"Exporting stats: {exportStats}"); var stats = new Stats(exportStats, projectName, metricsClient, numTransactionsDescriptor, operationLatencyDescriptor, numClients, clientType, directPath); - if (targetTps > 0 && transactionType == BenchmarkType.Tpcc) + if (false && targetTps > 0 && transactionType == BenchmarkType.Tpcc) { var maxWaitTime = 2 * 1000 / targetTps; Console.WriteLine($"Clients: {numClients}"); @@ -175,7 +175,7 @@ await SchemaUtil.CreateSchemaAsync(connection, DatabaseDialect.Postgresql, var runners = new BlockingCollection(); for (var client = 0; client < numClients; client++) { - runners.Add(await CreateRunnerAsync(clientType, transactionType, connectionString, stats, numWarehouses, cancellationTokenSource), cancellationTokenSource.Token); + runners.Add(await CreateRunnerAsync(targetTps, clientType, transactionType, connectionString, stats, numWarehouses, cancellationTokenSource), cancellationTokenSource.Token); } var lastLogTime = DateTime.UtcNow; while (!cancellationTokenSource.IsCancellationRequested) @@ -220,7 +220,7 @@ await SchemaUtil.CreateSchemaAsync(connection, DatabaseDialect.Postgresql, var tasks = new List(); for (var client = 0; client < numClients; client++) { - var runner = await CreateRunnerAsync(clientType, transactionType, connectionString, stats, numWarehouses, cancellationTokenSource); + var runner = await CreateRunnerAsync(targetTps, clientType, transactionType, connectionString, stats, numWarehouses, cancellationTokenSource); tasks.Add(runner.RunAsync(cancellationTokenSource.Token)); } while (!cancellationTokenSource.Token.IsCancellationRequested) @@ -234,12 +234,13 @@ await SchemaUtil.CreateSchemaAsync(connection, DatabaseDialect.Postgresql, if (app != null && webapp != null) { - await app.StopAsync(cancellationTokenSource.Token); + await app.StopAsync(CancellationToken.None); await webapp; } } private static async Task CreateRunnerAsync( + int targetTps, ClientType clientType, BenchmarkType benchmarkType, string connectionString, @@ -260,8 +261,8 @@ private static async Task CreateRunnerAsync( await connection.OpenAsync(cancellationTokenSource.Token); if (benchmarkType == BenchmarkType.Tpcc) { - return new TpccRunner(stats, connection, numWarehouses); + return new TpccRunner(targetTps, stats, connection, numWarehouses); } - return new BasicsRunner(stats, connection, benchmarkType, numWarehouses); + return new BasicsRunner(targetTps, stats, connection, benchmarkType, numWarehouses); } } \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/TpccRunner.cs b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/TpccRunner.cs index 29fb9923..98507c31 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/TpccRunner.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/tpcc/TpccRunner.cs @@ -35,6 +35,7 @@ internal enum TransactionType StockLevel, } + private readonly double _delayBetweenTransactions; private readonly Stats _stats; private readonly DbConnection _connection; private readonly int _numWarehouses; @@ -46,6 +47,7 @@ internal enum TransactionType private DbTransaction? _currentTransaction; internal TpccRunner( + long transactionsPerSecond, Stats stats, DbConnection connection, int numWarehouses, @@ -53,6 +55,14 @@ internal TpccRunner( int numCustomersPerDistrict = 3000, int numItems = 100_000) { + if (transactionsPerSecond > 0) + { + _delayBetweenTransactions = 1000d / transactionsPerSecond; + } + else + { + _delayBetweenTransactions = 0; + } _stats = stats; _connection = connection; _numWarehouses = numWarehouses; @@ -66,7 +76,20 @@ public override async Task RunAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { + var stopwatch = Stopwatch.StartNew(); await RunTransactionAsync(cancellationToken); + stopwatch.Stop(); + int delay; + if (_delayBetweenTransactions > 0) + { + delay = (int) (_delayBetweenTransactions - stopwatch.ElapsedMilliseconds); + } + else + { + delay = Random.Shared.Next(10, 100); + } + delay = Math.Max(delay, 0); + await Task.Delay(delay, cancellationToken); } } diff --git a/drivers/spanner-ado-net/spanner-ado-net-tests/BasicTests.cs b/drivers/spanner-ado-net/spanner-ado-net-tests/BasicTests.cs index 3406fa3c..ddb00b67 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-tests/BasicTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-tests/BasicTests.cs @@ -144,10 +144,10 @@ public void TestInsertAllDataTypes() } [Test] - public void TestQueryAllDataTypes() + public void TestQueryAllDataTypes([Values(0, 1, 10, 49, 50, 51, 100)] int numRows) { const string sql = "select * from all_types"; - var result = RandomResultSetGenerator.Generate(10, allowNull: true); + var result = RandomResultSetGenerator.Generate(numRows, allowNull: true); Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateQuery(result)); using var connection = new SpannerConnection(); @@ -157,6 +157,7 @@ public void TestQueryAllDataTypes() using var cmd = connection.CreateCommand(); cmd.CommandText = sql; using var reader = cmd.ExecuteReader(); + var numRowsFound = 0; while (reader.Read()) { var index = 0; @@ -190,7 +191,9 @@ public void TestQueryAllDataTypes() Assert.That(reader.GetFieldValue>(reader.GetOrdinal("col_array_string")), Is.EqualTo(ValueOrNull(reader["col_array_string"]))); Assert.That(reader.GetFieldValue>(reader.GetOrdinal("col_array_timestamp")), Is.EqualTo(ValueOrNull(reader["col_array_timestamp"]))); Assert.That(reader.GetFieldValue>(reader.GetOrdinal("col_array_uuid")), Is.EqualTo(ValueOrNull(reader["col_array_uuid"]))); + numRowsFound++; } + Assert.That(numRowsFound, Is.EqualTo(numRows)); } private static object? ValueOrNull(object value) diff --git a/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj b/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj index 3e160df4..ed64b99e 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj +++ b/drivers/spanner-ado-net/spanner-ado-net/spanner-ado-net.csproj @@ -9,7 +9,7 @@ Alpha.Google.Cloud.Spanner.DataProvider .NET Data Provider for Spanner Google - 1.0.0-alpha.20251216092833 + 1.0.0-alpha.20251217172149 ADO.NET Data Provider. Alpha version: Not for production use @@ -19,10 +19,10 @@ Alpha version: Not for production use - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/spannerlib-dotnet-grpc-impl.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/spannerlib-dotnet-grpc-impl.csproj index 75155a29..e1456863 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/spannerlib-dotnet-grpc-impl.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-impl/spannerlib-dotnet-grpc-impl.csproj @@ -6,13 +6,13 @@ enable default Alpha.Google.Cloud.SpannerLib.GrpcImpl - 1.0.0-alpha.20251208195113 + 1.0.0-alpha.20251217171434 Google - + diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-server/spannerlib-dotnet-grpc-server.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-server/spannerlib-dotnet-grpc-server.csproj index 364a0387..58d1dbc7 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-server/spannerlib-dotnet-grpc-server.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-server/spannerlib-dotnet-grpc-server.csproj @@ -8,7 +8,7 @@ Alpha.Google.Cloud.SpannerLib.GrpcServer SpannerLib Grpc Server Google - 1.0.0-alpha.20251208195113 + 1.0.0-alpha.20251217171434 diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-v1/spannerlib-dotnet-grpc-v1.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-v1/spannerlib-dotnet-grpc-v1.csproj index f2128792..548743d6 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-v1/spannerlib-dotnet-grpc-v1.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-grpc-v1/spannerlib-dotnet-grpc-v1.csproj @@ -6,7 +6,7 @@ enable default Alpha.Google.Cloud.SpannerLib.Grpc.V1 - 1.0.0-alpha.20251208195113 + 1.0.0-alpha.20251217171434 Google diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/spannerlib-dotnet-mockserver.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/spannerlib-dotnet-mockserver.csproj index 0a07b579..53940e12 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/spannerlib-dotnet-mockserver.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/spannerlib-dotnet-mockserver.csproj @@ -6,7 +6,7 @@ enable enable Alpha.Google.Cloud.SpannerLib.MockServer - 1.0.0-alpha.20251208195113 + 1.0.0-alpha.20251217171434 Google default diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native-impl/spannerlib-dotnet-native-impl.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native-impl/spannerlib-dotnet-native-impl.csproj index 46f3f5b6..c1cbb16e 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native-impl/spannerlib-dotnet-native-impl.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native-impl/spannerlib-dotnet-native-impl.csproj @@ -6,7 +6,7 @@ enable default Alpha.Google.Cloud.SpannerLib.NativeImpl - 1.0.0-alpha.20251208195113 + 1.0.0-alpha.20251217171434 Google @@ -16,7 +16,7 @@ - + diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native/spannerlib-dotnet-native.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native/spannerlib-dotnet-native.csproj index 79ec180f..e2a70b90 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native/spannerlib-dotnet-native.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-native/spannerlib-dotnet-native.csproj @@ -9,7 +9,7 @@ Alpha.Google.Cloud.SpannerLib.Native .NET wrapper for the native SpannerLib shared library Google - 1.0.0-alpha.20251208195113 + 1.0.0-alpha.20251217171434 diff --git a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/spannerlib-dotnet.csproj b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/spannerlib-dotnet.csproj index 722f3e16..406094eb 100644 --- a/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/spannerlib-dotnet.csproj +++ b/spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet/spannerlib-dotnet.csproj @@ -6,7 +6,7 @@ enable default Alpha.Google.Cloud.SpannerLib - 1.0.0-alpha.20251208195113 + 1.0.0-alpha.20251217171434 Google From ff26cb6688f3a89acbe3297253d2e0f480924026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 18 Dec 2025 13:17:58 +0100 Subject: [PATCH 28/29] chore: add sample tests --- .github/workflows/ado-net-tests.yml | 4 + connection_properties.go | 12 + driver.go | 15 +- .../spanner-ado-net-benchmarks/run.txt | 8 +- .../GettingStartedTests.cs | 403 ++++++++++++++++++ .../RunSamplesTests.cs | 230 ++++++++++ .../SamplesMockServerTests.cs | 158 ++++++- .../spanner-ado-net-samples/AssemblyInfo.cs | 2 + .../spanner-ado-net-samples/EmulatorRunner.cs | 15 +- .../GettingStartedGuide/AddColumnSample.cs | 32 ++ .../AddColumnSamplePostgreSql.cs | 32 ++ .../CreateConnectionSample.cs | 50 +++ .../CreateConnectionSamplePostgreSql.cs | 50 +++ .../GettingStartedGuide/CreateTablesSample.cs | 43 ++ .../CreateTablesSamplePostgreSql.cs | 43 ++ .../GettingStartedGuide/DataBoostSample.cs | 28 ++ .../DataBoostSamplePostgreSql.cs | 28 ++ .../GettingStartedGuide/DdlBatchSample.cs | 51 +++ .../DdlBatchSamplePostgreSql.cs | 51 +++ .../PartitionedDmlSample.cs | 42 ++ .../PartitionedDmlSamplePostgreSql.cs | 42 ++ .../GettingStartedGuide/QueryDataSample.cs | 36 ++ .../QueryDataSamplePostgreSql.cs | 36 ++ .../QueryDataWithParameterSample.cs | 37 ++ .../QueryDataWithParameterSamplePostgreSql.cs | 37 ++ .../QueryNewColumnSample.cs | 36 ++ .../QueryNewColumnSamplePostgreSql.cs | 36 ++ .../ReadOnlyTransactionSample.cs | 59 +++ .../ReadOnlyTransactionSamplePostgreSql.cs | 59 +++ .../GettingStartedGuide/TagsSample.cs | 62 +++ .../TagsSamplePostgreSql.cs | 62 +++ .../UpdateDataWithMutationsSample.cs | 44 ++ ...UpdateDataWithMutationsSamplePostgreSql.cs | 44 ++ .../WriteDataWithDmlSample.cs | 51 +++ .../WriteDataWithDmlSamplePostgreSql.cs | 51 +++ .../WriteDataWithMutationsSample.cs | 78 ++++ .../WriteDataWithMutationsSamplePostgreSql.cs | 78 ++++ .../WriteDataWithTransactionSample.cs | 100 +++++ ...riteDataWithTransactionSamplePostgreSql.cs | 100 +++++ .../spanner-ado-net-samples/SampleRunner.cs | 15 +- .../Snippets/CommitTimestampSample.cs | 2 +- .../Snippets/CustomConfigurationSample.cs | 2 +- .../Snippets/DataTypesSample.cs | 2 +- .../Snippets/DdlBatchSample.cs | 2 +- .../Snippets/DmlBatchSample.cs | 4 +- .../Snippets/EmulatorSample.cs | 48 +++ .../Snippets/MutationsSample.cs | 2 +- .../Snippets/PartitionedDmlSample.cs | 2 +- .../Snippets/QueryParametersSample.cs | 2 +- .../Snippets/ReadOnlyTransactionSample.cs | 2 +- .../Snippets/StaleReadSample.cs | 9 +- .../Snippets/TagsSample.cs | 2 +- .../Snippets/TransactionSample.cs | 2 +- .../spanner-ado-net/SpannerBatch.cs | 134 +++++- .../spanner-ado-net/SpannerBatchCommand.cs | 27 ++ .../spanner-ado-net/SpannerCommand.cs | 9 +- .../spanner-ado-net/SpannerConnection.cs | 30 +- emulator_util.go | 21 +- 58 files changed, 2597 insertions(+), 65 deletions(-) create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples-tests/GettingStartedTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples-tests/RunSamplesTests.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/AssemblyInfo.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/AddColumnSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/AddColumnSamplePostgreSql.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateConnectionSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateConnectionSamplePostgreSql.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateTablesSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateTablesSamplePostgreSql.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DataBoostSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DataBoostSamplePostgreSql.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DdlBatchSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DdlBatchSamplePostgreSql.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/PartitionedDmlSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/PartitionedDmlSamplePostgreSql.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataSamplePostgreSql.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataWithParameterSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataWithParameterSamplePostgreSql.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryNewColumnSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryNewColumnSamplePostgreSql.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/ReadOnlyTransactionSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/ReadOnlyTransactionSamplePostgreSql.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/TagsSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/TagsSamplePostgreSql.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/UpdateDataWithMutationsSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/UpdateDataWithMutationsSamplePostgreSql.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithDmlSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithDmlSamplePostgreSql.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithMutationsSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithMutationsSamplePostgreSql.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithTransactionSample.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithTransactionSamplePostgreSql.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/EmulatorSample.cs diff --git a/.github/workflows/ado-net-tests.yml b/.github/workflows/ado-net-tests.yml index 11749d17..3514839f 100644 --- a/.github/workflows/ado-net-tests.yml +++ b/.github/workflows/ado-net-tests.yml @@ -62,3 +62,7 @@ jobs: working-directory: drivers/spanner-ado-net/spanner-ado-net-specification-tests run: dotnet test --verbosity normal shell: bash + - name: spanner-ado-net-sample-tests + working-directory: drivers/spanner-ado-net/spanner-ado-net-samples-tests + run: dotnet test --verbosity normal + shell: bash diff --git a/connection_properties.go b/connection_properties.go index a5c6258c..26877ec6 100644 --- a/connection_properties.go +++ b/connection_properties.go @@ -21,6 +21,7 @@ import ( "time" "cloud.google.com/go/spanner" + "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" "cloud.google.com/go/spanner/apiv1/spannerpb" "github.com/googleapis/go-sql-spanner/connectionstate" "github.com/googleapis/go-sql-spanner/parser" @@ -65,6 +66,17 @@ var propertyConnectionStateType = createConnectionProperty( return parseConnectionStateType(value) }, ) +var propertyDialect = createConnectionProperty( + "dialect", + "The default dialect to use when creating new databases using this connection. "+ + "You do not need to set this property for any existing database. The driver will "+ + "automatically detect the dialect of the database that you connect to.", + databasepb.DatabaseDialect_DATABASE_DIALECT_UNSPECIFIED, + false, + nil, + connectionstate.ContextUser, + parseDatabaseDialect, +) var propertyAutoConfigEmulator = createConnectionProperty( "auto_config_emulator", "Automatically configure the connection to try to connect to the Cloud Spanner emulator (true/false). "+ diff --git a/driver.go b/driver.go index 392cb410..36786199 100644 --- a/driver.go +++ b/driver.go @@ -683,7 +683,7 @@ func createConnector(d *Driver, connectorConfig ConnectorConfig) (*connector, er internaloption.SkipDialSettingsValidation(), } opts = append(emulatorOpts, opts...) - if err := autoConfigEmulator(context.Background(), connectorConfig.Host, connectorConfig.Project, connectorConfig.Instance, connectorConfig.Database, opts); err != nil { + if err := autoConfigEmulator(context.Background(), connectorConfig.Host, connectorConfig.Project, connectorConfig.Instance, connectorConfig.Database, propertyDialect.GetValueOrDefault(state), opts); err != nil { return nil, err } } @@ -1548,6 +1548,19 @@ func parseRpcPriority(val string) (spannerpb.RequestOptions_Priority, error) { return spannerpb.RequestOptions_PRIORITY_UNSPECIFIED, status.Errorf(codes.InvalidArgument, "invalid or unsupported priority: %v", val) } +func parseDatabaseDialect(val string) (databasepb.DatabaseDialect, error) { + val = strings.ToUpper(val) + if dialect, ok := databasepb.DatabaseDialect_value[val]; ok { + return databasepb.DatabaseDialect(dialect), nil + } + if !strings.HasPrefix(val, "DATABASEDIALECT_") { + if dialect, ok := databasepb.DatabaseDialect_value["DATABASEDIALECT_"+val]; ok { + return databasepb.DatabaseDialect(dialect), nil + } + } + return databasepb.DatabaseDialect_DATABASE_DIALECT_UNSPECIFIED, status.Errorf(codes.InvalidArgument, "invalid or unsupported dialect: %v", val) +} + func parseIsolationLevel(val string) (sql.IsolationLevel, error) { s := strings.ToLower(strings.TrimSpace(val)) s = strings.ReplaceAll(s, " ", "") diff --git a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/run.txt b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/run.txt index f8735a79..9bc9c55a 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-benchmarks/run.txt +++ b/drivers/spanner-ado-net/spanner-ado-net-benchmarks/run.txt @@ -133,12 +133,12 @@ cd bin/Release/net8.0 ./Google.Cloud.Spanner.DataProvider.Benchmarks -# SpannerLib, 5 clients - TPC-C +# SpannerLib, 50 clients - TPC-C export GOOGLE_APPLICATION_CREDENTIALS=/home/loite/spanner-staging-testing.json export CREATE_ALIVENESS_SERVER=false export NUM_WAREHOUSES=100 -export NUM_CLIENTS=5 +export NUM_CLIENTS=50 export TRANSACTIONS_PER_SECOND=1 export TRANSACTION_TYPE=Tpcc dotnet build --configuration Release @@ -146,12 +146,12 @@ cd bin/Release/net8.0 ./Google.Cloud.Spanner.DataProvider.Benchmarks -# ClientLib, 5 clients - TPC-C +# ClientLib, 50 clients - TPC-C export GOOGLE_APPLICATION_CREDENTIALS=/home/loite/spanner-staging-testing.json export CREATE_ALIVENESS_SERVER=false export NUM_WAREHOUSES=100 -export NUM_CLIENTS=5 +export NUM_CLIENTS=50 export TRANSACTIONS_PER_SECOND=1 export TRANSACTION_TYPE=Tpcc export CLIENT_TYPE=ClientLib diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples-tests/GettingStartedTests.cs b/drivers/spanner-ado-net/spanner-ado-net-samples-tests/GettingStartedTests.cs new file mode 100644 index 00000000..be5d7af4 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples-tests/GettingStartedTests.cs @@ -0,0 +1,403 @@ +// Copyright 2025 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 +// +// https://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. + +using Docker.DotNet; +using Docker.DotNet.Models; +using Google.Cloud.Spanner.Admin.Database.V1; +using Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +namespace Google.Cloud.Spanner.DataProvider.Samples.Tests; + +[TestFixture(DatabaseDialect.GoogleStandardSql)] +[TestFixture(DatabaseDialect.Postgresql)] +public class GettingStartedTests +{ + private static EmulatorRunner? _emulatorRunner; + private static PortBinding? _portBinding; + + [OneTimeSetUp] + public static async Task CheckEmulatorAvailability() + { + if (EmulatorRunner.IsEmulatorRunning()) + { + return; + } + using var client = EmulatorRunner.CreateDockerClient(); + try + { + client.System.GetVersionAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + } + catch (Exception) + { + Assert.Ignore("Docker and Emulator not available"); + } + _emulatorRunner = new EmulatorRunner(); + try + { + _portBinding = await _emulatorRunner.StartEmulator(); + } + catch (DockerImageNotFoundException) + { + _emulatorRunner = null; + Assert.Ignore("Docker and Emulator not available"); + } + } + + [OneTimeTearDown] + public async Task StopEmulator() + { + if (_emulatorRunner != null) + { + await _emulatorRunner.StopEmulator(); + } + } + + private readonly DatabaseDialect _dialect; + private readonly string _database; + private SpannerConnectionStringBuilder? _builder; + + private SpannerConnectionStringBuilder Builder + { + get + { + _builder ??= new() + { + Project = "emulator-project", + Instance = "sample-instance", + Database = _database, + AutoConfigEmulator = true, + Options = _dialect == DatabaseDialect.Postgresql ? "dialect=postgresql" : "", + Host = _portBinding?.HostIP ?? "", + Port = uint.Parse(_portBinding?.HostPort ?? "443"), + }; + return _builder; + } + } + + private StringWriter? _writer; + private TextWriter _output; + private StringWriter Writer => _writer ??= new StringWriter(); + + public GettingStartedTests(DatabaseDialect dialect) + { + _dialect = dialect; + _database = _dialect switch + { + DatabaseDialect.GoogleStandardSql => "sample-database", + DatabaseDialect.Postgresql => "sample-database-pg", + _ => throw new InvalidOperationException("Unknown dialect " + _dialect) + }; + if (EmulatorRunner.IsEmulatorRunning()) + { + DropSampleTables().GetAwaiter().GetResult(); + } + } + + private async Task DropSampleTables() + { + await using var connection = new SpannerConnection(Builder.ConnectionString); + await connection.OpenAsync(); + var batch = connection.CreateBatch(); + batch.BatchCommands.Add("drop table if exists Concerts"); + batch.BatchCommands.Add("drop table if exists Venues"); + batch.BatchCommands.Add("drop table if exists Albums"); + batch.BatchCommands.Add("drop table if exists Singers"); + await batch.ExecuteNonQueryAsync(); + } + + [SetUp] + public void SetupOutput() + { + _output = Console.Out; + _writer = new StringWriter(); + Console.SetOut(_writer); + } + + [TearDown] + public void CleanupOutput() + { + _writer?.Dispose(); + Console.SetOut(_output); + } + + // These tests must be ordered, so they match the order of the Getting Started Guide. + [Test, Order(1)] + public async Task TestCreateTables() + { + if (_dialect == DatabaseDialect.GoogleStandardSql) + { + await CreateTablesSample.CreateTables(Builder.ConnectionString); + } + else if (_dialect == DatabaseDialect.Postgresql) + { + await CreateTablesSamplePostgreSql.CreateTables(Builder.ConnectionString); + } + + var output = Writer.ToString(); + Assert.That(output, Does.Contain("Created Singers & Albums tables")); + } + + [Test, Order(2)] + public async Task TestCreateConnection() + { + if (_dialect == DatabaseDialect.GoogleStandardSql) + { + await CreateConnectionSample.CreateConnection(Builder.ConnectionString); + } + else if (_dialect == DatabaseDialect.Postgresql) + { + await CreateConnectionSamplePostgreSql.CreateConnection(Builder.ConnectionString); + } + + var output = Writer.ToString(); + Assert.That(output, Does.Contain("Greeting from Spanner: Hello World")); + } + + [Test, Order(3)] + public async Task TestWriteDataWithDml() + { + if (_dialect == DatabaseDialect.GoogleStandardSql) + { + await WriteDataWithDmlSample.WriteDataWithDml(Builder.ConnectionString); + } + else if (_dialect == DatabaseDialect.Postgresql) + { + await WriteDataWithDmlSamplePostgreSql.WriteDataWithDml(Builder.ConnectionString); + } + + var output = Writer.ToString(); + Assert.That(output, Does.Contain("4 record(s) inserted.")); + } + + [Test, Order(4)] + public async Task TestWriteDataWithMutations() + { + if (_dialect == DatabaseDialect.GoogleStandardSql) + { + await WriteDataWithMutationsSample.WriteDataWithMutations(Builder.ConnectionString); + } + else if (_dialect == DatabaseDialect.Postgresql) + { + await WriteDataWithMutationsSamplePostgreSql.WriteDataWithMutations(Builder.ConnectionString); + } + + var output = Writer.ToString(); + Assert.That(output, Does.Contain("Inserted 10 rows.")); + } + + [Test, Order(5)] + public async Task TestQueryData() + { + if (_dialect == DatabaseDialect.GoogleStandardSql) + { + await QueryDataSample.QueryData(Builder.ConnectionString); + } + else if (_dialect == DatabaseDialect.Postgresql) + { + await QueryDataSamplePostgreSql.QueryData(Builder.ConnectionString); + } + + var output = Writer.ToString(); + Assert.That(output, Is.EqualTo( + $"1 1 Total Junk{Environment.NewLine}" + + $"1 2 Go, Go, Go{Environment.NewLine}" + + $"2 1 Green{Environment.NewLine}" + + $"2 2 Forever Hold Your Peace{Environment.NewLine}" + + $"2 3 Terrified{Environment.NewLine}")); + } + + [Test, Order(6)] + public async Task TestQueryDataWithParameter() + { + if (_dialect == DatabaseDialect.GoogleStandardSql) + { + await QueryDataWithParameterSample.QueryDataWithParameter(Builder.ConnectionString); + } + else if (_dialect == DatabaseDialect.Postgresql) + { + await QueryDataWithParameterSamplePostgreSql.QueryDataWithParameter(Builder.ConnectionString); + } + + var output = Writer.ToString(); + Assert.That(output, Is.EqualTo($"12 Melissa Garcia{Environment.NewLine}")); + } + + [Test, Order(7)] + public async Task TestAddColumn() + { + if (_dialect == DatabaseDialect.GoogleStandardSql) + { + await AddColumnSample.AddColumn(Builder.ConnectionString); + } + else if (_dialect == DatabaseDialect.Postgresql) + { + await AddColumnSamplePostgreSql.AddColumn(Builder.ConnectionString); + } + + var output = Writer.ToString(); + if (_dialect == DatabaseDialect.GoogleStandardSql) + { + Assert.That(output, Is.EqualTo($"Added MarketingBudget column{Environment.NewLine}")); + } + else if (_dialect == DatabaseDialect.Postgresql) + { + Assert.That(output, Is.EqualTo($"Added marketing_budget column{Environment.NewLine}")); + } + } + + [Test, Order(8)] + public async Task TestDdlBatch() + { + if (_dialect == DatabaseDialect.GoogleStandardSql) + { + await DdlBatchSample.DdlBatch(Builder.ConnectionString); + } + else if (_dialect == DatabaseDialect.Postgresql) + { + await DdlBatchSamplePostgreSql.DdlBatch(Builder.ConnectionString); + } + + var output = Writer.ToString(); + Assert.That(output, Is.EqualTo($"Added Venues and Concerts tables{Environment.NewLine}")); + } + + [Test, Order(9)] + public async Task TestUpdateDataWithMutations() + { + if (_dialect == DatabaseDialect.GoogleStandardSql) + { + await UpdateDataWithMutationsSample.UpdateDataWithMutations(Builder.ConnectionString); + } + else if (_dialect == DatabaseDialect.Postgresql) + { + await UpdateDataWithMutationsSamplePostgreSql.UpdateDataWithMutations(Builder.ConnectionString); + } + + var output = Writer.ToString(); + Assert.That(output, Is.EqualTo($"Updated 2 albums.{Environment.NewLine}")); + } + + [Test, Order(10)] + public async Task TestQueryNewColumn() + { + if (_dialect == DatabaseDialect.GoogleStandardSql) + { + await QueryNewColumnSample.QueryNewColumn(Builder.ConnectionString); + } + else if (_dialect == DatabaseDialect.Postgresql) + { + await QueryNewColumnSamplePostgreSql.QueryNewColumn(Builder.ConnectionString); + } + + var output = Writer.ToString(); + Assert.That(output, Is.EqualTo( + $"1 1 100000{Environment.NewLine}" + + $"1 2 {Environment.NewLine}" + + $"2 1 {Environment.NewLine}" + + $"2 2 500000{Environment.NewLine}" + + $"2 3 {Environment.NewLine}")); + } + + [Test, Order(11)] + public async Task TestWriteDataWithTransaction() + { + if (_dialect == DatabaseDialect.GoogleStandardSql) + { + await WriteDataWithTransactionSample.WriteDataWithTransaction(Builder.ConnectionString); + } + else if (_dialect == DatabaseDialect.Postgresql) + { + await WriteDataWithTransactionSamplePostgreSql.WriteDataWithTransaction(Builder.ConnectionString); + } + + var output = Writer.ToString(); + Assert.That(output, Is.EqualTo($"Transferred marketing budget from Album 2 to Album 1{Environment.NewLine}")); + } + + [Test, Order(12)] + public async Task TestTags() + { + if (_dialect == DatabaseDialect.GoogleStandardSql) + { + await TagsSample.Tags(Builder.ConnectionString); + } + else if (_dialect == DatabaseDialect.Postgresql) + { + await TagsSamplePostgreSql.Tags(Builder.ConnectionString); + } + + var output = Writer.ToString(); + Assert.That(output, Is.EqualTo($"Reduced marketing budget{Environment.NewLine}")); + } + + [Test, Order(13)] + public async Task TestReadOnlyTransaction() + { + if (_dialect == DatabaseDialect.GoogleStandardSql) + { + await ReadOnlyTransactionSample.ReadOnlyTransaction(Builder.ConnectionString); + } + else if (_dialect == DatabaseDialect.Postgresql) + { + await ReadOnlyTransactionSamplePostgreSql.ReadOnlyTransaction(Builder.ConnectionString); + } + + var output = Writer.ToString(); + Assert.That(output, Is.EqualTo( + $"1 1 Total Junk{Environment.NewLine}" + + $"1 2 Go, Go, Go{Environment.NewLine}" + + $"2 1 Green{Environment.NewLine}" + + $"2 2 Forever Hold Your Peace{Environment.NewLine}" + + $"2 3 Terrified{Environment.NewLine}" + + $"2 2 Forever Hold Your Peace{Environment.NewLine}" + + $"1 2 Go, Go, Go{Environment.NewLine}" + + $"2 1 Green{Environment.NewLine}" + + $"2 3 Terrified{Environment.NewLine}" + + $"1 1 Total Junk{Environment.NewLine}")); + } + + [Test, Order(14)] + public async Task TestDataBoost() + { + if (_dialect == DatabaseDialect.GoogleStandardSql) + { + await DataBoostSample.DataBoost(Builder.ConnectionString); + } + else if (_dialect == DatabaseDialect.Postgresql) + { + await DataBoostSamplePostgreSql.DataBoost(Builder.ConnectionString); + } + + // TODO: Implement + var output = Writer.ToString(); + Assert.That(output, Is.EqualTo("")); + } + + [Test, Order(15)] + public async Task TestPartitionedDml() + { + if (_dialect == DatabaseDialect.GoogleStandardSql) + { + await PartitionedDmlSample.PartitionedDml(Builder.ConnectionString); + } + else if (_dialect == DatabaseDialect.Postgresql) + { + await PartitionedDmlSamplePostgreSql.PartitionedDml(Builder.ConnectionString); + } + + var output = Writer.ToString(); + Assert.That(output, Is.EqualTo($"Updated at least 3 albums{Environment.NewLine}")); + } + +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples-tests/RunSamplesTests.cs b/drivers/spanner-ado-net/spanner-ado-net-samples-tests/RunSamplesTests.cs new file mode 100644 index 00000000..c23fa032 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples-tests/RunSamplesTests.cs @@ -0,0 +1,230 @@ +// Copyright 2025 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 +// +// https://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. + +using Docker.DotNet; + +namespace Google.Cloud.Spanner.DataProvider.Samples.Tests; + +public class RunSamplesTests +{ + private StringWriter? _writer; + private TextWriter _output; + + private StringWriter Writer => _writer ??= new StringWriter(); + + [OneTimeSetUp] + public static async Task CheckEmulatorAvailability() + { + if (EmulatorRunner.IsEmulatorRunning()) + { + return; + } + using var client = EmulatorRunner.CreateDockerClient(); + try + { + client.System.GetVersionAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + } + catch (Exception) + { + Assert.Ignore("Docker and Emulator not available"); + } + var emulatorRunner = new EmulatorRunner(); + try + { + await emulatorRunner.StartEmulator(); + await emulatorRunner.StopEmulator(); + } + catch (DockerImageNotFoundException) + { + Assert.Ignore("Docker and Emulator not available"); + } + } + + [SetUp] + public void SetupOutput() + { + _output = Console.Out; + _writer = new StringWriter(); + Console.SetOut(_writer); + } + + [TearDown] + public void CleanupOutput() + { + _writer?.Dispose(); + Console.SetOut(_output); + } + + [Test] + public void TestHelloWorldSample() + { + SampleRunner.RunSample("HelloWorld", false); + + var output = Writer.ToString(); + Assert.That(output, Does.Contain("Running sample HelloWorld")); + Assert.That(output, Does.Contain("Greeting from Spanner: Hello World")); + } + + [Test] + public void TestEmulatorSample() + { + SampleRunner.RunSample("Emulator", false); + + var output = Writer.ToString(); + Assert.That(output, Does.Contain("Running sample Emulator")); + Assert.That(output, Does.Contain("Greeting from Spanner Emulator: Hello World")); + } + + [Test] + public void TestCommitTimestampSample() + { + SampleRunner.RunSample("CommitTimestamp", false); + + var output = Writer.ToString(); + Assert.That(output, Does.Contain("Running sample CommitTimestamp")); + Assert.That(output, Does.Contain("Inserted 1 singer(s)")); + Assert.That(output, Does.Contain("Transaction committed at ")); + } + + [Test] + public void TestCustomConfigurationSample() + { + SampleRunner.RunSample("CustomConfiguration", false); + + var output = Writer.ToString(); + Assert.That(output, Does.Contain("Running sample CustomConfiguration")); + Assert.That(output, Does.Contain("Greeting: Hello from Spanner")); + } + + [Test] + public void TestDataTypesSample() + { + SampleRunner.RunSample("DataTypes", false); + + var output = Writer.ToString(); + Assert.That(output, Does.Contain("Running sample DataTypes")); + Assert.That(output, Does.Contain("Inserted: 1")); + Assert.That(output, Does.Contain("id: 1")); + Assert.That(output, Does.Contain("col_bool: True")); + Assert.That(output, Does.Contain("col_bytes: System.Byte[]")); + Assert.That(output, Does.Contain($"col_date: {DateOnly.FromDateTime(DateTime.Now)}")); + Assert.That(output, Does.Contain($"col_float32: {3.14f}")); + Assert.That(output, Does.Contain($"col_float64: {3.14d}")); + Assert.That(output, Does.Contain("col_int64: 100")); + Assert.That(output, Does.Contain("col_json: {\"key\":\"value\"}")); + Assert.That(output, Does.Contain($"col_numeric: {3.14m}")); + Assert.That(output, Does.Contain("col_string: test-string")); + Assert.That(output, Does.Contain("col_timestamp: ")); + } + + [Test] + public void TestDdlBatchSample() + { + SampleRunner.RunSample("DdlBatch", false); + + var output = Writer.ToString(); + Assert.That(output, Does.Contain("Running sample DdlBatch")); + Assert.That(output, Does.Contain("Executed a single SQL string with multiple DDL statements as one batch.")); + Assert.That(output, Does.Contain("Executed ADO.NET batch")); + Assert.That(output, Does.Contain("Executed DDL batch")); + } + + [Test] + public void TestDmlBatchSample() + { + SampleRunner.RunSample("DmlBatch", false); + + var output = Writer.ToString(); + Assert.That(output, Does.Contain("Running sample DmlBatch")); + Assert.That(output, Does.Contain($"Executed ADO.NET batch{Environment.NewLine}Affected: 3{Environment.NewLine}")); + Assert.That(output, Does.Contain($"Executed DML batch{Environment.NewLine}Affected: -1{Environment.NewLine}")); + } + + [Test] + public void TestMutationsSample() + { + SampleRunner.RunSample("Mutations", false); + + var output = Writer.ToString(); + Assert.That(output, Does.Contain("Running sample Mutations")); + Assert.That(output, Does.Contain("Inserted data using mutations. Affected: 1")); + } + + [Test] + public void TestPartitionedDmlSample() + { + SampleRunner.RunSample("PartitionedDml", false); + + var output = Writer.ToString(); + Assert.That(output, Does.Contain("Running sample PartitionedDml")); + Assert.That(output, Does.Contain("Executed a Partitioned DML statement. Affected: ")); + } + + [Test] + public void TestQueryParametersSample() + { + SampleRunner.RunSample("QueryParameters", false); + + var output = Writer.ToString(); + Assert.That(output, Does.Contain("Running sample QueryParameters")); + Assert.That(output, Does.Contain("Found singer with named parameters: Mark Richards")); + Assert.That(output, Does.Contain("Found singer with named parameters: Alice Trentor")); + Assert.That(output, Does.Contain("Found singer with positional parameters: Mark Richards")); + Assert.That(output, Does.Contain("Found singer with positional parameters: Alice Trentor")); + } + + [Test] + public void TestReadOnlyTransactionSample() + { + SampleRunner.RunSample("ReadOnlyTransaction", false); + + var output = Writer.ToString(); + Assert.That(output, Does.Contain("Running sample ReadOnlyTransaction")); + Assert.That(output, Does.Contain("Found singer: Mark Richards")); + Assert.That(output, Does.Contain("Found singer: Alice Trentor")); + } + + [Test] + public void TestStaleReadSample() + { + SampleRunner.RunSample("StaleRead", false); + + var output = Writer.ToString(); + Assert.That(output, Does.Contain("Running sample StaleRead")); + Assert.That(output, Does.Contain("Found singer using a single stale query: Mark Richards")); + Assert.That(output, Does.Contain("Found singer using a single stale query: Alice Trentor")); + Assert.That(output, Does.Contain("Found singer using a stale read-only transaction: Mark Richards")); + Assert.That(output, Does.Contain("Found singer using a stale read-only transaction: Alice Trentor")); + } + + [Test] + public void TestTagsSample() + { + SampleRunner.RunSample("Tags", false); + + var output = Writer.ToString(); + Assert.That(output, Does.Contain("Running sample Tags")); + Assert.That(output, Does.Contain("Greeting from Spanner: Hello World")); + } + + [Test] + public void TestTransactionSample() + { + SampleRunner.RunSample("Transaction", false); + + var output = Writer.ToString(); + Assert.That(output, Does.Contain("Running sample Transaction")); + Assert.That(output, Does.Contain("Set a default birthdate for 1 singers")); + } +} \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples-tests/SamplesMockServerTests.cs b/drivers/spanner-ado-net/spanner-ado-net-samples-tests/SamplesMockServerTests.cs index f970c8f3..05a3a642 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples-tests/SamplesMockServerTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples-tests/SamplesMockServerTests.cs @@ -43,11 +43,12 @@ public void CleanupOutput() } [Test] - public async Task TestHelloWorld() + public async Task TestHelloWorldSample() { const string sql = "SELECT 'Hello World' as Message"; Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet([Tuple.Create(TypeCode.String, "Message")], [["Hello World"]])); + await HelloWorldSample.Run(ConnectionString); Assert.That(Writer.ToString(), Is.EqualTo($"Greeting from Spanner: Hello World{Environment.NewLine}")); @@ -231,13 +232,158 @@ public async Task TestPartitionedDmlSample() await PartitionedDmlSample.Run(ConnectionString); Assert.That(Writer.ToString(), Is.EqualTo($"Executed a Partitioned DML statement. Affected: 100{Environment.NewLine}{Environment.NewLine}")); - // var beginRequests = Fixture.SpannerMock.Requests.OfType().ToList(); - // Assert.That(beginRequests, Has.Count.EqualTo(1)); - // var beginRequest = beginRequests[0]; - // Assert.That(beginRequest.Options?.PartitionedDml, Is.Not.Null); + var beginRequests = Fixture.SpannerMock.Requests.OfType().ToList(); + Assert.That(beginRequests, Has.Count.EqualTo(1)); + var beginRequest = beginRequests[0]; + Assert.That(beginRequest.Options?.PartitionedDml, Is.Not.Null); var executeRequests = Fixture.SpannerMock.Requests.OfType().ToList(); Assert.That(executeRequests, Has.Count.EqualTo(1)); var executeRequest = executeRequests[0]; - Assert.That(executeRequest.Transaction?.Begin?.PartitionedDml, Is.Not.Null); + Assert.That(executeRequest.Transaction?.Id, Is.Not.Null); + Assert.That(Fixture.SpannerMock.Requests.OfType(), Is.Empty); + } + + [Test] + public async Task TestQueryParametersSample() + { + const string namedParameters = + "SELECT SingerId, FullName FROM Singers WHERE LastName LIKE @lastName OR FirstName LIKE @firstName ORDER BY LastName, FirstName"; + const string positionalParameters = + "SELECT SingerId, FullName FROM Singers WHERE LastName LIKE @p1 OR FirstName LIKE @p2 ORDER BY LastName, FirstName"; + var result = StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "SingerId"), Tuple.Create(TypeCode.String, "FullName")], + [[1L, "Pete Allison"], [2L, "Alice Peterson"]]); + Fixture.SpannerMock.AddOrUpdateStatementResult(namedParameters, result); + Fixture.SpannerMock.AddOrUpdateStatementResult(positionalParameters, result); + + await QueryParametersSample.Run(ConnectionString); + + Assert.That(Writer.ToString(), Is.EqualTo( + $"Found singer with named parameters: Pete Allison{Environment.NewLine}" + + $"Found singer with named parameters: Alice Peterson{Environment.NewLine}" + + $"Found singer with positional parameters: Pete Allison{Environment.NewLine}" + + $"Found singer with positional parameters: Alice Peterson{Environment.NewLine}")); + + var requests = Fixture.SpannerMock.Requests.OfType().ToList(); + Assert.That(requests, Has.Count.EqualTo(2)); + var index = 0; + foreach (var request in requests) + { + Assert.That(request.Transaction?.SingleUse?.ReadOnly, Is.Not.Null); + Assert.That(request.ParamTypes, Is.Empty); + Assert.That(request.Params.Fields, Has.Count.EqualTo(2)); + if (index == 0) + { + Assert.That(request.Params.Fields["firstName"].StringValue, Is.EqualTo("A%")); + Assert.That(request.Params.Fields["lastName"].StringValue, Is.EqualTo("R%")); + } + else + { + Assert.That(request.Params.Fields["p1"].StringValue, Is.EqualTo("R%")); + Assert.That(request.Params.Fields["p2"].StringValue, Is.EqualTo("A%")); + } + index++; + } + } + + [Test] + public async Task TestReadOnlyTransactionSample() + { + const string sql = + "SELECT SingerId, FullName FROM Singers WHERE LastName LIKE @lastName OR FirstName LIKE @firstName ORDER BY LastName, FirstName"; + var result = StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "SingerId"), Tuple.Create(TypeCode.String, "FullName")], + [[1L, "Pete Allison"], [2L, "Alice Peterson"]]); + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, result); + + await ReadOnlyTransactionSample.Run(ConnectionString); + + Assert.That(Writer.ToString(), Is.EqualTo( + $"Found singer: Pete Allison{Environment.NewLine}" + + $"Found singer: Alice Peterson{Environment.NewLine}")); + + Assert.That(Fixture.SpannerMock.Requests.OfType(), Is.Empty); + Assert.That(Fixture.SpannerMock.Requests.OfType(), Is.Empty); + var requests = Fixture.SpannerMock.Requests.OfType().ToList(); + Assert.That(requests, Has.Count.EqualTo(1)); + var request = requests[0]; + Assert.That(request.Transaction?.Begin?.ReadOnly, Is.Not.Null); + } + + [Test] + public async Task TestStaleReadSample() + { + var now = DateTime.UtcNow; + const string sql = "SELECT SingerId, FullName FROM Singers ORDER BY LastName, FirstName"; + var result = StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Int64, "SingerId"), Tuple.Create(TypeCode.String, "FullName")], + [[1L, "Pete Allison"], [2L, "Alice Peterson"]]); + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, result); + const string currentTimestamp = "SELECT CURRENT_TIMESTAMP"; + Fixture.SpannerMock.AddOrUpdateStatementResult(currentTimestamp, StatementResult.CreateResultSet( + [Tuple.Create(TypeCode.Timestamp, "CURRENT_TIMESTAMP")], + [[now]])); + + await StaleReadSample.Run(ConnectionString); + + Assert.That(Writer.ToString(), Is.EqualTo( + $"Found singer using a single stale query: Pete Allison{Environment.NewLine}" + + $"Found singer using a single stale query: Alice Peterson{Environment.NewLine}" + + $"Found singer using a stale read-only transaction: Pete Allison{Environment.NewLine}" + + $"Found singer using a stale read-only transaction: Alice Peterson{Environment.NewLine}")); + + Assert.That(Fixture.SpannerMock.Requests.OfType(), Is.Empty); + Assert.That(Fixture.SpannerMock.Requests.OfType(), Is.Empty); + var requests = Fixture.SpannerMock.Requests.OfType().ToList(); + Assert.That(requests, Has.Count.EqualTo(3)); + Assert.That(requests[0].Transaction?.SingleUse?.ReadOnly?.MaxStaleness?.Seconds ?? -1, Is.EqualTo(10)); + Assert.That(requests[1].Transaction?.SingleUse?.ReadOnly, Is.Not.Null); + Assert.That(requests[1].Transaction?.SingleUse?.ReadOnly?.MaxStaleness, Is.Null); + Assert.That(requests[2].Transaction?.Begin?.ReadOnly?.ReadTimestamp, Is.Not.Null); + + var actual = + TimeSpan.FromTicks(TimeSpan.TicksPerSecond * requests[2].Transaction.Begin.ReadOnly.ReadTimestamp.Seconds + + requests[2].Transaction.Begin.ReadOnly.ReadTimestamp.Nanos / + TimeSpan.NanosecondsPerTick); + var expected = TimeSpan.FromTicks(now.Ticks - DateTime.UnixEpoch.Ticks); + Assert.That(actual, Is.EqualTo(expected)); + } + + [Test] + public async Task TestTagsSample() + { + const string sql = "SELECT 'Hello World' as Message"; + Fixture.SpannerMock.AddOrUpdateStatementResult(sql, + StatementResult.CreateResultSet([Tuple.Create(TypeCode.String, "Message")], [["Hello World"]])); + + await TagsSample.Run(ConnectionString); + + Assert.That(Fixture.SpannerMock.Requests.OfType(), Is.Empty); + var executeRequest = Fixture.SpannerMock.Requests.OfType().Single(); + Assert.That(executeRequest.Transaction?.Begin?.ReadWrite, Is.Not.Null); + Assert.That(executeRequest.RequestOptions?.TransactionTag ?? "", Is.EqualTo("my_transaction_tag")); + Assert.That(executeRequest.RequestOptions?.RequestTag ?? "", Is.EqualTo("my_query_tag")); + var commitRequest = Fixture.SpannerMock.Requests.OfType().Single(); + Assert.That(commitRequest.RequestOptions.TransactionTag ?? "", Is.EqualTo("my_transaction_tag")); + } + + [Test] + public async Task TestTransactionSample() + { + const string query = "SELECT SingerId FROM Singers WHERE BirthDate IS NULL"; + Fixture.SpannerMock.AddOrUpdateStatementResult(query, + StatementResult.CreateResultSet([Tuple.Create(TypeCode.Int64, "SingerId")], [[1L]])); + const string update = "UPDATE Singers SET BirthDate=DATE '1900-01-01' WHERE SingerId=@singerId"; + Fixture.SpannerMock.AddOrUpdateStatementResult(update, StatementResult.CreateUpdateCount(1L)); + + await TransactionSample.Run(ConnectionString); + + Assert.That(Writer.ToString(), Is.EqualTo($"Set a default birthdate for 1 singers{Environment.NewLine}")); + Assert.That(Fixture.SpannerMock.Requests.OfType(), Is.Empty); + Assert.That(Fixture.SpannerMock.Requests.OfType().ToList(), Has.Count.EqualTo(1)); + var requests = Fixture.SpannerMock.Requests.OfType().ToList(); + Assert.That(requests, Has.Count.EqualTo(2)); + Assert.That(requests[0].Transaction?.Begin?.ReadWrite, Is.Not.Null); + Assert.That(requests[1].Transaction?.Id, Is.Not.Null); } } \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/AssemblyInfo.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/AssemblyInfo.cs new file mode 100644 index 00000000..2765294c --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/AssemblyInfo.cs @@ -0,0 +1,2 @@ +using System.Runtime.CompilerServices; +[assembly:InternalsVisibleTo("Google.Cloud.Spanner.DataProvider.Samples.Tests")] diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/EmulatorRunner.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/EmulatorRunner.cs index 88280d7b..6fe8d7d5 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/EmulatorRunner.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/EmulatorRunner.cs @@ -33,12 +33,17 @@ internal class EmulatorRunner private readonly DockerClient _dockerClient; private string? _containerId; + internal static DockerClient CreateDockerClient() + { + return new DockerClientConfiguration(new Uri(GetDockerApiUri())).CreateClient(); + } + internal EmulatorRunner() { - _dockerClient = new DockerClientConfiguration(new Uri(GetDockerApiUri())).CreateClient(); + _dockerClient = CreateDockerClient(); } - internal bool IsEmulatorRunning() + internal static bool IsEmulatorRunning() { using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try @@ -104,12 +109,10 @@ await _dockerClient.Images.CreateImageAsync(new ImagesCreateParameters { FromImage = SEmulatorImageName, Tag = "latest" - }, - new AuthConfig(), - new Progress()); + }, new AuthConfig(), new Progress()); } - private string GetDockerApiUri() + private static string GetDockerApiUri() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/AddColumnSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/AddColumnSample.cs new file mode 100644 index 00000000..a595b02b --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/AddColumnSample.cs @@ -0,0 +1,32 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class AddColumnSample +{ + // [START spanner_add_column] + public static async Task AddColumn(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + await using var command = connection.CreateCommand(); + command.CommandText = "ALTER TABLE Albums ADD COLUMN MarketingBudget INT64"; + await command.ExecuteNonQueryAsync(); + + Console.WriteLine("Added MarketingBudget column"); + } + // [END spanner_add_column] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/AddColumnSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/AddColumnSamplePostgreSql.cs new file mode 100644 index 00000000..d37a3689 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/AddColumnSamplePostgreSql.cs @@ -0,0 +1,32 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class AddColumnSamplePostgreSql +{ + // [START spanner_add_column] + public static async Task AddColumn(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + await using var command = connection.CreateCommand(); + command.CommandText = "alter table albums add column marketing_budget bigint"; + await command.ExecuteNonQueryAsync(); + + Console.WriteLine("Added marketing_budget column"); + } + // [END spanner_add_column] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateConnectionSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateConnectionSample.cs new file mode 100644 index 00000000..37093c51 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateConnectionSample.cs @@ -0,0 +1,50 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Data; + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class CreateConnectionSample +{ + // [START spanner_create_connection] + public static async Task CreateConnection(string connectionString) + { + // Use a SpannerConnectionStringBuilder to construct a connection string. + // The SpannerConnectionStringBuilder contains properties for the most + // used connection string variables. + var builder = new SpannerConnectionStringBuilder(connectionString) + { + // Sets the default isolation level that should be used for all + // read/write transactions on this connection. + DefaultIsolationLevel = IsolationLevel.RepeatableRead, + + // The Options property can be used to set any connection property + // as a key-value pair. + Options = "statement_cache_size=2000" + }; + + await using var connection = new SpannerConnection(builder.ConnectionString); + await connection.OpenAsync(); + + await using var command = connection.CreateCommand(); + command.CommandText = "SELECT 'Hello World' as Message"; + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + Console.WriteLine($"Greeting from Spanner: {reader.GetString(0)}"); + } + } + // [END spanner_create_connection] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateConnectionSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateConnectionSamplePostgreSql.cs new file mode 100644 index 00000000..a3db3a8a --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateConnectionSamplePostgreSql.cs @@ -0,0 +1,50 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Data; + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class CreateConnectionSamplePostgreSql +{ + // [START spanner_create_connection] + public static async Task CreateConnection(string connectionString) + { + // Use a SpannerConnectionStringBuilder to construct a connection string. + // The SpannerConnectionStringBuilder contains properties for the most + // used connection string variables. + var builder = new SpannerConnectionStringBuilder(connectionString) + { + // Sets the default isolation level that should be used for all + // read/write transactions on this connection. + DefaultIsolationLevel = IsolationLevel.RepeatableRead, + + // The Options property can be used to set any connection property + // as a key-value pair. + Options = "statement_cache_size=2000" + }; + + await using var connection = new SpannerConnection(builder.ConnectionString); + await connection.OpenAsync(); + + await using var command = connection.CreateCommand(); + command.CommandText = "SELECT 'Hello World' as Message"; + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + Console.WriteLine($"Greeting from Spanner: {reader.GetString(0)}"); + } + } + // [END spanner_create_connection] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateTablesSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateTablesSample.cs new file mode 100644 index 00000000..d1414759 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateTablesSample.cs @@ -0,0 +1,43 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class CreateTablesSample +{ + // [START spanner_create_database] + public static async Task CreateTables(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // Create two tables in one batch on Spanner. + var batch = connection.CreateBatch(); + batch.BatchCommands.Add("CREATE TABLE Singers (" + + " SingerId INT64 NOT NULL, " + + " FirstName STRING(1024), " + + " LastName STRING(1024), " + + " SingerInfo BYTES(MAX) " + + ") PRIMARY KEY (SingerId)"); + batch.BatchCommands.Add("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"); + await batch.ExecuteNonQueryAsync(); + Console.WriteLine("Created Singers & Albums tables"); + } + // [END spanner_create_database] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateTablesSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateTablesSamplePostgreSql.cs new file mode 100644 index 00000000..6dc1a91d --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateTablesSamplePostgreSql.cs @@ -0,0 +1,43 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class CreateTablesSamplePostgreSql +{ + // [START spanner_create_database] + public static async Task CreateTables(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // Create two tables in one batch on Spanner. + var batch = connection.CreateBatch(); + batch.BatchCommands.Add("create table singers (" + + " singer_id bigint not null primary key, " + + " first_name varchar(1024), " + + " last_name varchar(1024), " + + " singer_info bytea" + + ")"); + batch.BatchCommands.Add("create table albums (" + + " singer_id bigint not null, " + + " album_id bigint not null, " + + " album_title varchar, " + + " primary key (singer_id, album_id)" + + ") interleave in parent singers on delete cascade"); + await batch.ExecuteNonQueryAsync(); + Console.WriteLine("Created Singers & Albums tables"); + } + // [END spanner_create_database] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DataBoostSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DataBoostSample.cs new file mode 100644 index 00000000..6b24da7d --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DataBoostSample.cs @@ -0,0 +1,28 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class DataBoostSample +{ + // [START spanner_data_boost] + public static async Task DataBoost(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // TODO: Implement support for Partitioned Queries and DataBoost. + } + // [END spanner_data_boost] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DataBoostSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DataBoostSamplePostgreSql.cs new file mode 100644 index 00000000..ad8f7640 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DataBoostSamplePostgreSql.cs @@ -0,0 +1,28 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class DataBoostSamplePostgreSql +{ + // [START spanner_data_boost] + public static async Task DataBoost(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // TODO: Implement support for Partitioned Queries and DataBoost. + } + // [END spanner_data_boost] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DdlBatchSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DdlBatchSample.cs new file mode 100644 index 00000000..0d38bd75 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DdlBatchSample.cs @@ -0,0 +1,51 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class DdlBatchSample +{ + // [START spanner_ddl_batch] + public static async Task DdlBatch(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // Executing multiple DDL statements as one batch is + // more efficient than executing each statement individually. + var batch = connection.CreateBatch(); + batch.BatchCommands.Add( + "CREATE TABLE Venues (" + + " VenueId INT64 NOT NULL, " + + " Name STRING(1024), " + + " Description JSON, " + + ") PRIMARY KEY (VenueId)"); + batch.BatchCommands.Add( + "CREATE TABLE Concerts (" + + " ConcertId INT64 NOT NULL, " + + " VenueId INT64 NOT NULL, " + + " SingerId INT64 NOT NULL, " + + " StartTime TIMESTAMP, " + + " EndTime TIMESTAMP, " + + " CONSTRAINT Fk_Concerts_Venues " + + " FOREIGN KEY (VenueId) REFERENCES Venues (VenueId), " + + " CONSTRAINT Fk_Concerts_Singers " + + " FOREIGN KEY (SingerId) REFERENCES Singers (SingerId), " + + ") PRIMARY KEY (ConcertId)"); + await batch.ExecuteNonQueryAsync(); + + Console.WriteLine("Added Venues and Concerts tables"); + } + // [END spanner_ddl_batch] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DdlBatchSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DdlBatchSamplePostgreSql.cs new file mode 100644 index 00000000..cb94d2c8 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DdlBatchSamplePostgreSql.cs @@ -0,0 +1,51 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class DdlBatchSamplePostgreSql +{ + // [START spanner_ddl_batch] + public static async Task DdlBatch(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // Executing multiple DDL statements as one batch is + // more efficient than executing each statement individually. + var batch = connection.CreateBatch(); + batch.BatchCommands.Add( + "create table venues (" + + " venue_id bigint not null primary key, " + + " name varchar(1024), " + + " description jsonb" + + ")"); + batch.BatchCommands.Add( + "create table concerts (" + + " concert_id bigint not null primary key, " + + " venue_id bigint not null, " + + " singer_id bigint not null, " + + " start_time timestamptz, " + + " end_time timestamptz, " + + " constraint fk_concerts_venues foreign key " + + " (venue_id) references venues (venue_id), " + + " constraint fk_concerts_singers foreign key " + + " (singer_id) references singers (singer_id)" + + ")"); + await batch.ExecuteNonQueryAsync(); + + Console.WriteLine("Added Venues and Concerts tables"); + } + // [END spanner_ddl_batch] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/PartitionedDmlSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/PartitionedDmlSample.cs new file mode 100644 index 00000000..81660b77 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/PartitionedDmlSample.cs @@ -0,0 +1,42 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class PartitionedDmlSample +{ + // [START spanner_partitioned_dml] + public static async Task PartitionedDml(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // Enable Partitioned DML on this connection. + await using var command = connection.CreateCommand(); + command.CommandText = "SET AUTOCOMMIT_DML_MODE='PARTITIONED_NON_ATOMIC'"; + await command.ExecuteNonQueryAsync(); + + // Back-fill a default value for the MarketingBudget column. + command.CommandText = "UPDATE Albums SET MarketingBudget=0 WHERE MarketingBudget IS NULL"; + var affected = await command.ExecuteNonQueryAsync(); + + // Partitioned DML returns the minimum number of records that were affected. + Console.WriteLine($"Updated at least {affected} albums"); + + // Reset the value for AUTOCOMMIT_DML_MODE to its default. + command.CommandText = "RESET AUTOCOMMIT_DML_MODE"; + await command.ExecuteNonQueryAsync(); + } + // [END spanner_partitioned_dml] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/PartitionedDmlSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/PartitionedDmlSamplePostgreSql.cs new file mode 100644 index 00000000..5607dc9c --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/PartitionedDmlSamplePostgreSql.cs @@ -0,0 +1,42 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class PartitionedDmlSamplePostgreSql +{ + // [START spanner_partitioned_dml] + public static async Task PartitionedDml(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // Enable Partitioned DML on this connection. + await using var command = connection.CreateCommand(); + command.CommandText = "set autocommit_dml_mode='partitioned_non_atomic'"; + await command.ExecuteNonQueryAsync(); + + // Back-fill a default value for the MarketingBudget column. + command.CommandText = "update albums set marketing_budget=0 where marketing_budget is null"; + var affected = await command.ExecuteNonQueryAsync(); + + // Partitioned DML returns the minimum number of records that were affected. + Console.WriteLine($"Updated at least {affected} albums"); + + // Reset the value for autocommit_dml_mode to its default. + command.CommandText = "reset autocommit_dml_mode"; + await command.ExecuteNonQueryAsync(); + } + // [END spanner_partitioned_dml] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataSample.cs new file mode 100644 index 00000000..d81aa27f --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataSample.cs @@ -0,0 +1,36 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class QueryDataSample +{ + // [START spanner_query_data] + public static async Task QueryData(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + await using var command = connection.CreateCommand(); + command.CommandText = "SELECT SingerId, AlbumId, AlbumTitle " + + "FROM Albums " + + "ORDER BY SingerId, AlbumId"; + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + Console.WriteLine($"{reader["SingerId"]} {reader["AlbumId"]} {reader["AlbumTitle"]}"); + } + } + // [END spanner_query_data] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataSamplePostgreSql.cs new file mode 100644 index 00000000..306ae440 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataSamplePostgreSql.cs @@ -0,0 +1,36 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class QueryDataSamplePostgreSql +{ + // [START spanner_query_data] + public static async Task QueryData(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + await using var command = connection.CreateCommand(); + command.CommandText = "SELECT singer_id, album_id, album_title " + + "FROM albums " + + "ORDER BY singer_id, album_id"; + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + Console.WriteLine($"{reader["singer_id"]} {reader["album_id"]} {reader["album_title"]}"); + } + } + // [END spanner_query_data] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataWithParameterSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataWithParameterSample.cs new file mode 100644 index 00000000..56547c2b --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataWithParameterSample.cs @@ -0,0 +1,37 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class QueryDataWithParameterSample +{ + // [START spanner_query_with_parameter] + public static async Task QueryDataWithParameter(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + await using var command = connection.CreateCommand(); + command.CommandText = "SELECT SingerId, FirstName, LastName " + + "FROM Singers " + + "WHERE LastName = ?"; + command.Parameters.Add("Garcia"); + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + Console.WriteLine($"{reader["SingerId"]} {reader["FirstName"]} {reader["LastName"]}"); + } + } + // [END spanner_query_with_parameter] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataWithParameterSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataWithParameterSamplePostgreSql.cs new file mode 100644 index 00000000..ad6993e1 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataWithParameterSamplePostgreSql.cs @@ -0,0 +1,37 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class QueryDataWithParameterSamplePostgreSql +{ + // [START spanner_query_with_parameter] + public static async Task QueryDataWithParameter(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + await using var command = connection.CreateCommand(); + command.CommandText = "SELECT singer_id, first_name, last_name " + + "FROM singers " + + "WHERE last_name = ?"; + command.Parameters.Add("Garcia"); + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + Console.WriteLine($"{reader["singer_id"]} {reader["first_name"]} {reader["last_name"]}"); + } + } + // [END spanner_query_with_parameter] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryNewColumnSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryNewColumnSample.cs new file mode 100644 index 00000000..104a529a --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryNewColumnSample.cs @@ -0,0 +1,36 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class QueryNewColumnSample +{ + // [START spanner_query_data_with_new_column] + public static async Task QueryNewColumn(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + await using var command = connection.CreateCommand(); + command.CommandText = "SELECT SingerId, AlbumId, MarketingBudget " + + "FROM Albums " + + "ORDER BY SingerId, AlbumId"; + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + Console.WriteLine($"{reader["SingerId"]} {reader["AlbumId"]} {reader["MarketingBudget"]}"); + } + } + // [END spanner_query_data_with_new_column] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryNewColumnSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryNewColumnSamplePostgreSql.cs new file mode 100644 index 00000000..e81f655c --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryNewColumnSamplePostgreSql.cs @@ -0,0 +1,36 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class QueryNewColumnSamplePostgreSql +{ + // [START spanner_query_data_with_new_column] + public static async Task QueryNewColumn(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + await using var command = connection.CreateCommand(); + command.CommandText = "select singer_id, album_id, marketing_budget " + + "from albums " + + "order by singer_id, album_id"; + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + Console.WriteLine($"{reader["singer_id"]} {reader["album_id"]} {reader["marketing_budget"]}"); + } + } + // [END spanner_query_data_with_new_column] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/ReadOnlyTransactionSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/ReadOnlyTransactionSample.cs new file mode 100644 index 00000000..7fe71c9c --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/ReadOnlyTransactionSample.cs @@ -0,0 +1,59 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class ReadOnlyTransactionSample +{ + // [START spanner_read_only_transaction] + public static async Task ReadOnlyTransaction(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // Start a read-only transaction on this connection. + await using var transaction = await connection.BeginReadOnlyTransactionAsync(); + + await using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.CommandText = "SELECT SingerId, AlbumId, AlbumTitle " + + "FROM Albums " + + "ORDER BY SingerId, AlbumId"; + await using (var reader = await command.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + Console.WriteLine( + $"{reader["SingerId"]} {reader["AlbumId"]} {reader["AlbumTitle"]}"); + } + } + + // Execute another query using the same read-only transaction. + command.CommandText = "SELECT SingerId, AlbumId, AlbumTitle " + + "FROM Albums " + + "ORDER BY AlbumTitle"; + await using (var reader = await command.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + Console.WriteLine( + $"{reader["SingerId"]} {reader["AlbumId"]} {reader["AlbumTitle"]}"); + } + } + + // End the read-only transaction by calling Commit. + await transaction.CommitAsync(); + } + // [END spanner_read_only_transaction] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/ReadOnlyTransactionSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/ReadOnlyTransactionSamplePostgreSql.cs new file mode 100644 index 00000000..a6308d2b --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/ReadOnlyTransactionSamplePostgreSql.cs @@ -0,0 +1,59 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class ReadOnlyTransactionSamplePostgreSql +{ + // [START spanner_read_only_transaction] + public static async Task ReadOnlyTransaction(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // Start a read-only transaction on this connection. + await using var transaction = await connection.BeginReadOnlyTransactionAsync(); + + await using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.CommandText = "select singer_id, album_id, album_title " + + "from albums " + + "order by singer_id, album_id"; + await using (var reader = await command.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + Console.WriteLine( + $"{reader["singer_id"]} {reader["album_id"]} {reader["album_title"]}"); + } + } + + // Execute another query using the same read-only transaction. + command.CommandText = "select singer_id, album_id, album_title " + + "from albums " + + "order by album_title"; + await using (var reader = await command.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + Console.WriteLine( + $"{reader["singer_id"]} {reader["album_id"]} {reader["album_title"]}"); + } + } + + // End the read-only transaction by calling Commit. + await transaction.CommitAsync(); + } + // [END spanner_read_only_transaction] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/TagsSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/TagsSample.cs new file mode 100644 index 00000000..7d93f066 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/TagsSample.cs @@ -0,0 +1,62 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class TagsSample +{ + // [START spanner_transaction_and_statement_tag] + public static async Task Tags(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + const long singerId = 1L; + const long albumId = 1L; + + await using var transaction = await connection.BeginTransactionAsync(); + // Set a tag on the transaction before executing any statements. + transaction.Tag = "example-tx-tag"; + + await using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.Tag = "query-marketing-budget"; + command.CommandText = + "SELECT MarketingBudget " + + "FROM Albums " + + "WHERE SingerId=? and AlbumId=?"; + command.Parameters.Add(singerId); + command.Parameters.Add(albumId); + var budget = (long)(await command.ExecuteScalarAsync() ?? 0L); + + // Reduce the marketing budget by 10% if it is more than 1,000. + if (budget > 1000) + { + budget -= budget / 10; + await using var updateCommand = connection.CreateCommand(); + updateCommand.Transaction = transaction; + updateCommand.Tag = "reduce-marketing-budget"; + updateCommand.CommandText = + "UPDATE Albums SET MarketingBudget=@budget WHERE SingerId=@singerId AND AlbumId=@albumId"; + updateCommand.Parameters.AddWithValue("budget", budget); + updateCommand.Parameters.AddWithValue("singerId", singerId); + updateCommand.Parameters.AddWithValue("albumId", albumId); + await updateCommand.ExecuteNonQueryAsync(); + } + // Commit the transaction. + await transaction.CommitAsync(); + Console.WriteLine("Reduced marketing budget"); + } + // [END spanner_transaction_and_statement_tag] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/TagsSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/TagsSamplePostgreSql.cs new file mode 100644 index 00000000..086e4d00 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/TagsSamplePostgreSql.cs @@ -0,0 +1,62 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class TagsSamplePostgreSql +{ + // [START spanner_transaction_and_statement_tag] + public static async Task Tags(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + const long singerId = 1L; + const long albumId = 1L; + + await using var transaction = await connection.BeginTransactionAsync(); + // Set a tag on the transaction before executing any statements. + transaction.Tag = "example-tx-tag"; + + await using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.Tag = "query-marketing-budget"; + command.CommandText = + "select marketing_budget " + + "from albums " + + "where singer_id=? and album_id=?"; + command.Parameters.Add(singerId); + command.Parameters.Add(albumId); + var budget = (long)(await command.ExecuteScalarAsync() ?? 0L); + + // Reduce the marketing budget by 10% if it is more than 1,000. + if (budget > 1000) + { + budget -= budget / 10; + await using var updateCommand = connection.CreateCommand(); + updateCommand.Transaction = transaction; + updateCommand.Tag = "reduce-marketing-budget"; + updateCommand.CommandText = + "update albums set marketing_budget=$1 where singer_id=$2 and album_id=$3"; + updateCommand.Parameters.Add(budget); + updateCommand.Parameters.Add(singerId); + updateCommand.Parameters.Add(albumId); + await updateCommand.ExecuteNonQueryAsync(); + } + // Commit the transaction. + await transaction.CommitAsync(); + Console.WriteLine("Reduced marketing budget"); + } + // [END spanner_transaction_and_statement_tag] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/UpdateDataWithMutationsSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/UpdateDataWithMutationsSample.cs new file mode 100644 index 00000000..76a8f5cf --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/UpdateDataWithMutationsSample.cs @@ -0,0 +1,44 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class UpdateDataWithMutationsSample +{ + // [START spanner_update_data] + public static async Task UpdateDataWithMutations(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + (long SingerId, long AlbumId, long MarketingBudget)[] albums = [ + (1L, 1L, 100000L), + (2L, 2L, 500000L), + ]; + // Use a batch to update two rows in one round-trip. + var batch = connection.CreateBatch(); + foreach (var album in albums) + { + // This creates a command that will use a mutation to update the row. + var command = batch.CreateUpdateCommand("Albums"); + command.AddParameter("SingerId", album.SingerId); + command.AddParameter("AlbumId", album.AlbumId); + command.AddParameter("MarketingBudget", album.MarketingBudget); + batch.BatchCommands.Add(command); + } + var affected = await batch.ExecuteNonQueryAsync(); + Console.WriteLine($"Updated {affected} albums."); + } + // [END spanner_update_data] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/UpdateDataWithMutationsSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/UpdateDataWithMutationsSamplePostgreSql.cs new file mode 100644 index 00000000..cdeb7a18 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/UpdateDataWithMutationsSamplePostgreSql.cs @@ -0,0 +1,44 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class UpdateDataWithMutationsSamplePostgreSql +{ + // [START spanner_update_data] + public static async Task UpdateDataWithMutations(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + (long SingerId, long AlbumId, long MarketingBudget)[] albums = [ + (1L, 1L, 100000L), + (2L, 2L, 500000L), + ]; + // Use a batch to update two rows in one round-trip. + var batch = connection.CreateBatch(); + foreach (var album in albums) + { + // This creates a command that will use a mutation to update the row. + var command = batch.CreateUpdateCommand("albums"); + command.AddParameter("singer_id", album.SingerId); + command.AddParameter("album_id", album.AlbumId); + command.AddParameter("marketing_budget", album.MarketingBudget); + batch.BatchCommands.Add(command); + } + var affected = await batch.ExecuteNonQueryAsync(); + Console.WriteLine($"Updated {affected} albums."); + } + // [END spanner_update_data] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithDmlSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithDmlSample.cs new file mode 100644 index 00000000..0b0f4162 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithDmlSample.cs @@ -0,0 +1,51 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class WriteDataWithDmlSample +{ + // [START spanner_dml_getting_started_insert] + public static async Task WriteDataWithDml(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // Add 4 rows in one statement. + // The ADO.NET driver supports positional query parameters. + await using var command = connection.CreateCommand(); + command.CommandText = "INSERT INTO Singers (SingerId, FirstName, LastName) " + + "VALUES (?, ?, ?), (?, ?, ?), " + + " (?, ?, ?), (?, ?, ?)"; + command.Parameters.Add(12); + command.Parameters.Add("Melissa"); + command.Parameters.Add("Garcia"); + + command.Parameters.Add(13); + command.Parameters.Add("Russel"); + command.Parameters.Add("Morales"); + + command.Parameters.Add(14); + command.Parameters.Add("Jacqueline"); + command.Parameters.Add("Long"); + + command.Parameters.Add(15); + command.Parameters.Add("Dylan"); + command.Parameters.Add("Shaw"); + + var affected = await command.ExecuteNonQueryAsync(); + Console.WriteLine($"{affected} record(s) inserted."); + } + // [END spanner_dml_getting_started_insert] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithDmlSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithDmlSamplePostgreSql.cs new file mode 100644 index 00000000..c56c3d9f --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithDmlSamplePostgreSql.cs @@ -0,0 +1,51 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class WriteDataWithDmlSamplePostgreSql +{ + // [START spanner_dml_getting_started_insert] + public static async Task WriteDataWithDml(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // Add 4 rows in one statement. + // The ADO.NET driver supports positional query parameters. + await using var command = connection.CreateCommand(); + command.CommandText = "insert into singers (singer_id, first_name, last_name) " + + "VALUES (?, ?, ?), (?, ?, ?), " + + " (?, ?, ?), (?, ?, ?)"; + command.Parameters.Add(12); + command.Parameters.Add("Melissa"); + command.Parameters.Add("Garcia"); + + command.Parameters.Add(13); + command.Parameters.Add("Russel"); + command.Parameters.Add("Morales"); + + command.Parameters.Add(14); + command.Parameters.Add("Jacqueline"); + command.Parameters.Add("Long"); + + command.Parameters.Add(15); + command.Parameters.Add("Dylan"); + command.Parameters.Add("Shaw"); + + var affected = await command.ExecuteNonQueryAsync(); + Console.WriteLine($"{affected} record(s) inserted."); + } + // [END spanner_dml_getting_started_insert] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithMutationsSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithMutationsSample.cs new file mode 100644 index 00000000..9d9aa895 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithMutationsSample.cs @@ -0,0 +1,78 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class WriteDataWithMutationsSample +{ + // [START spanner_insert_data] + struct Singer + { + internal long SingerId; + internal string FirstName; + internal string LastName; + } + + struct Album + { + internal long SingerId; + internal long AlbumId; + internal string Title; + } + + public static async Task WriteDataWithMutations(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + Singer[] singers = + [ + new() {SingerId=1, FirstName = "Marc", LastName = "Richards"}, + new() {SingerId=2, FirstName = "Catalina", LastName = "Smith"}, + new() {SingerId=3, FirstName = "Alice", LastName = "Trentor"}, + new() {SingerId=4, FirstName = "Lea", LastName = "Martin"}, + new() {SingerId=5, FirstName = "David", LastName = "Lomond"}, + ]; + Album[] albums = + [ + new() {SingerId = 1, AlbumId = 1, Title = "Total Junk"}, + new() {SingerId = 1, AlbumId = 2, Title = "Go, Go, Go"}, + new() {SingerId = 2, AlbumId = 1, Title = "Green"}, + new() {SingerId = 2, AlbumId = 2, Title = "Forever Hold Your Peace"}, + new() {SingerId = 2, AlbumId = 3, Title = "Terrified"}, + ]; + var batch = connection.CreateBatch(); + foreach (var singer in singers) + { + // The name of a parameter must correspond with a column name. + var command = batch.CreateInsertCommand("Singers"); + command.AddParameter("SingerId", singer.SingerId); + command.AddParameter("FirstName", singer.FirstName); + command.AddParameter("LastName", singer.LastName); + batch.BatchCommands.Add(command); + } + foreach (var album in albums) + { + // The name of a parameter must correspond with a column name. + var command = batch.CreateInsertCommand("Albums"); + command.AddParameter("SingerId", album.SingerId); + command.AddParameter("AlbumId", album.AlbumId); + command.AddParameter("AlbumTitle", album.Title); + batch.BatchCommands.Add(command); + } + var affected = await batch.ExecuteNonQueryAsync(); + Console.WriteLine($"Inserted {affected} rows."); + } + // [END spanner_insert_data] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithMutationsSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithMutationsSamplePostgreSql.cs new file mode 100644 index 00000000..b15f0f01 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithMutationsSamplePostgreSql.cs @@ -0,0 +1,78 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class WriteDataWithMutationsSamplePostgreSql +{ + // [START spanner_insert_data] + struct Singer + { + internal long SingerId; + internal string FirstName; + internal string LastName; + } + + struct Album + { + internal long SingerId; + internal long AlbumId; + internal string Title; + } + + public static async Task WriteDataWithMutations(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + Singer[] singers = + [ + new() {SingerId=1, FirstName = "Marc", LastName = "Richards"}, + new() {SingerId=2, FirstName = "Catalina", LastName = "Smith"}, + new() {SingerId=3, FirstName = "Alice", LastName = "Trentor"}, + new() {SingerId=4, FirstName = "Lea", LastName = "Martin"}, + new() {SingerId=5, FirstName = "David", LastName = "Lomond"}, + ]; + Album[] albums = + [ + new() {SingerId = 1, AlbumId = 1, Title = "Total Junk"}, + new() {SingerId = 1, AlbumId = 2, Title = "Go, Go, Go"}, + new() {SingerId = 2, AlbumId = 1, Title = "Green"}, + new() {SingerId = 2, AlbumId = 2, Title = "Forever Hold Your Peace"}, + new() {SingerId = 2, AlbumId = 3, Title = "Terrified"}, + ]; + var batch = connection.CreateBatch(); + foreach (var singer in singers) + { + // The name of a parameter must correspond with a column name. + var command = batch.CreateInsertCommand("singers"); + command.AddParameter("singer_id", singer.SingerId); + command.AddParameter("first_name", singer.FirstName); + command.AddParameter("last_name", singer.LastName); + batch.BatchCommands.Add(command); + } + foreach (var album in albums) + { + // The name of a parameter must correspond with a column name. + var command = batch.CreateInsertCommand("albums"); + command.AddParameter("singer_id", album.SingerId); + command.AddParameter("album_id", album.AlbumId); + command.AddParameter("album_title", album.Title); + batch.BatchCommands.Add(command); + } + var affected = await batch.ExecuteNonQueryAsync(); + Console.WriteLine($"Inserted {affected} rows."); + } + // [END spanner_insert_data] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithTransactionSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithTransactionSample.cs new file mode 100644 index 00000000..d0513b57 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithTransactionSample.cs @@ -0,0 +1,100 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class WriteDataWithTransactionSample +{ + // [START spanner_dml_getting_started_update] + public static async Task WriteDataWithTransaction(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // Transfer marketing budget from one album to another. We do it in a + // transaction to ensure that the transfer is atomic. + await using var transaction = await connection.BeginTransactionAsync(); + + // The Spanner ADO.NET driver supports both positional and named + // query parameters. This query uses named query parameters. + const string selectSql = + "SELECT MarketingBudget " + + "FROM Albums " + + "WHERE SingerId = @singerId and AlbumId = @albumId"; + // Get the marketing_budget of singer 2 / album 2. + await using var command = connection.CreateCommand(); + command.CommandText = selectSql; + command.Transaction = transaction; + command.Parameters.AddWithValue("singerId", 2); + command.Parameters.AddWithValue("albumId", 2); + var budget2 = (long) (await command.ExecuteScalarAsync() ?? 0L); + + const long transfer = 20000L; + // The transaction will only be committed if this condition still holds + // at the time of commit. Otherwise, the transaction will be aborted. + if (budget2 >= transfer) + { + // Get the marketing_budget of singer 1 / album 1. + command.Parameters["singerId"].Value = 1; + command.Parameters["albumId"].Value = 1; + var budget1 = (long) (await command.ExecuteScalarAsync() ?? 0L); + + // Transfer part of the marketing budget of Album 2 to Album 1. + budget1 += transfer; + budget2 -= transfer; + const string updateSql = + "UPDATE Albums " + + "SET MarketingBudget = @budget " + + "WHERE SingerId = @singerId and AlbumId = @albumId"; + // Create a DML batch and execute it as part of the current transaction. + var batch = connection.CreateBatch(); + batch.Transaction = transaction; + + // Update the marketing budgets of both Album 1 and Album 2 in a batch. + (long SingerId, long AlbumId, long MarketingBudget)[] budgets = [ + new (1L, 1L, budget1), + new (2L, 2L, budget2), + ]; + foreach (var budget in budgets) + { + var batchCommand = batch.CreateBatchCommand(); + batchCommand.CommandText = updateSql; + var singerIdParameter = batchCommand.CreateParameter(); + singerIdParameter.ParameterName = "singerId"; + singerIdParameter.Value = budget.SingerId; + batchCommand.Parameters.Add(singerIdParameter); + var albumIdParameter = batchCommand.CreateParameter(); + albumIdParameter.ParameterName = "albumId"; + albumIdParameter.Value = budget.AlbumId; + batchCommand.Parameters.Add(albumIdParameter); + var marketingBudgetParameter = batchCommand.CreateParameter(); + marketingBudgetParameter.ParameterName = "budget"; + marketingBudgetParameter.Value = budget.MarketingBudget; + batchCommand.Parameters.Add(marketingBudgetParameter); + batch.BatchCommands.Add(batchCommand); + } + var affected = await batch.ExecuteNonQueryAsync(); + // The batch should update 2 rows. + if (affected != 2) + { + await transaction.RollbackAsync(); + throw new InvalidOperationException($"Unexpected num affected: {affected}"); + } + } + // Commit the transaction. + await transaction.CommitAsync(); + Console.WriteLine("Transferred marketing budget from Album 2 to Album 1"); + } + // [END spanner_dml_getting_started_update] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithTransactionSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithTransactionSamplePostgreSql.cs new file mode 100644 index 00000000..b3811d6b --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithTransactionSamplePostgreSql.cs @@ -0,0 +1,100 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; + +public static class WriteDataWithTransactionSamplePostgreSql +{ + // [START spanner_dml_getting_started_update] + public static async Task WriteDataWithTransaction(string connectionString) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + + // Transfer marketing budget from one album to another. We do it in a + // transaction to ensure that the transfer is atomic. + await using var transaction = await connection.BeginTransactionAsync(); + + // The Spanner ADO.NET driver supports both positional and named + // query parameters. This query uses named query parameters. + const string selectSql = + "SELECT marketing_budget " + + "FROM albums " + + "WHERE singer_id = $1 and album_id = $2"; + // Get the marketing_budget of singer 2 / album 2. + await using var command = connection.CreateCommand(); + command.CommandText = selectSql; + command.Transaction = transaction; + command.Parameters.AddWithValue("p1", 2); + command.Parameters.AddWithValue("p2", 2); + var budget2 = (long) (await command.ExecuteScalarAsync() ?? 0L); + + const long transfer = 20000L; + // The transaction will only be committed if this condition still holds + // at the time of commit. Otherwise, the transaction will be aborted. + if (budget2 >= transfer) + { + // Get the marketing_budget of singer 1 / album 1. + command.Parameters["p1"].Value = 1; + command.Parameters["p2"].Value = 1; + var budget1 = (long) (await command.ExecuteScalarAsync() ?? 0L); + + // Transfer part of the marketing budget of Album 2 to Album 1. + budget1 += transfer; + budget2 -= transfer; + const string updateSql = + "UPDATE albums " + + "SET marketing_budget = $1 " + + "WHERE singer_id = $2 and album_id = $3"; + // Create a DML batch and execute it as part of the current transaction. + var batch = connection.CreateBatch(); + batch.Transaction = transaction; + + // Update the marketing budgets of both Album 1 and Album 2 in a batch. + (long SingerId, long AlbumId, long MarketingBudget)[] budgets = [ + new (1L, 1L, budget1), + new (2L, 2L, budget2), + ]; + foreach (var budget in budgets) + { + var batchCommand = batch.CreateBatchCommand(); + batchCommand.CommandText = updateSql; + var marketingBudgetParameter = batchCommand.CreateParameter(); + marketingBudgetParameter.ParameterName = "p1"; + marketingBudgetParameter.Value = budget.MarketingBudget; + batchCommand.Parameters.Add(marketingBudgetParameter); + var singerIdParameter = batchCommand.CreateParameter(); + singerIdParameter.ParameterName = "p2"; + singerIdParameter.Value = budget.SingerId; + batchCommand.Parameters.Add(singerIdParameter); + var albumIdParameter = batchCommand.CreateParameter(); + albumIdParameter.ParameterName = "p3"; + albumIdParameter.Value = budget.AlbumId; + batchCommand.Parameters.Add(albumIdParameter); + batch.BatchCommands.Add(batchCommand); + } + var affected = await batch.ExecuteNonQueryAsync(); + // The batch should update 2 rows. + if (affected != 2) + { + await transaction.RollbackAsync(); + throw new InvalidOperationException($"Unexpected num affected: {affected}"); + } + } + // Commit the transaction. + await transaction.CommitAsync(); + Console.WriteLine("Transferred marketing budget from Album 2 to Album 1"); + } + // [END spanner_dml_getting_started_update] +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/SampleRunner.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/SampleRunner.cs index a504ac7c..ab5d25b0 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/SampleRunner.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/SampleRunner.cs @@ -13,6 +13,7 @@ // limitations under the License. using System.Reflection; +using Docker.DotNet.Models; using Google.Api.Gax; using Google.Cloud.Spanner.Common.V1; using Google.Cloud.Spanner.DataProvider.Samples.Snippets; @@ -61,7 +62,7 @@ private static void RunAllSamples() } } - private static void RunSample(string sampleName, bool failOnException) + internal static void RunSample(string sampleName, bool failOnException) { if (sampleName.EndsWith("Sample")) { @@ -93,7 +94,8 @@ private static async Task RunSampleAsync(Func sampleMethod) try { Console.WriteLine(""); - if (emulatorRunner.IsEmulatorRunning()) + PortBinding? portBinding = null; + if (EmulatorRunner.IsEmulatorRunning()) { Console.WriteLine("Emulator is already running. Re-using existing Emulator instance..."); Console.WriteLine(""); @@ -101,7 +103,7 @@ private static async Task RunSampleAsync(Func sampleMethod) else { Console.WriteLine("Starting emulator..."); - var portBinding = await emulatorRunner.StartEmulator(); + portBinding = await emulatorRunner.StartEmulator(); Console.WriteLine($"Emulator started on port {portBinding.HostPort}"); Console.WriteLine(""); startedEmulator = true; @@ -111,11 +113,16 @@ private static async Task RunSampleAsync(Func sampleMethod) var instanceId = "sample-instance"; var databaseId = "sample-database"; DatabaseName databaseName = DatabaseName.FromProjectInstanceDatabase(projectId, instanceId, databaseId); - var connectionStringBuilder = new SpannerConnectionStringBuilder() + var connectionStringBuilder = new SpannerConnectionStringBuilder { DataSource = databaseName.ToString(), AutoConfigEmulator = true, }; + if (portBinding != null) + { + connectionStringBuilder.Host = portBinding.HostIP; + connectionStringBuilder.Port = uint.Parse(portBinding.HostPort); + } await ExecuteScript(connectionStringBuilder.ConnectionString, "create_sample_tables.sql"); await ExecuteScript(connectionStringBuilder.ConnectionString, "insert_sample_data.sql"); diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/CommitTimestampSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/CommitTimestampSample.cs index fcabb63e..1b950de8 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/CommitTimestampSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/CommitTimestampSample.cs @@ -15,7 +15,7 @@ namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; // Example for getting the commit timestamp of a read/write transaction. -public class CommitTimestampSample +public static class CommitTimestampSample { public static async Task Run(string connectionString) { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/CustomConfigurationSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/CustomConfigurationSample.cs index 304535e5..cbc4306b 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/CustomConfigurationSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/CustomConfigurationSample.cs @@ -18,7 +18,7 @@ namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; // Sample that shows how to supply a custom configuration for the Spanner client // that is used by the driver. -public class CustomConfigurationSample +public static class CustomConfigurationSample { public static async Task Run(string connectionString) { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DataTypesSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DataTypesSample.cs index d5cbc9f3..48c8bf98 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DataTypesSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DataTypesSample.cs @@ -3,7 +3,7 @@ namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; // Sample showing how to work with the different data types that are supported by Spanner: // 1. How to set data of each type as a statement parameter. // 2. How to get data from columns of each type. -public class DataTypesSample +public static class DataTypesSample { private const string CreateAllTypesTable = @" CREATE TABLE IF NOT EXISTS AllTypes ( diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DdlBatchSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DdlBatchSample.cs index f87274ff..e069c80f 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DdlBatchSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DdlBatchSample.cs @@ -19,7 +19,7 @@ namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; /// Executing multiple DDL statements as a single batch is much more efficient than /// executing them as separate statements. /// -public class DdlBatchSample +public static class DdlBatchSample { public static async Task Run(string connectionString) { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DmlBatchSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DmlBatchSample.cs index 47ab909f..78a5f72e 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DmlBatchSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/DmlBatchSample.cs @@ -21,7 +21,7 @@ namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; /// Executing multiple DML statements as a single batch is more efficient than /// executing them as separate statements, as it reduces the number of round-trips to Spanner. /// -public class DmlBatchSample +public static class DmlBatchSample { private struct Singer { @@ -74,7 +74,7 @@ private static async Task ExecuteWithAdoBatch(SpannerConnection connection, stri // Execute the batch of DML statements. long affected = await batch.ExecuteNonQueryAsync(); - Console.WriteLine("Executed ADO.NET batch"); + Console.WriteLine($"Executed ADO.NET batch"); Console.WriteLine("Affected: " + affected); Console.WriteLine(); } diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/EmulatorSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/EmulatorSample.cs new file mode 100644 index 00000000..5cd5a4e8 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/EmulatorSample.cs @@ -0,0 +1,48 @@ +// Copyright 2025 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 +// +// https://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. + +namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; + +/// +/// This sample shows how to connect to the Spanner Emulator with the Spanner ADO.NET driver. +/// +public class EmulatorSample +{ + public static async Task Run(string connectionString) + { + // Create a SpannerConnectionStringBuilder and set the AutoConfigEmulator property to true. + // This instructs the driver to do the following: + // 1. Connect to the default Emulator endpoint 'localhost:9010' using plain text and no credentials. + // 2. Automatically create the Spanner instance and database in the connection string. + // The latter means that you do not need to first create the instance and database using some other tool + // before connecting to the Emulator. + // You can override the default endpoint by setting the Host and Port properties in the ConnectionStringBuilder + // if the Emulator is running on a non-default host/port. + var builder = new SpannerConnectionStringBuilder(connectionString) + { + AutoConfigEmulator = true + }; + + // Create a new connection using the ConnectionString from the SpannerConnectionStringBuilder. + await using var connection = new SpannerConnection(builder.ConnectionString); + await connection.OpenAsync(); + await using var command = connection.CreateCommand(); + command.CommandText = "SELECT 'Hello World' as Message"; + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + Console.WriteLine($"Greeting from Spanner Emulator: {reader.GetString(0)}"); + } + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/MutationsSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/MutationsSample.cs index 7286e102..dbeebd87 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/MutationsSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/MutationsSample.cs @@ -17,7 +17,7 @@ namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; /// /// Example for using Mutations with the Spanner ADO.NET data provider. /// -public class MutationsSample +public static class MutationsSample { public static async Task Run(string connectionString) { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/PartitionedDmlSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/PartitionedDmlSample.cs index d6f2a28b..4eaee965 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/PartitionedDmlSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/PartitionedDmlSample.cs @@ -17,7 +17,7 @@ namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; /// /// Example for using Partitioned DML with the Spanner ADO.NET data provider. /// -public class PartitionedDmlSample +public static class PartitionedDmlSample { public static async Task Run(string connectionString) { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/QueryParametersSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/QueryParametersSample.cs index b17c111d..59883186 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/QueryParametersSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/QueryParametersSample.cs @@ -19,7 +19,7 @@ namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; /// Using query parameters is recommended, as it allows Spanner to cache and reuse the query plan, /// and it helps to prevent SQL injection attacks in your application. /// -public class QueryParametersSample +public static class QueryParametersSample { public static async Task Run(string connectionString) { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/ReadOnlyTransactionSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/ReadOnlyTransactionSample.cs index 39e945c8..1452a744 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/ReadOnlyTransactionSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/ReadOnlyTransactionSample.cs @@ -19,7 +19,7 @@ namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; /// Read-only transactions do not take locks on Spanner, and should be used when your application needs /// to execute multiple queries that read from the same snapshot of the database. /// -public class ReadOnlyTransactionSample +public static class ReadOnlyTransactionSample { public static async Task Run(string connectionString) { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/StaleReadSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/StaleReadSample.cs index b4b602f0..28aaaa96 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/StaleReadSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/StaleReadSample.cs @@ -21,7 +21,7 @@ namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; /// This sample shows how to execute stale reads using both single statements and read-only transaction /// with the Spanner ADO.NET data provider. /// -public class StaleReadSample +public static class StaleReadSample { public static async Task Run(string connectionString) { @@ -60,10 +60,15 @@ private static async Task SingleStaleQuery(SpannerConnection connection) /// private static async Task StaleReadOnlyTransaction(SpannerConnection connection) { + // Get the current time from Spanner so we can use that as the read-timestamp for the transaction. + await using var currentTimeCommand = connection.CreateCommand(); + currentTimeCommand.CommandText = "SELECT CURRENT_TIMESTAMP"; + var currentTime = (DateTime?) await currentTimeCommand.ExecuteScalarAsync(); + // Start a read-only transaction using the BeginReadOnlyTransaction method. await using var transaction = connection.BeginReadOnlyTransaction(new TransactionOptions.Types.ReadOnly { - ExactStaleness = Duration.FromTimeSpan(TimeSpan.FromSeconds(10)), + ReadTimestamp = Timestamp.FromDateTime(currentTime!.Value), }); // Execute a query that uses this transaction. diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/TagsSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/TagsSample.cs index 9b516145..196193dc 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/TagsSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/TagsSample.cs @@ -17,7 +17,7 @@ namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; /// /// This sample shows how to use request and transaction tags with the Spanner ADO.NET data provider. /// -public class TagsSample +public static class TagsSample { public static async Task Run(string connectionString) { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/TransactionSample.cs b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/TransactionSample.cs index aac9970f..fd0a1315 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/TransactionSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples/Snippets/TransactionSample.cs @@ -19,7 +19,7 @@ namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets; /// /// This sample shows how to execute a read/write transaction using the Spanner ADO.NET data provider. /// -public class TransactionSample +public static class TransactionSample { public static async Task Run(string connectionString) { diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs index 1fa89cc2..3b8bbc57 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatch.cs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; using System.Collections.Generic; using System.Data; using System.Data.Common; @@ -38,6 +39,10 @@ public class SpannerBatch : DbBatch protected override DbTransaction? DbTransaction { get; set; } private SpannerTransaction? SpannerTransaction => DbTransaction as SpannerTransaction; public string? Tag { get; set; } + + private bool HasOnlyMutations => BatchCommands.All(cmd => ((SpannerBatchCommand)cmd).HasMutation); + + private bool HasOnlyDmlCommands => BatchCommands.All(cmd => !((SpannerBatchCommand)cmd).HasMutation); public SpannerBatch() {} @@ -49,12 +54,30 @@ internal SpannerBatch(SpannerConnection connection) protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) { - throw new System.NotImplementedException(); + throw new NotImplementedException(); } protected override Task ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) { - throw new System.NotImplementedException(); + throw new NotImplementedException(); + } + + private List CreateMutations() + { + var mutations = new List(DbBatchCommands.Count); + foreach (var command in DbBatchCommands) + { + mutations.Add(SpannerCommand.BuildMutation(command.Mutation!, command.SpannerParameterCollection)); + } + return mutations; + } + + private BatchWriteRequest.Types.MutationGroup CreateMutationGroup() + { + return new BatchWriteRequest.Types.MutationGroup + { + Mutations = { CreateMutations() }, + }; } private List CreateStatements() @@ -62,9 +85,7 @@ protected override Task ExecuteDbDataReaderAsync(CommandBehavior b var statements = new List(DbBatchCommands.Count); foreach (var command in DbBatchCommands) { - var spannerParams = ((SpannerParameterCollection)command.Parameters).CreateSpannerParams(prepare: false); - var queryParams = spannerParams.Item1; - var paramTypes = spannerParams.Item2; + var (queryParams, paramTypes) = ((SpannerParameterCollection)command.Parameters).CreateSpannerParams(prepare: false); var batchStatement = new ExecuteBatchDmlRequest.Types.Statement { Sql = command.CommandText, @@ -103,7 +124,7 @@ private void SetTags() CreateSetTagsCommandText()?.ExecuteNonQuery(); } - private Task SetRequestTagAsync(CancellationToken cancellationToken) + private Task SetTagsAsync(CancellationToken cancellationToken) { var command = CreateSetTagsCommandText(); if (command != null) @@ -119,11 +140,27 @@ public override int ExecuteNonQuery() { return 0; } - var statements = CreateStatements(); - SetTags(); - var results = SpannerConnection.ExecuteBatch(statements); + long[] results; + if (HasOnlyMutations) + { + var mutationGroup = CreateMutationGroup(); + SetTags(); + SpannerConnection.WriteMutations(mutationGroup); + results = new long[mutationGroup.Mutations.Count]; + Array.Fill(results, 1L); + } + else if (HasOnlyDmlCommands) + { + var statements = CreateStatements(); + SetTags(); + results = SpannerConnection.ExecuteBatch(statements); + } + else + { + throw new InvalidOperationException("Batches must contain either only DML statements or only mutations."); + } DbBatchCommands.SetAffected(results); - return (int) results.Sum(); + return (int)results.Sum(); } public override async Task ExecuteNonQueryAsync(CancellationToken cancellationToken = default) @@ -132,31 +169,47 @@ public override async Task ExecuteNonQueryAsync(CancellationToken cancellat { return 0; } - var statements = CreateStatements(); - await SetRequestTagAsync(cancellationToken).ConfigureAwait(false); - var results = await SpannerConnection.ExecuteBatchAsync(statements, cancellationToken).ConfigureAwait(false); + long[] results; + if (HasOnlyMutations) + { + var mutationGroup = CreateMutationGroup(); + await SetTagsAsync(cancellationToken).ConfigureAwait(false); + await SpannerConnection.WriteMutationsAsync(mutationGroup, cancellationToken).ConfigureAwait(false); + results = new long[mutationGroup.Mutations.Count]; + Array.Fill(results, 1L); + } + else if (HasOnlyDmlCommands) + { + var statements = CreateStatements(); + await SetTagsAsync(cancellationToken).ConfigureAwait(false); + results = await SpannerConnection.ExecuteBatchAsync(statements, cancellationToken) .ConfigureAwait(false); + } + else + { + throw new InvalidOperationException("Batches must contain either only DML statements or only mutations."); + } DbBatchCommands.SetAffected(results); - return (int) results.Sum(); + return (int)results.Sum(); } public override object? ExecuteScalar() { - throw new System.NotImplementedException(); + throw new NotImplementedException(); } public override Task ExecuteScalarAsync(CancellationToken cancellationToken = default) { - throw new System.NotImplementedException(); + throw new NotImplementedException(); } public override void Prepare() { - throw new System.NotImplementedException(); + throw new NotImplementedException(); } public override Task PrepareAsync(CancellationToken cancellationToken = default) { - throw new System.NotImplementedException(); + throw new NotImplementedException(); } public override void Cancel() @@ -164,6 +217,51 @@ public override void Cancel() throw new System.NotImplementedException(); } + /// + /// Creates a command to insert data into Spanner using mutations. + /// + /// The table to insert data into + public SpannerBatchCommand CreateInsertCommand(string table) + { + return new SpannerBatchCommand(new Mutation { Insert = new Mutation.Types.Write { Table = table } }); + } + + /// + /// Creates a command to insert-or-update data into Spanner using mutations. + /// + /// The table to insert-or-update data into + public SpannerBatchCommand CreateInsertOrUpdateCommand(string table) + { + return new SpannerBatchCommand(new Mutation { InsertOrUpdate = new Mutation.Types.Write { Table = table } }); + } + + /// + /// Creates a command to update data in Spanner using mutations. + /// + /// The table that contains the data that should be updated + public SpannerBatchCommand CreateUpdateCommand(string table) + { + return new SpannerBatchCommand(new Mutation { Update = new Mutation.Types.Write { Table = table } }); + } + + /// + /// Creates a command to replace data in Spanner using mutations. + /// + /// The table that contains the data that should be replaced + public SpannerBatchCommand CreateReplaceCommand(string table) + { + return new SpannerBatchCommand(new Mutation { Replace = new Mutation.Types.Write { Table = table } }); + } + + /// + /// Creates a command to delete data in Spanner using mutations. + /// + /// The table that contains the data that should be deleted + public SpannerBatchCommand CreateDeleteCommand(string table) + { + return new SpannerBatchCommand(new Mutation { Delete = new Mutation.Types.Delete { Table = table } }); + } + protected override DbBatchCommand CreateDbBatchCommand() { return new SpannerBatchCommand(); diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerBatchCommand.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatchCommand.cs index 4e93a7cd..6c2bde58 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerBatchCommand.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerBatchCommand.cs @@ -14,6 +14,7 @@ using System.Data; using System.Data.Common; +using Google.Cloud.Spanner.V1; namespace Google.Cloud.Spanner.DataProvider; @@ -26,9 +27,35 @@ public class SpannerBatchCommand : DbBatchCommand public override int RecordsAffected => InternalRecordsAffected; protected override DbParameterCollection DbParameterCollection { get; } = new SpannerParameterCollection(); public override bool CanCreateParameter => true; + + internal SpannerParameterCollection SpannerParameterCollection => (SpannerParameterCollection)DbParameterCollection; + + internal bool HasMutation => Mutation != null; + + internal Mutation? Mutation { get; } + + internal SpannerBatchCommand() + { + } + + internal SpannerBatchCommand(Mutation mutation) + { + Mutation = mutation; + } public override DbParameter CreateParameter() { return new SpannerParameter(); } + + public SpannerParameter AddParameter(string name, object value) + { + var parameter = new SpannerParameter + { + ParameterName = name, + Value = value + }; + SpannerParameterCollection.Add(parameter); + return parameter; + } } \ No newline at end of file diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs index 69da7bc6..31f523bb 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerCommand.cs @@ -167,8 +167,11 @@ private Mutation BuildMutation() GaxPreconditions.CheckNotNull(SpannerConnection, nameof(SpannerConnection)); GaxPreconditions.CheckState(!(HasTransaction && SingleUseReadOnlyTransactionOptions != null), "Cannot set both a transaction and single-use read-only options"); + return BuildMutation(_mutation!.Clone(), Parameters); + } - var mutation = _mutation!.Clone(); + internal static Mutation BuildMutation(Mutation mutation, SpannerParameterCollection parameters) + { Mutation.Types.Write? write = null; Mutation.Types.Delete? delete = mutation.OperationCase == Mutation.OperationOneofCase.Delete ? mutation.Delete @@ -190,9 +193,9 @@ private Mutation BuildMutation() } var values = new ListValue(); - for (var index = 0; index < DbParameterCollection.Count; index++) + for (var index = 0; index < parameters.Count; index++) { - var param = DbParameterCollection[index]; + var param = parameters[index]; if (param is SpannerParameter spannerParameter) { if (write != null) diff --git a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs index 4e552a22..587091f2 100644 --- a/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs +++ b/drivers/spanner-ado-net/spanner-ado-net/SpannerConnection.cs @@ -17,7 +17,6 @@ using System.Data; using System.Data.Common; using System.Diagnostics.CodeAnalysis; -using System.Reflection.Metadata.Ecma335; using System.Threading; using System.Threading.Tasks; using Google.Api.Gax; @@ -210,6 +209,19 @@ public SpannerTransaction BeginReadOnlyTransaction() }); } + /// + /// Starts a new read-only transaction with default options. + /// + /// The new transaction + /// If the connection has an active transaction + public ValueTask BeginReadOnlyTransactionAsync(CancellationToken cancellationToken = default) + { + return BeginTransactionAsync(new TransactionOptions + { + ReadOnly = new TransactionOptions.Types.ReadOnly(), + }, cancellationToken); + } + /// /// Starts a new read-only transaction using the given options. /// @@ -224,6 +236,22 @@ public SpannerTransaction BeginReadOnlyTransaction(TransactionOptions.Types.Read }); } + /// + /// Starts a new read-only transaction using the given options. + /// + /// The options to use for the new read-only transaction + /// The cancellation token + /// The new transaction + /// If the connection has an active transaction + public ValueTask BeginReadOnlyTransactionAsync( + TransactionOptions.Types.ReadOnly readOnlyOptions, CancellationToken cancellationToken) + { + return BeginTransactionAsync(new TransactionOptions + { + ReadOnly = readOnlyOptions, + }, cancellationToken); + } + /// /// Start a new transaction using the given TransactionOptions. /// diff --git a/emulator_util.go b/emulator_util.go index 7c3efa77..8aa99c12 100644 --- a/emulator_util.go +++ b/emulator_util.go @@ -20,20 +20,20 @@ import ( "cloud.google.com/go/spanner" database "cloud.google.com/go/spanner/admin/database/apiv1" - databasepb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" + "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" instance "cloud.google.com/go/spanner/admin/instance/apiv1" - instancepb "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb" + "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb" "google.golang.org/api/option" "google.golang.org/grpc/codes" ) -func autoConfigEmulator(ctx context.Context, host, project, instance, database string, opts []option.ClientOption) error { +func autoConfigEmulator(ctx context.Context, host, project, instance, database string, dialect databasepb.DatabaseDialect, opts []option.ClientOption) error { if err := createInstance(project, instance, opts); err != nil { if spanner.ErrCode(err) != codes.AlreadyExists { return err } } - if err := createDatabase(project, instance, database, opts); err != nil { + if err := createDatabase(project, instance, database, dialect, opts); err != nil { if spanner.ErrCode(err) != codes.AlreadyExists { return err } @@ -47,7 +47,7 @@ func createInstance(projectId, instanceId string, opts []option.ClientOption) er if err != nil { return err } - defer instanceAdmin.Close() + defer func() { _ = instanceAdmin.Close() }() op, err := instanceAdmin.CreateInstance(ctx, &instancepb.CreateInstanceRequest{ Parent: fmt.Sprintf("projects/%s", projectId), InstanceId: instanceId, @@ -67,16 +67,21 @@ func createInstance(projectId, instanceId string, opts []option.ClientOption) er return nil } -func createDatabase(projectId, instanceId, databaseId string, opts []option.ClientOption) error { +func createDatabase(projectId, instanceId, databaseId string, dialect databasepb.DatabaseDialect, opts []option.ClientOption) error { ctx := context.Background() databaseAdminClient, err := database.NewDatabaseAdminClient(ctx, opts...) if err != nil { return err } - defer databaseAdminClient.Close() + defer func() { _ = databaseAdminClient.Close() }() + createStatement := fmt.Sprintf("CREATE DATABASE `%s`", databaseId) + if dialect == databasepb.DatabaseDialect_POSTGRESQL { + createStatement = fmt.Sprintf(`CREATE DATABASE "%s"`, databaseId) + } opDB, err := databaseAdminClient.CreateDatabase(ctx, &databasepb.CreateDatabaseRequest{ Parent: fmt.Sprintf("projects/%s/instances/%s", projectId, instanceId), - CreateStatement: fmt.Sprintf("CREATE DATABASE `%s`", databaseId), + CreateStatement: createStatement, + DatabaseDialect: dialect, }) if err != nil { return err From ae5cc38e75308e1b927be416b391635048a8c32e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Fri, 16 Jan 2026 14:47:20 +0100 Subject: [PATCH 29/29] chore: moved Getting Started Guide to separate project --- .../AssemblyInfo.cs | 2 + .../EmulatorRunner.cs | 127 ++++++++++ .../GettingStartedGuide/AddColumnSample.cs | 2 +- .../AddColumnSamplePostgreSql.cs | 2 +- .../CreateConnectionSample.cs | 2 +- .../CreateConnectionSamplePostgreSql.cs | 2 +- .../GettingStartedGuide/CreateTablesSample.cs | 2 +- .../CreateTablesSamplePostgreSql.cs | 2 +- .../GettingStartedGuide/DataBoostSample.cs | 2 +- .../DataBoostSamplePostgreSql.cs | 2 +- .../GettingStartedGuide/DdlBatchSample.cs | 2 +- .../DdlBatchSamplePostgreSql.cs | 2 +- .../PartitionedDmlSample.cs | 2 +- .../PartitionedDmlSamplePostgreSql.cs | 2 +- .../GettingStartedGuide/QueryDataSample.cs | 2 +- .../QueryDataSamplePostgreSql.cs | 2 +- .../QueryDataWithParameterSample.cs | 2 +- .../QueryDataWithParameterSamplePostgreSql.cs | 2 +- .../QueryNewColumnSample.cs | 2 +- .../QueryNewColumnSamplePostgreSql.cs | 2 +- .../ReadOnlyTransactionSample.cs | 2 +- .../ReadOnlyTransactionSamplePostgreSql.cs | 2 +- .../GettingStartedGuide/TagsSample.cs | 2 +- .../TagsSamplePostgreSql.cs | 2 +- .../UpdateDataWithMutationsSample.cs | 2 +- ...UpdateDataWithMutationsSamplePostgreSql.cs | 2 +- .../WriteDataWithDmlSample.cs | 2 +- .../WriteDataWithDmlSamplePostgreSql.cs | 2 +- .../WriteDataWithMutationsSample.cs | 2 +- .../WriteDataWithMutationsSamplePostgreSql.cs | 2 +- .../WriteDataWithTransactionSample.cs | 2 +- ...riteDataWithTransactionSamplePostgreSql.cs | 2 +- .../README.md | 5 + .../SampleRunner.cs | 217 ++++++++++++++++++ ...anner-ado-net-getting-started-guide.csproj | 21 ++ .../GettingStartedTests.cs | 2 +- .../spanner-ado-net-samples-tests.csproj | 1 + drivers/spanner-ado-net/spanner-ado-net.sln | 6 + 38 files changed, 410 insertions(+), 31 deletions(-) create mode 100644 drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/AssemblyInfo.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/EmulatorRunner.cs rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/AddColumnSample.cs (94%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/AddColumnSamplePostgreSql.cs (94%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/CreateConnectionSample.cs (96%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/CreateConnectionSamplePostgreSql.cs (96%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/CreateTablesSample.cs (96%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/CreateTablesSamplePostgreSql.cs (96%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/DataBoostSample.cs (93%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/DataBoostSamplePostgreSql.cs (93%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/DdlBatchSample.cs (96%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/DdlBatchSamplePostgreSql.cs (96%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/PartitionedDmlSample.cs (95%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/PartitionedDmlSamplePostgreSql.cs (95%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/QueryDataSample.cs (94%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/QueryDataSamplePostgreSql.cs (94%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/QueryDataWithParameterSample.cs (95%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/QueryDataWithParameterSamplePostgreSql.cs (95%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/QueryNewColumnSample.cs (95%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/QueryNewColumnSamplePostgreSql.cs (95%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/ReadOnlyTransactionSample.cs (96%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/ReadOnlyTransactionSamplePostgreSql.cs (96%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/TagsSample.cs (97%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/TagsSamplePostgreSql.cs (97%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/UpdateDataWithMutationsSample.cs (95%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/UpdateDataWithMutationsSamplePostgreSql.cs (95%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/WriteDataWithDmlSample.cs (96%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/WriteDataWithDmlSamplePostgreSql.cs (96%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/WriteDataWithMutationsSample.cs (97%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/WriteDataWithMutationsSamplePostgreSql.cs (97%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/WriteDataWithTransactionSample.cs (98%) rename drivers/spanner-ado-net/{spanner-ado-net-samples => spanner-ado-net-getting-started-guide}/GettingStartedGuide/WriteDataWithTransactionSamplePostgreSql.cs (98%) create mode 100644 drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/README.md create mode 100644 drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/SampleRunner.cs create mode 100644 drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/spanner-ado-net-getting-started-guide.csproj diff --git a/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/AssemblyInfo.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/AssemblyInfo.cs new file mode 100644 index 00000000..2765294c --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/AssemblyInfo.cs @@ -0,0 +1,2 @@ +using System.Runtime.CompilerServices; +[assembly:InternalsVisibleTo("Google.Cloud.Spanner.DataProvider.Samples.Tests")] diff --git a/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/EmulatorRunner.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/EmulatorRunner.cs new file mode 100644 index 00000000..5e821c8e --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/EmulatorRunner.cs @@ -0,0 +1,127 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Net.Sockets; + +namespace Google.Cloud.Spanner.DataProvider; + +using Docker.DotNet; +using Docker.DotNet.Models; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +/// +/// This class can be used to programmatically start and stop an instance of the Cloud Spanner emulator. +/// +internal class EmulatorRunner +{ + private static readonly string SEmulatorImageName = "gcr.io/cloud-spanner-emulator/emulator"; + private readonly DockerClient _dockerClient; + private string? _containerId; + + internal static DockerClient CreateDockerClient() + { + return new DockerClientConfiguration(new Uri(GetDockerApiUri())).CreateClient(); + } + + internal EmulatorRunner() + { + _dockerClient = CreateDockerClient(); + } + + internal static bool IsEmulatorRunning() + { + using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + try + { + socket.Connect("localhost", 9010); + } + catch (SocketException ex) + { + if (ex.SocketErrorCode == SocketError.ConnectionRefused) + { + return false; + } + throw; + } + return true; + } + + /// + /// Downloads the latest Spanner emulator docker image and starts the emulator on port 9010. + /// + internal async Task StartEmulator() + { + await PullEmulatorImage(); + var response = await _dockerClient.Containers.CreateContainerAsync(new CreateContainerParameters + { + Image = SEmulatorImageName, + ExposedPorts = new Dictionary + { + { + "9010", default + } + }, + HostConfig = new HostConfig + { + PortBindings = new Dictionary?> + { + {"9010", null} + }, + } + }); + _containerId = response.ID; + await _dockerClient.Containers.StartContainerAsync(_containerId, null); + var inspectResponse = await _dockerClient.Containers.InspectContainerAsync(_containerId); + Thread.Sleep(500); + return inspectResponse.NetworkSettings.Ports["9010/tcp"][0]; + } + + /// + /// Stops the currently running emulator. Fails if no emulator has been started. + /// + internal async Task StopEmulator() + { + if (_containerId != null) + { + await _dockerClient.Containers.KillContainerAsync(_containerId, new ContainerKillParameters()); + await _dockerClient.Containers.RemoveContainerAsync(_containerId, new ContainerRemoveParameters()); + } + } + + private async Task PullEmulatorImage() + { + await _dockerClient.Images.CreateImageAsync(new ImagesCreateParameters + { + FromImage = SEmulatorImageName, + Tag = "latest" + }, new AuthConfig(), new Progress()); + } + + private static string GetDockerApiUri() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return "npipe://./pipe/docker_engine"; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return "unix:/var/run/docker.sock"; + } + throw new Exception("Was unable to determine what OS this is running on, does not appear to be Windows or Linux!?"); + } +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/AddColumnSample.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/AddColumnSample.cs similarity index 94% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/AddColumnSample.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/AddColumnSample.cs index a595b02b..bf294cad 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/AddColumnSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/AddColumnSample.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class AddColumnSample { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/AddColumnSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/AddColumnSamplePostgreSql.cs similarity index 94% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/AddColumnSamplePostgreSql.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/AddColumnSamplePostgreSql.cs index d37a3689..5258dec1 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/AddColumnSamplePostgreSql.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/AddColumnSamplePostgreSql.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class AddColumnSamplePostgreSql { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateConnectionSample.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/CreateConnectionSample.cs similarity index 96% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateConnectionSample.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/CreateConnectionSample.cs index 37093c51..c10ffdf8 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateConnectionSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/CreateConnectionSample.cs @@ -14,7 +14,7 @@ using System.Data; -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class CreateConnectionSample { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateConnectionSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/CreateConnectionSamplePostgreSql.cs similarity index 96% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateConnectionSamplePostgreSql.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/CreateConnectionSamplePostgreSql.cs index a3db3a8a..a7775cc0 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateConnectionSamplePostgreSql.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/CreateConnectionSamplePostgreSql.cs @@ -14,7 +14,7 @@ using System.Data; -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class CreateConnectionSamplePostgreSql { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateTablesSample.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/CreateTablesSample.cs similarity index 96% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateTablesSample.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/CreateTablesSample.cs index d1414759..7e314819 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateTablesSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/CreateTablesSample.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class CreateTablesSample { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateTablesSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/CreateTablesSamplePostgreSql.cs similarity index 96% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateTablesSamplePostgreSql.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/CreateTablesSamplePostgreSql.cs index 6dc1a91d..290d35f0 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/CreateTablesSamplePostgreSql.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/CreateTablesSamplePostgreSql.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class CreateTablesSamplePostgreSql { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DataBoostSample.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/DataBoostSample.cs similarity index 93% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DataBoostSample.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/DataBoostSample.cs index 6b24da7d..aad0cfeb 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DataBoostSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/DataBoostSample.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class DataBoostSample { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DataBoostSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/DataBoostSamplePostgreSql.cs similarity index 93% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DataBoostSamplePostgreSql.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/DataBoostSamplePostgreSql.cs index ad8f7640..ab5df32f 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DataBoostSamplePostgreSql.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/DataBoostSamplePostgreSql.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class DataBoostSamplePostgreSql { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DdlBatchSample.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/DdlBatchSample.cs similarity index 96% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DdlBatchSample.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/DdlBatchSample.cs index 0d38bd75..3eec1784 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DdlBatchSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/DdlBatchSample.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class DdlBatchSample { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DdlBatchSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/DdlBatchSamplePostgreSql.cs similarity index 96% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DdlBatchSamplePostgreSql.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/DdlBatchSamplePostgreSql.cs index cb94d2c8..bc240ab7 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/DdlBatchSamplePostgreSql.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/DdlBatchSamplePostgreSql.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class DdlBatchSamplePostgreSql { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/PartitionedDmlSample.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/PartitionedDmlSample.cs similarity index 95% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/PartitionedDmlSample.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/PartitionedDmlSample.cs index 81660b77..2abda535 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/PartitionedDmlSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/PartitionedDmlSample.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class PartitionedDmlSample { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/PartitionedDmlSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/PartitionedDmlSamplePostgreSql.cs similarity index 95% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/PartitionedDmlSamplePostgreSql.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/PartitionedDmlSamplePostgreSql.cs index 5607dc9c..b1e93183 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/PartitionedDmlSamplePostgreSql.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/PartitionedDmlSamplePostgreSql.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class PartitionedDmlSamplePostgreSql { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataSample.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/QueryDataSample.cs similarity index 94% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataSample.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/QueryDataSample.cs index d81aa27f..d38ffaef 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/QueryDataSample.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class QueryDataSample { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/QueryDataSamplePostgreSql.cs similarity index 94% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataSamplePostgreSql.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/QueryDataSamplePostgreSql.cs index 306ae440..3389096a 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataSamplePostgreSql.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/QueryDataSamplePostgreSql.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class QueryDataSamplePostgreSql { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataWithParameterSample.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/QueryDataWithParameterSample.cs similarity index 95% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataWithParameterSample.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/QueryDataWithParameterSample.cs index 56547c2b..24422cd6 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataWithParameterSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/QueryDataWithParameterSample.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class QueryDataWithParameterSample { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataWithParameterSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/QueryDataWithParameterSamplePostgreSql.cs similarity index 95% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataWithParameterSamplePostgreSql.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/QueryDataWithParameterSamplePostgreSql.cs index ad6993e1..4c58ea39 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryDataWithParameterSamplePostgreSql.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/QueryDataWithParameterSamplePostgreSql.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class QueryDataWithParameterSamplePostgreSql { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryNewColumnSample.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/QueryNewColumnSample.cs similarity index 95% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryNewColumnSample.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/QueryNewColumnSample.cs index 104a529a..86efcb1e 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryNewColumnSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/QueryNewColumnSample.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class QueryNewColumnSample { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryNewColumnSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/QueryNewColumnSamplePostgreSql.cs similarity index 95% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryNewColumnSamplePostgreSql.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/QueryNewColumnSamplePostgreSql.cs index e81f655c..9f0360ff 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/QueryNewColumnSamplePostgreSql.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/QueryNewColumnSamplePostgreSql.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class QueryNewColumnSamplePostgreSql { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/ReadOnlyTransactionSample.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/ReadOnlyTransactionSample.cs similarity index 96% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/ReadOnlyTransactionSample.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/ReadOnlyTransactionSample.cs index 7fe71c9c..470050ba 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/ReadOnlyTransactionSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/ReadOnlyTransactionSample.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class ReadOnlyTransactionSample { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/ReadOnlyTransactionSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/ReadOnlyTransactionSamplePostgreSql.cs similarity index 96% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/ReadOnlyTransactionSamplePostgreSql.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/ReadOnlyTransactionSamplePostgreSql.cs index a6308d2b..766d4f98 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/ReadOnlyTransactionSamplePostgreSql.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/ReadOnlyTransactionSamplePostgreSql.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class ReadOnlyTransactionSamplePostgreSql { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/TagsSample.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/TagsSample.cs similarity index 97% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/TagsSample.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/TagsSample.cs index 7d93f066..fa819155 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/TagsSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/TagsSample.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class TagsSample { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/TagsSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/TagsSamplePostgreSql.cs similarity index 97% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/TagsSamplePostgreSql.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/TagsSamplePostgreSql.cs index 086e4d00..bc1a4c50 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/TagsSamplePostgreSql.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/TagsSamplePostgreSql.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class TagsSamplePostgreSql { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/UpdateDataWithMutationsSample.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/UpdateDataWithMutationsSample.cs similarity index 95% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/UpdateDataWithMutationsSample.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/UpdateDataWithMutationsSample.cs index 76a8f5cf..792835fa 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/UpdateDataWithMutationsSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/UpdateDataWithMutationsSample.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class UpdateDataWithMutationsSample { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/UpdateDataWithMutationsSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/UpdateDataWithMutationsSamplePostgreSql.cs similarity index 95% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/UpdateDataWithMutationsSamplePostgreSql.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/UpdateDataWithMutationsSamplePostgreSql.cs index cdeb7a18..01eb6785 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/UpdateDataWithMutationsSamplePostgreSql.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/UpdateDataWithMutationsSamplePostgreSql.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class UpdateDataWithMutationsSamplePostgreSql { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithDmlSample.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/WriteDataWithDmlSample.cs similarity index 96% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithDmlSample.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/WriteDataWithDmlSample.cs index 0b0f4162..f26236ad 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithDmlSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/WriteDataWithDmlSample.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class WriteDataWithDmlSample { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithDmlSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/WriteDataWithDmlSamplePostgreSql.cs similarity index 96% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithDmlSamplePostgreSql.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/WriteDataWithDmlSamplePostgreSql.cs index c56c3d9f..cc4ea89f 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithDmlSamplePostgreSql.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/WriteDataWithDmlSamplePostgreSql.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class WriteDataWithDmlSamplePostgreSql { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithMutationsSample.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/WriteDataWithMutationsSample.cs similarity index 97% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithMutationsSample.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/WriteDataWithMutationsSample.cs index 9d9aa895..7e257c5e 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithMutationsSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/WriteDataWithMutationsSample.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class WriteDataWithMutationsSample { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithMutationsSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/WriteDataWithMutationsSamplePostgreSql.cs similarity index 97% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithMutationsSamplePostgreSql.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/WriteDataWithMutationsSamplePostgreSql.cs index b15f0f01..99bfd6a3 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithMutationsSamplePostgreSql.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/WriteDataWithMutationsSamplePostgreSql.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class WriteDataWithMutationsSamplePostgreSql { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithTransactionSample.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/WriteDataWithTransactionSample.cs similarity index 98% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithTransactionSample.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/WriteDataWithTransactionSample.cs index d0513b57..6751ca51 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithTransactionSample.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/WriteDataWithTransactionSample.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class WriteDataWithTransactionSample { diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithTransactionSamplePostgreSql.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/WriteDataWithTransactionSamplePostgreSql.cs similarity index 98% rename from drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithTransactionSamplePostgreSql.cs rename to drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/WriteDataWithTransactionSamplePostgreSql.cs index b3811d6b..eac9f8d6 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples/GettingStartedGuide/WriteDataWithTransactionSamplePostgreSql.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/GettingStartedGuide/WriteDataWithTransactionSamplePostgreSql.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +namespace Google.Cloud.Spanner.DataProvider.GettingStartedGuide; public static class WriteDataWithTransactionSamplePostgreSql { diff --git a/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/README.md b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/README.md new file mode 100644 index 00000000..e7250b3e --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/README.md @@ -0,0 +1,5 @@ +# Spanner ADO.NET Data Provider Getting Started Guide Samples + +__ALPHA: This library is still in development. It is not yet ready for production use.__ + +This project contains the code samples that are used for the Spanner ADO.NET Getting Started Guide. diff --git a/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/SampleRunner.cs b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/SampleRunner.cs new file mode 100644 index 00000000..8a5fc427 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/SampleRunner.cs @@ -0,0 +1,217 @@ +// Copyright 2025 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 +// +// https://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. + +using System.Reflection; +using Docker.DotNet.Models; +using Google.Api.Gax; +using Google.Cloud.Spanner.Common.V1; +using Google.Cloud.Spanner.DataProvider.GettingStartedGuide; + +namespace Google.Cloud.Spanner.DataProvider; + +/// +/// Main class for running a sample from the GettingStartedGuide directory. +/// Usage: `dotnet run SampleName` +/// Example: `dotnet run CreateConnection` +/// +/// The SampleRunner will automatically start a docker container with a Spanner emulator and execute +/// the sample on that emulator instance. No further setup or configuration is required. +/// +public static class SampleRunner +{ + private static readonly string SnippetsNamespace = typeof(CreateConnectionSample).Namespace!; + + static void Main(string[] args) + { + if (args.Length < 1) + { + Console.Error.WriteLine("Not enough arguments.\r\nUsage: dotnet run \r\nExample: dotnet run CreateConnection"); + PrintValidSampleNames(); + return; + } + var sampleName = args[0]; + if (sampleName.Equals("All")) + { + // Run all samples. This is used to test that all samples are runnable. + RunAllSamples(); + } + else + { + RunSample(sampleName, false); + } + } + + + private static void RunAllSamples() + { + var sampleClasses = GetSampleClasses(); + foreach (var sample in sampleClasses) + { + RunSample(sample.Name, true); + } + } + + internal static void RunSample(string sampleName, bool failOnException) + { + if (sampleName.EndsWith("Sample")) + { + sampleName = sampleName.Substring(0, sampleName.Length - "Sample".Length); + } + try + { + var sampleMethod = GetSampleMethod(sampleName); + if (sampleMethod != null) + { + Console.WriteLine($"Running sample {sampleName}"); + RunSampleAsync((connectionString) => (Task)sampleMethod.Invoke(null, [connectionString])!).WaitWithUnwrappedExceptions(); + } + } + catch (Exception e) + { + Console.WriteLine($"Running sample failed: {e.Message}\n{e.StackTrace}"); + if (failOnException) + { + throw; + } + } + } + + private static async Task RunSampleAsync(Func sampleMethod) + { + var emulatorRunner = new EmulatorRunner(); + var startedEmulator = false; + try + { + Console.WriteLine(""); + PortBinding? portBinding = null; + if (EmulatorRunner.IsEmulatorRunning()) + { + Console.WriteLine("Emulator is already running. Re-using existing Emulator instance..."); + Console.WriteLine(""); + } + else + { + Console.WriteLine("Starting emulator..."); + portBinding = await emulatorRunner.StartEmulator(); + Console.WriteLine($"Emulator started on port {portBinding.HostPort}"); + Console.WriteLine(""); + startedEmulator = true; + } + + var projectId = "sample-project"; + var instanceId = "sample-instance"; + var databaseId = "sample-database"; + DatabaseName databaseName = DatabaseName.FromProjectInstanceDatabase(projectId, instanceId, databaseId); + var connectionStringBuilder = new SpannerConnectionStringBuilder + { + DataSource = databaseName.ToString(), + AutoConfigEmulator = true, + }; + if (portBinding != null) + { + connectionStringBuilder.Host = portBinding.HostIP; + connectionStringBuilder.Port = uint.Parse(portBinding.HostPort); + } + + await ExecuteScript(connectionStringBuilder.ConnectionString, "create_sample_tables.sql"); + await ExecuteScript(connectionStringBuilder.ConnectionString, "insert_sample_data.sql"); + await sampleMethod.Invoke(connectionStringBuilder.ConnectionString); + } + catch (Exception e) + { + Console.WriteLine($"Running sample failed: {e.Message}"); + throw; + } + finally + { + if (startedEmulator) + { + Console.WriteLine(""); + Console.WriteLine("Stopping emulator..."); + emulatorRunner.StopEmulator().WaitWithUnwrappedExceptions(); + Console.WriteLine(""); + } + } + } + + private static MethodInfo? GetSampleMethod(string sampleName) + { + try + { + var sampleClass = System.Type.GetType($"{SnippetsNamespace}.{sampleName}Sample"); + if (sampleClass == null) + { + Console.Error.WriteLine($"Unknown sample name: {sampleName}"); + PrintValidSampleNames(); + return null; + } + var sampleMethod = sampleClass.GetMethod("Run"); + if (sampleMethod == null) + { + Console.Error.WriteLine($"{sampleName} is not a valid sample as it does not contain a Run method"); + PrintValidSampleNames(); + return null; + } + return sampleMethod; + } + catch (Exception e) + { + Console.Error.WriteLine($"Could not load sample {sampleName}. Please check that the sample name is a valid sample name.\r\nException: {e.Message}"); + PrintValidSampleNames(); + return null; + } + } + + private static async Task ExecuteScript(string connectionString, string file) + { + var codeBaseUrl = new Uri(Assembly.GetExecutingAssembly().Location); + var codeBasePath = Uri.UnescapeDataString(codeBaseUrl.AbsolutePath); + var dirPath = Path.GetDirectoryName(codeBasePath); + if (dirPath == null) + { + throw new DirectoryNotFoundException("Could not find the sample directory"); + } + var filePath = Path.Combine(dirPath, file); + var script = await File.ReadAllTextAsync(filePath); + var statements = script.Split(";").Where(statement => !string.IsNullOrWhiteSpace(statement)); + await ExecuteBatchAsync(connectionString, statements); + } + + private static async Task ExecuteBatchAsync(string connectionString, IEnumerable statements) + { + await using var connection = new SpannerConnection(connectionString); + await connection.OpenAsync(); + var batch = connection.CreateBatch(); + foreach (var statement in statements) + { + var cmd = batch.CreateBatchCommand(); + cmd.CommandText = statement; + batch.BatchCommands.Add(cmd); + } + await batch.ExecuteNonQueryAsync(); + } + + private static void PrintValidSampleNames() + { + var sampleClasses = GetSampleClasses(); + Console.Error.WriteLine(""); + Console.Error.WriteLine("Supported samples:"); + sampleClasses.ToList().ForEach(t => Console.Error.WriteLine($" * {t.Name}")); + } + + private static IEnumerable GetSampleClasses() + => from t in Assembly.GetExecutingAssembly().GetTypes() + where t.IsClass && t.Name.EndsWith("Sample") + select t; +} diff --git a/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/spanner-ado-net-getting-started-guide.csproj b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/spanner-ado-net-getting-started-guide.csproj new file mode 100644 index 00000000..305ca4d7 --- /dev/null +++ b/drivers/spanner-ado-net/spanner-ado-net-getting-started-guide/spanner-ado-net-getting-started-guide.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + Google.Cloud.Spanner.DataProvider + enable + enable + Google.Cloud.Spanner.DataProvider.GettingStartedGuide + default + + + + + + + + + + + diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples-tests/GettingStartedTests.cs b/drivers/spanner-ado-net/spanner-ado-net-samples-tests/GettingStartedTests.cs index be5d7af4..882a4a94 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples-tests/GettingStartedTests.cs +++ b/drivers/spanner-ado-net/spanner-ado-net-samples-tests/GettingStartedTests.cs @@ -15,7 +15,7 @@ using Docker.DotNet; using Docker.DotNet.Models; using Google.Cloud.Spanner.Admin.Database.V1; -using Google.Cloud.Spanner.DataProvider.Samples.GettingStartedGuide; +using Google.Cloud.Spanner.DataProvider.GettingStartedGuide; namespace Google.Cloud.Spanner.DataProvider.Samples.Tests; diff --git a/drivers/spanner-ado-net/spanner-ado-net-samples-tests/spanner-ado-net-samples-tests.csproj b/drivers/spanner-ado-net/spanner-ado-net-samples-tests/spanner-ado-net-samples-tests.csproj index ed386981..e6b8a9e6 100644 --- a/drivers/spanner-ado-net/spanner-ado-net-samples-tests/spanner-ado-net-samples-tests.csproj +++ b/drivers/spanner-ado-net/spanner-ado-net-samples-tests/spanner-ado-net-samples-tests.csproj @@ -30,6 +30,7 @@ + diff --git a/drivers/spanner-ado-net/spanner-ado-net.sln b/drivers/spanner-ado-net/spanner-ado-net.sln index 315ab09c..e24da279 100644 --- a/drivers/spanner-ado-net/spanner-ado-net.sln +++ b/drivers/spanner-ado-net/spanner-ado-net.sln @@ -22,6 +22,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spannerlib-dotnet-grpc-v1", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spanner-ado-net-samples-tests", "spanner-ado-net-samples-tests\spanner-ado-net-samples-tests.csproj", "{17F5C0B7-7932-4536-9328-5BBB0EC02A3B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spanner-ado-net-getting-started-guide", "spanner-ado-net-getting-started-guide\spanner-ado-net-getting-started-guide.csproj", "{D18C11BF-2841-4FC5-B1CA-F3E4EDDEA279}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -72,5 +74,9 @@ Global {17F5C0B7-7932-4536-9328-5BBB0EC02A3B}.Debug|Any CPU.Build.0 = Debug|Any CPU {17F5C0B7-7932-4536-9328-5BBB0EC02A3B}.Release|Any CPU.ActiveCfg = Release|Any CPU {17F5C0B7-7932-4536-9328-5BBB0EC02A3B}.Release|Any CPU.Build.0 = Release|Any CPU + {D18C11BF-2841-4FC5-B1CA-F3E4EDDEA279}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D18C11BF-2841-4FC5-B1CA-F3E4EDDEA279}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D18C11BF-2841-4FC5-B1CA-F3E4EDDEA279}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D18C11BF-2841-4FC5-B1CA-F3E4EDDEA279}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal