Repository: gcpug/handy-spanner Branch: master Commit: 71d5f2e28545 Files: 33 Total size: 852.9 KB Directory structure: gitextract_s4miq0z1/ ├── .circleci/ │ └── config.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd/ │ └── handy-spanner/ │ └── main.go ├── fake/ │ ├── example_test.go │ ├── integration_test.go │ ├── server.go │ └── testdata/ │ └── schema.sql ├── go.mod ├── go.sum └── server/ ├── database.go ├── database_function.go ├── database_json.go ├── database_nonjson.go ├── database_query_test.go ├── database_test.go ├── debug.go ├── keyset.go ├── meta_schema.go ├── query.go ├── query_test.go ├── server.go ├── server_test.go ├── server_transaction_test.go ├── session.go ├── spanner_error.go ├── table.go ├── transaction.go ├── value.go └── value_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .circleci/config.yml ================================================ version: 2 jobs: test: docker: - image: golang:1.23-bookworm environment: GO111MODULE: "on" working_directory: /go/src/handy-spanner steps: - checkout - restore_cache: key: go-mod-{{ checksum "go.sum" }} - run: name: Install dependencies command: | if [ ! -d 'vendor' ]; then go mod download fi - run: name: build command: | make build - run: name: run tests command: | make test workflows: version: 2 build-workflow: jobs: - test ================================================ FILE: .gitignore ================================================ /handy-spanner /vendor/ ================================================ FILE: Dockerfile ================================================ FROM golang:1.18-alpine3.16 AS builder RUN set -eux \ && apk --no-cache add \ g++ \ gcc \ git \ make \ musl-dev COPY . /go/src/handy-spanner WORKDIR /go/src/handy-spanner RUN make build FROM alpine:3.16.1 COPY --from=builder /go/src/handy-spanner/handy-spanner /usr/local/bin/handy-spanner RUN apk --no-cache add \ ca-certificates tzdata USER nobody EXPOSE 9999 ENTRYPOINT ["/usr/local/bin/handy-spanner"] ================================================ FILE: LICENSE ================================================ 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 2019 Masahiro Sano 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. ================================================ FILE: Makefile ================================================ all: test build export GO111MODULE=on .PHONY: build build: go build -v --tags "json1" ./cmd/handy-spanner .PHONY: test test: go test -v --tags "json1" -race ./... docker-build: docker build . -t handy-spanner ================================================ FILE: README.md ================================================ # An emulator for Cloud Spanner ![Status: Maintenance](https://img.shields.io/badge/status-maintenance-yellow.svg) ## ⚠️ Maintenance Mode > **Note:** > Active development of `handy-spanner` has ended due to the release of the [official Google Cloud Spanner Emulator](https://cloud.google.com/spanner/docs/emulator). > > This project is currently in **maintenance mode**. No new features will be added. We will only perform essential maintenance, such as updating dependencies. --- ## Install ``` go get github.com/gcpug/handy-spanner/cmd/handy-spanner ``` NOTE: If you want to use some features (e.g. array literal `[]` in DML), require "json1" build tag. The Spanner emulator uses sqlite3 internally. You may need to build go-sqlite3 explicitly. It also requires cgo to use sqlite3. ``` go get -u github.com/mattn/go-sqlite3 go install github.com/mattn/go-sqlite3 ``` ## Usage ### Run as an independent process ``` ./handy-spanner ``` or ``` docker run --rm -it -p 9999:9999 handy-spanner ``` It runs a hand-spanner server as a process. It serves spanner gRPC server by port 9999 by default. #### Access to the server The google-cloud-go, the official Spanner SDK, supports to access an emulator server. Set the address to an emulator server to environment variable `SPANNER_EMULATOR_HOST`, then google-cloud-go transparently use the server in the client. So if you want to replace spanner server with the handy-spanner you run, just do: ``` export SPANNER_EMULATOR_HOST=localhost:9999 ``` #### Schema setup The handy-spanner server has no databases nor tables by default. You need to create them by yourself. You can prepare them at startup. handy-spanner accepts some command-line arguments to prepare schema. * `schema`: Path to a DDL file which handy-spanner creates at startup * `project`: The project the DDL is applied. * `instance`: The instance the DDL is applied. * `database`: The database the DDL is applied. If you prepare schema at startup, these 4 arguments are required. #### Implicit creation of databases Without schema setup, databases are created automatically by your database access. handy-spanner creates a database when a session to the database is created. In detail, when `CreateSession` or `BatchCreateSessions` is called, the accessed database is created. #### Database operations You can also operate databases by your applications. If you need additional databases or database alterations, you need to follow this section. As Cloud Spanner supports, there are dedicated gRPC service for instance and database operations for Spanner. * Instance operations * [Document](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1) * [Protobuf](https://github.com/googleapis/googleapis/blob/master/google/spanner/admin/instance/v1/spanner_instance_admin.proto) * Database operations * [Document](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.database.v1) * [Protobuf](https://github.com/googleapis/googleapis/blob/master/google/spanner/admin/database/v1/spanner_database_admin.proto) handy-spanner also supports these gRPC services so that you can operate databases. The gRPC services are provided in the same address to the normal gRPC service. It means it is provided in `localhost:9999` by default. It seems most clients for each language for these gRPC services provided Google Cloud Platform dont support `SPANNER_EMULATOR_HOST` to connect an emulator server, so you need to setup a client to connect an emulator server explicitly. For Go, you can refer [examples](./fake/example_test.go) to connect an emulator server. ### Run as a buillt-in server in Go If you use a handy-spanner server in tests in Go, it's easier to run it in a process. See an [example](https://github.com/gcpug/handy-spanner/blob/master/fake/example_test.go) for the details. Note that the tests highly depend on handhy-spanner, which means you cannot switch the backend depending on the situation. If you want to test on both Cloud Spanner and handy-spanner, it's better to use a handy-spanner server as an independent process. #### Database operations handy-spanner provides utility functions to operate databases. For the detail, please refer [examples](./fake/example_test.go). * `ParseAndApplyDDL` * You can pass `io.Reader` as a schema file to apply DDL to a database. * `ApplyDDL` * You can pass `ast.DDL` as an already parsed definition to apply DDL to a database. You can also operate databases or instances as Cloud Spanner supports. Please also refer _Run as an independent process_ section. ## Can and Cannot ### Supported features * Read * Keys and KeyRange as KeySet * Secondary index * STORING columns for secondary index * Respect column orders for index * Query * Select result set by column name and * * Most operators in WHERE clause: IN, BETWEEN, IS NULL * Conditions in WHERE clause: =, !=, >, <, AND, OR * Order By keyword with ASC, DESC * Group By and Having statement * LIMIT OFFSET * SELECT alias * Query Parameters * Literals * JOINs: COMMMA, CROSS, INNER, LEFT/RIGHT/FULL OUTER * FULL OUTER has some limitations * Subquery * SET operations: UNION, INTERSECT, EXCEPT * UNNEST: IN UNNEST, FROM UNNEST * Functions (partially) * Arithmetic operations * Mutation * All mutation types: Insert, Update, InsertOrUpdate, Replace, Delete * Commit timestamp * Transaction * Isolation level: SERIALIZABLE * DML * Insert, Update, Delete * Batch DML * DDL * CreateTable, CreateIndex only * Respect INTERLEAVE * Data Types * Int, Float, String, Bool, Byte, Date, Timestamp, Array, Struct * [Information Schema](https://cloud.google.com/spanner/docs/information-schema) * partially supported ### Not supported features * Transaction * Timestamp bound read * Query * Strict type checking * More functions * Partionan Query * EXCEPT ALL and INTERSECT ALL (because of sqlite) * FULL OUTER JOIN * Not support table alias. Use `*` for now * Not support ON condition. Use USING condition for now * Merging INT64 and FLOAT64 in SET operations * Array operations * DDL * Alter Table, Drop Table, Drop Index * Database management * Long running operations * Replace * wrong behavior on conflict * [Change Stream Schema](https://cloud.google.com/spanner/docs/change-streams) * only can parse in [Database operations](#Database-operations) with `schema` flag. ## Implementation ### Transaction simulation handy-spanner uses sqlite3 in [Shared-Cache Mode](https://www.sqlite.org/sharedcache.html). There is a characteristic in the trasactions. * Only one transaction can hold write lock per database to write database tables. * Other transactions still can hold read lock. * Write transaction holds write lock against database tables while writing the tables. * Other read transactions cannot read the table while locked * Read transaction holds read lock against database tables while reading the tables. * Other read transactions can read the table by holding read lock * Write transaction cannot write the table while read-locked If we simply use the transactions, dead lock should happen in read and write locks. To simulate spanner transactions correctly as possible, handy-spanner manages sqlite3 transactions inside. * Each spanner transaction starts own sqlite3 transaction. * Only one spanner transaction can hold write lock per database. * While a transaction holds write lock, other spanner transactions cannot newly get read or write lock. * When write transaction tries to write a table, it forces transactions that hold read lock to the table to release the lock. * The transactions become "aborted" * The aborted transactions are expected to be retried by the client. ![abort](img/abort1.png) ![abort](img/abort2.png) ### DML Because of transaction limitations, DML also has limitations. When a transaction(A) updates a table, other transactions cannot read/write the table until the transaction(A) commits. This limitation may become an inconsistency to the Cloud Spanner. Other limitations are same to mutations with commit. ![block](img/read_block.png) ## Feature Request and Bug Report Feature requests and any bug reports are welcome. There are many things to implement features and make better compatibility to Cloud Spanner. Now I'm prioritizing features by on-demand from users and me. Please create an issue if you find lack of a feature to use handy-spanner or imcompatibility to Cloud Spanner. #### Feature request Please describe what feature you want. It's the best to refer a Cloud Spanner's document for the feature. If the feature is partiality implemented or wrong behavior you expected, please also see a Bug Report section. #### Bug report Please describe a reproducable conditions. If you describe more information about the bug, it makes more easir to fix the bug. * Schema * Initial Data * Query or any requests you run * Actual behavior * Expected behavior * Document in Cloud Spanner ## Copyright * Author: Masahiro Sano ([@kazegusuri](https://github.com/kazegusuri)) * Copyright: 2019 Masahiro Sano * License: Apache License, Version 2.0 ================================================ FILE: cmd/handy-spanner/main.go ================================================ // Copyright 2019 Masahiro Sano // // 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 main import ( "context" "flag" "fmt" "log" "net" "os" "os/signal" "syscall" "github.com/gcpug/handy-spanner/fake" "golang.org/x/sync/errgroup" ) var ( addr = flag.String("addr", "0.0.0.0:9999", "address to listen") project = flag.String("project", "fake", "project to apply DDL at startup") database = flag.String("database", "fake", "database to apply DDL at startup") instance = flag.String("instance", "fake", "instance to apply DDL at startup") schema = flag.String("schema", "", "path to a DDL file") ) func main() { flag.CommandLine.SetOutput(os.Stdout) flag.Parse() if err := runMain(); err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) os.Exit(1) } } func runMain() error { if *addr == "" { return fmt.Errorf("addr must be specified") } var file *os.File if *schema != "" { f, err := os.Open(*schema) if err != nil { return fmt.Errorf("failed to open schema file: %v", err) } file = f defer file.Close() } // root context notifies server shutdown by SIGINT or SIGTERM ctx, cancel := context.WithCancel(context.Background()) defer cancel() lis, err := net.Listen("tcp", *addr) if err != nil { return err } server, err := fake.New(lis) if err != nil { return fmt.Errorf("failed to setup fake server: %v", err) } wg, ctx := errgroup.WithContext(ctx) wg.Go(func() error { return server.Start() }) if file != nil { dbName := fmt.Sprintf("projects/%s/instances/%s/databases/%s", *project, *instance, *database) if err := server.ParseAndApplyDDL(ctx, dbName, file); err != nil { server.Stop() return fmt.Errorf("failed apply DDL: %v", err) } } log.Print("spanner server is ready") // Waiting for SIGTERM or Interrupt signal sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGTERM, os.Interrupt) select { case <-sigCh: log.Print("received SIGTERM, exiting server") case <-ctx.Done(): } // stop server server.Stop() return wg.Wait() } ================================================ FILE: fake/example_test.go ================================================ // Copyright 2019 Masahiro Sano // // 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 fake_test import ( "context" "log" "strings" "cloud.google.com/go/spanner" admindatabasev1 "cloud.google.com/go/spanner/admin/database/apiv1" databasepb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" "github.com/gcpug/handy-spanner/fake" "google.golang.org/api/option" ) func Example_spannerClient() { ctx := context.Background() dbName := "projects/fake/instances/fake/databases/fake" // Run fake server srv, conn, err := fake.Run() if err != nil { log.Fatal(err) } defer srv.Stop() defer conn.Close() // Prepare spanner client client, err := spanner.NewClient(ctx, dbName, option.WithGRPCConn(conn)) if err != nil { log.Fatalf("failed to connect fake spanner server: %v", err) } // Use the client _, _ = client.Apply(ctx, []*spanner.Mutation{ spanner.Insert("Test", []string{"ColA", "ColB"}, []interface{}{"foo", 100}, ), }) // output: } func Example_adminClient() { ctx := context.Background() dbName := "projects/fake/instances/fake/databases/fake" stmts := []string{ `CREATE TABLE Table1 ( Id STRING(MAX) NOT NULL ) PRIMARY KEY (Id)`, `CREATE TABLE Table2 ( Id TIMESTAMP NOT NULL ) PRIMARY KEY (Id)`, } // Run fake server srv, conn, err := fake.Run() if err != nil { log.Fatal(err) } defer srv.Stop() // Prepare spanner client adminclient, err := admindatabasev1.NewDatabaseAdminClient(ctx, option.WithGRPCConn(conn)) if err != nil { log.Fatalf("failed to connect fake spanner server: %v", err) } // Use the client _, err = adminclient.UpdateDatabaseDdl(ctx, &databasepb.UpdateDatabaseDdlRequest{ Database: dbName, Statements: stmts, }) if err != nil { log.Fatal(err) } // output: } func Example_applyDDL() { ctx := context.Background() dbName := "projects/fake/instances/fake/databases/fake" schema := ` CREATE TABLE Table1 ( Id STRING(MAX) NOT NULL ) PRIMARY KEY (Id); CREATE TABLE Table2 ( Id TIMESTAMP NOT NULL ) PRIMARY KEY (Id); ` // Run fake server srv, _, err := fake.Run() if err != nil { log.Fatal(err) } defer srv.Stop() err = srv.ParseAndApplyDDL(ctx, dbName, strings.NewReader(schema)) if err != nil { log.Fatal(err) } // output: } ================================================ FILE: fake/integration_test.go ================================================ // Copyright 2019 Masahiro Sano // // 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 fake import ( "context" "fmt" "io/ioutil" "os" "reflect" "regexp" "strconv" "strings" "sync" "testing" "time" "cloud.google.com/go/civil" "cloud.google.com/go/spanner" admindatabasev1 "cloud.google.com/go/spanner/admin/database/apiv1" databasepb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" cmp "github.com/google/go-cmp/cmp" "google.golang.org/api/option" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) type FullType struct { PKey string `spanner:"PKey" json:"PKey"` // PKey FTString string `spanner:"FTString" json:"FTString"` // FTString FTStringNull spanner.NullString `spanner:"FTStringNull" json:"FTStringNull"` // FTStringNull FTBool bool `spanner:"FTBool" json:"FTBool"` // FTBool FTBoolNull spanner.NullBool `spanner:"FTBoolNull" json:"FTBoolNull"` // FTBoolNull FTBytes []byte `spanner:"FTBytes" json:"FTBytes"` // FTBytes FTBytesNull []byte `spanner:"FTBytesNull" json:"FTBytesNull"` // FTBytesNull FTTimestamp time.Time `spanner:"FTTimestamp" json:"FTTimestamp"` // FTTimestamp FTTimestampNull spanner.NullTime `spanner:"FTTimestampNull" json:"FTTimestampNull"` // FTTimestampNull FTInt int64 `spanner:"FTInt" json:"FTInt"` // FTInt FTIntNull spanner.NullInt64 `spanner:"FTIntNull" json:"FTIntNull"` // FTIntNull FTFloat float64 `spanner:"FTFloat" json:"FTFloat"` // FTFloat FTFloatNull spanner.NullFloat64 `spanner:"FTFloatNull" json:"FTFloatNull"` // FTFloatNull FTDate civil.Date `spanner:"FTDate" json:"FTDate"` // FTDate FTDateNull spanner.NullDate `spanner:"FTDateNull" json:"FTDateNull"` // FTDateNull } var fullTypesKeys = []string{ "PKey", "FTString", "FTStringNull", "FTBool", "FTBoolNull", "FTBytes", "FTBytesNull", "FTTimestamp", "FTTimestampNull", "FTInt", "FTIntNull", "FTFloat", "FTFloatNull", "FTDate", "FTDateNull", } type Simple struct { ID int64 `spanner:"Id"` Value string `spanner:"Value"` } func prepareSimpleData(t *testing.T, ctx context.Context, client *spanner.Client) { _, err := client.Apply(ctx, []*spanner.Mutation{ spanner.Insert("Simple", []string{"Id", "Value"}, []interface{}{100, "xxx0"}, ), spanner.Insert("Simple", []string{"Id", "Value"}, []interface{}{101, "xxx1"}, ), spanner.Insert("Simple", []string{"Id", "Value"}, []interface{}{102, "xxx2"}, ), spanner.Insert("Simple", []string{"Id", "Value"}, []interface{}{200, "yyy"}, ), spanner.Insert("Simple", []string{"Id", "Value"}, []interface{}{300, "zzz"}, ), spanner.Insert("Simple", []string{"Id", "Value"}, []interface{}{400, "zzz"}, ), }) if err != nil { t.Fatalf("Applying mutations: %v", err) } } func prepareFullTypeData(t *testing.T, ctx context.Context, client *spanner.Client, now time.Time) { _, err := client.Apply(ctx, []*spanner.Mutation{ spanner.Insert("FullTypes", fullTypesKeys, []interface{}{ "pkey0", "xxx0", spanner.NullString{}, true, spanner.NullBool{}, []byte("xxx0"), []byte("xxx0"), now, spanner.NullTime{}, 100, spanner.NullInt64{}, 0.123, spanner.NullFloat64{}, civil.DateOf(now), spanner.NullDate{}, }, ), spanner.Insert("FullTypes", fullTypesKeys, []interface{}{ "pkey1", "xxx1", spanner.NullString{}, true, spanner.NullBool{}, []byte("xxx1"), []byte("xxx1"), now.Add(time.Second), spanner.NullTime{}, 100, spanner.NullInt64{}, 0.123, spanner.NullFloat64{}, civil.DateOf(now), spanner.NullDate{}, }, ), spanner.Insert("FullTypes", fullTypesKeys, []interface{}{ "pkey2", "xxx2", spanner.NullString{}, true, spanner.NullBool{}, []byte("xxx2"), []byte("xxx2"), now.Add(2 * time.Second), spanner.NullTime{}, 100, spanner.NullInt64{}, 0.123, spanner.NullFloat64{}, civil.DateOf(now), spanner.NullDate{}, }, ), spanner.Insert("FullTypes", fullTypesKeys, []interface{}{ "pkey3", "yyy3", spanner.NullString{}, true, spanner.NullBool{}, []byte("yyy3"), []byte("yyy3"), now.Add(3 * time.Second), spanner.NullTime{}, 200, spanner.NullInt64{}, 0.123, spanner.NullFloat64{}, civil.DateOf(now), spanner.NullDate{}, }, ), }) if err != nil { t.Fatalf("Applying mutations: %v", err) } } func TestIntegration_ReadWrite(t *testing.T) { ctx := context.Background() dbName := "projects/fake/instances/fake/databases/fake" f, err := os.Open("./testdata/schema.sql") if err != nil { t.Fatalf("err %v", err) } srv, conn, err := Run() if err != nil { t.Fatalf("err %v", err) } defer srv.Stop() if err := srv.ParseAndApplyDDL(ctx, dbName, f); err != nil { t.Fatal(err) } client, err := spanner.NewClient(ctx, dbName, option.WithGRPCConn(conn)) if err != nil { t.Fatalf("Connecting to in-memory fake: %v", err) } now := time.Now() date := civil.DateOf(now) table := []struct { expected FullType }{ { expected: FullType{ PKey: "pkey1", FTString: "xxx1", FTStringNull: spanner.NullString{ StringVal: "xxx1", Valid: true, }, FTBool: true, FTBoolNull: spanner.NullBool{ Bool: true, Valid: true, }, FTBytes: []byte("xxx1"), FTBytesNull: []byte("xxx2"), FTTimestamp: now, FTTimestampNull: spanner.NullTime{ Time: now, Valid: true, }, FTInt: 101, FTIntNull: spanner.NullInt64{ Int64: 101, Valid: true, }, FTFloat: 0.123, FTFloatNull: spanner.NullFloat64{ Float64: 0.123, Valid: true, }, FTDate: date, FTDateNull: spanner.NullDate{ Date: date, Valid: true, }, }, }, { expected: FullType{ PKey: "pkey1", FTString: "xxx1", FTStringNull: spanner.NullString{}, FTBool: true, FTBoolNull: spanner.NullBool{}, FTBytes: []byte("xxx1"), FTBytesNull: []byte("xxx2"), FTTimestamp: now, FTTimestampNull: spanner.NullTime{}, FTInt: 101, FTIntNull: spanner.NullInt64{}, FTFloat: 0.123, FTFloatNull: spanner.NullFloat64{}, FTDate: date, FTDateNull: spanner.NullDate{}, }, }, } for _, tc := range table { _, err = client.Apply(ctx, []*spanner.Mutation{ spanner.Insert("FullTypes", fullTypesKeys, []interface{}{ tc.expected.PKey, tc.expected.FTString, tc.expected.FTStringNull, tc.expected.FTBool, tc.expected.FTBoolNull, tc.expected.FTBytes, tc.expected.FTBytesNull, tc.expected.FTTimestamp, tc.expected.FTTimestampNull, tc.expected.FTInt, tc.expected.FTIntNull, tc.expected.FTFloat, tc.expected.FTFloatNull, tc.expected.FTDate, tc.expected.FTDateNull, }), }) if err != nil { t.Fatalf("Applying mutations: %v", err) } var results []*FullType rows := client.Single().Read(ctx, "FullTypes", spanner.AllKeys(), fullTypesKeys) err = rows.Do(func(row *spanner.Row) error { var ft FullType if err := row.ToStruct(&ft); err != nil { return err } results = append(results, &ft) return nil }) if err != nil { t.Fatalf("Iterating over all row read: %v", err) } if len(results) != 1 { t.Fatalf("rows should be 1 but got %v row", len(results)) } if diff := cmp.Diff(&tc.expected, results[0]); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } _, err = client.Apply(ctx, []*spanner.Mutation{ spanner.Delete("FullTypes", spanner.AllKeys()), }) if err != nil { t.Fatalf("delete failed: %v", err) } } } func TestIntegration_ReadWrite_AtomicCount(t *testing.T) { ctx := context.Background() dbName := "projects/fake/instances/fake/databases/fake" f, err := os.Open("./testdata/schema.sql") if err != nil { t.Fatalf("err %v", err) } srv, conn, err := Run() if err != nil { t.Fatalf("err %v", err) } defer srv.Stop() if err := srv.ParseAndApplyDDL(ctx, dbName, f); err != nil { t.Fatal(err) } client, err := spanner.NewClient(ctx, dbName, option.WithGRPCConn(conn)) if err != nil { t.Fatalf("Connecting to in-memory fake: %v", err) } if _, err := client.Apply(ctx, []*spanner.Mutation{ spanner.Insert("Simple", []string{"Id", "Value"}, []interface{}{100, "100"}, ), }); err != nil { t.Fatalf("failed to apply: %v", err) } errCh := make(chan error, 10) wg := &sync.WaitGroup{} for i := 0; i < 10; i++ { wg.Add(1) go func(me int) { defer wg.Done() for j := 0; j < 10; j++ { _, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, tx *spanner.ReadWriteTransaction) error { var num int it := tx.Read(ctx, "Simple", spanner.Key([]interface{}{100}), []string{"Value"}) err := it.Do(func(r *spanner.Row) error { var value string if err := r.Column(0, &value); err != nil { return err } num, _ = strconv.Atoi(value) return nil }) if err != nil { return err } next := num + 1 return tx.BufferWrite([]*spanner.Mutation{ spanner.Update("Simple", []string{"Id", "Value"}, []interface{}{100, fmt.Sprint(next)}, ), }) }) if err != nil { errCh <- fmt.Errorf("Transaction failed: %v", err) return } } }(i) } wg.Wait() var errs []error L: for { select { case err := <-errCh: errs = append(errs, err) default: break L } } if len(errs) != 0 { for _, err := range errs { t.Error(err) } t.FailNow() } var num int it := client.Single().Read(ctx, "Simple", spanner.Key([]interface{}{100}), []string{"Value"}) err = it.Do(func(r *spanner.Row) error { var value string if err := r.Column(0, &value); err != nil { return err } num, _ = strconv.Atoi(value) return nil }) if err != nil { t.Fatalf("Read failed: %v", err) } expected := 200 if num != expected { t.Fatalf("expect num to be %v, but got %v", expected, num) } } func TestIntegration_Read_KeySet(t *testing.T) { ctx := context.Background() dbName := "projects/fake/instances/fake/databases/fake" f, err := os.Open("./testdata/schema.sql") if err != nil { t.Fatalf("err %v", err) } srv, conn, err := Run() if err != nil { t.Fatalf("err %v", err) } defer srv.Stop() if err := srv.ParseAndApplyDDL(ctx, dbName, f); err != nil { t.Fatal(err) } client, err := spanner.NewClient(ctx, dbName, option.WithGRPCConn(conn)) if err != nil { t.Fatalf("Connecting to in-memory fake: %v", err) } prepareSimpleData(t, ctx, client) table := map[string]struct { keyset spanner.KeySet expected []*Simple }{ "SingleKey": { keyset: spanner.Key([]interface{}{100}), expected: []*Simple{ {ID: 100, Value: "xxx0"}, }, }, "MultipleKeys": { keyset: spanner.KeySets( spanner.Key([]interface{}{100}), spanner.Key([]interface{}{101}), spanner.Key([]interface{}{102}), ), expected: []*Simple{ {ID: 100, Value: "xxx0"}, {ID: 101, Value: "xxx1"}, {ID: 102, Value: "xxx2"}, }, }, "SingleRange_CloseClose": { keyset: spanner.KeyRange{ Start: spanner.Key([]interface{}{100}), End: spanner.Key([]interface{}{102}), Kind: spanner.ClosedClosed, }, expected: []*Simple{ {ID: 100, Value: "xxx0"}, {ID: 101, Value: "xxx1"}, {ID: 102, Value: "xxx2"}, }, }, "SingleRange_OpenClose": { keyset: spanner.KeyRange{ Start: spanner.Key([]interface{}{100}), End: spanner.Key([]interface{}{102}), Kind: spanner.OpenClosed, }, expected: []*Simple{ {ID: 101, Value: "xxx1"}, {ID: 102, Value: "xxx2"}, }, }, "SingleRange_CloseOpen": { keyset: spanner.KeyRange{ Start: spanner.Key([]interface{}{100}), End: spanner.Key([]interface{}{102}), Kind: spanner.ClosedOpen, }, expected: []*Simple{ {ID: 100, Value: "xxx0"}, {ID: 101, Value: "xxx1"}, }, }, "SingleRange_OpenOpen": { keyset: spanner.KeyRange{ Start: spanner.Key([]interface{}{100}), End: spanner.Key([]interface{}{102}), Kind: spanner.OpenOpen, }, expected: []*Simple{ {ID: 101, Value: "xxx1"}, }, }, "SingleMultiRange": { keyset: spanner.KeySets( spanner.KeyRange{ Start: spanner.Key([]interface{}{100}), End: spanner.Key([]interface{}{103}), Kind: spanner.ClosedClosed, }, spanner.KeyRange{ Start: spanner.Key([]interface{}{200}), End: spanner.Key([]interface{}{300}), Kind: spanner.ClosedOpen, }, ), expected: []*Simple{ {ID: 100, Value: "xxx0"}, {ID: 101, Value: "xxx1"}, {ID: 102, Value: "xxx2"}, {ID: 200, Value: "yyy"}, }, }, } for name, tc := range table { t.Run(name, func(t *testing.T) { var results []*Simple rows := client.Single().Read(ctx, "Simple", tc.keyset, []string{"Id", "Value"}) err = rows.Do(func(row *spanner.Row) error { var s Simple if err := row.ToStruct(&s); err != nil { return err } results = append(results, &s) return nil }) if err != nil { t.Fatalf("Iterating over all row read: %v", err) } if diff := cmp.Diff(tc.expected, results); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) } } func TestIntegration_Read_KeySet2(t *testing.T) { ctx := context.Background() dbName := "projects/fake/instances/fake/databases/fake" f, err := os.Open("./testdata/schema.sql") if err != nil { t.Fatalf("err %v", err) } srv, conn, err := Run() if err != nil { t.Fatalf("err %v", err) } defer srv.Stop() if err := srv.ParseAndApplyDDL(ctx, dbName, f); err != nil { t.Fatal(err) } client, err := spanner.NewClient(ctx, dbName, option.WithGRPCConn(conn)) if err != nil { t.Fatalf("Connecting to in-memory fake: %v", err) } now := time.Now().UTC() prepareFullTypeData(t, ctx, client, now) table := map[string]struct { index string keyset spanner.KeySet expected []*FullType }{ "TimestampAsc_AsPrefix": { index: "FullTypesByIntTimestamp", keyset: spanner.Key{100}.AsPrefix(), expected: []*FullType{ {PKey: "pkey0", FTInt: 100, FTTimestamp: now}, {PKey: "pkey1", FTInt: 100, FTTimestamp: now.Add(time.Second)}, {PKey: "pkey2", FTInt: 100, FTTimestamp: now.Add(2 * time.Second)}, }, }, "TimestamAsc_Range": { index: "FullTypesByIntTimestamp", keyset: &spanner.KeyRange{ Start: spanner.Key{100, now}, End: spanner.Key{100}, Kind: spanner.ClosedOpen, }, expected: []*FullType{ {PKey: "pkey0", FTInt: 100, FTTimestamp: now}, {PKey: "pkey1", FTInt: 100, FTTimestamp: now.Add(time.Second)}, {PKey: "pkey2", FTInt: 100, FTTimestamp: now.Add(2 * time.Second)}, }, }, "TimestamAsc_Range2": { index: "FullTypesByIntTimestamp", keyset: &spanner.KeyRange{ Start: spanner.Key{100, time.Time{}}, End: spanner.Key{100, now.Add(2 * time.Second)}, Kind: spanner.ClosedOpen, }, expected: []*FullType{ {PKey: "pkey0", FTInt: 100, FTTimestamp: now}, {PKey: "pkey1", FTInt: 100, FTTimestamp: now.Add(time.Second)}, }, }, "TimestampDesc_AsPrefix": { index: "FullTypesByIntTimestampReverse", keyset: spanner.Key{100}.AsPrefix(), expected: []*FullType{ {PKey: "pkey2", FTInt: 100, FTTimestamp: now.Add(2 * time.Second)}, {PKey: "pkey1", FTInt: 100, FTTimestamp: now.Add(time.Second)}, {PKey: "pkey0", FTInt: 100, FTTimestamp: now}, }, }, "TimestampDesc_Range1": { index: "FullTypesByIntTimestampReverse", keyset: &spanner.KeyRange{ Start: spanner.Key{100, now.Add(time.Second)}, End: spanner.Key{100}, Kind: spanner.ClosedOpen, }, expected: []*FullType{ {PKey: "pkey1", FTInt: 100, FTTimestamp: now.Add(time.Second)}, {PKey: "pkey0", FTInt: 100, FTTimestamp: now}, }, }, "TimestampDesc_Range2": { index: "FullTypesByIntTimestampReverse", keyset: &spanner.KeyRange{ Start: spanner.Key{100, now.Add(time.Second)}, End: spanner.Key{100, time.Time{}}, Kind: spanner.ClosedOpen, }, expected: []*FullType{ {PKey: "pkey1", FTInt: 100, FTTimestamp: now.Add(time.Second)}, {PKey: "pkey0", FTInt: 100, FTTimestamp: now}, }, }, "TimestampDesc_Range3": { index: "FullTypesByIntTimestampReverse", keyset: &spanner.KeyRange{ Start: spanner.Key{100, now.Add(time.Second)}, End: spanner.Key{100, time.Unix(1, 0)}, Kind: spanner.ClosedOpen, }, expected: []*FullType{ {PKey: "pkey1", FTInt: 100, FTTimestamp: now.Add(time.Second)}, {PKey: "pkey0", FTInt: 100, FTTimestamp: now}, }, }, } for name, tc := range table { t.Run(name, func(t *testing.T) { var results []*FullType rows := client.Single().ReadUsingIndex(ctx, "FullTypes", tc.index, tc.keyset, []string{"PKey", "FTInt", "FTTimestamp"}) err = rows.Do(func(row *spanner.Row) error { var ft FullType if err := row.ToStruct(&ft); err != nil { return err } results = append(results, &ft) return nil }) if err != nil { t.Fatalf("Iterating over all row read: %v", err) } if diff := cmp.Diff(tc.expected, results); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) } } func TestIntegration_ReadWrite_Update(t *testing.T) { ctx := context.Background() dbName := "projects/fake/instances/fake/databases/fake" f, err := os.Open("./testdata/schema.sql") if err != nil { t.Fatalf("err %v", err) } srv, conn, err := Run() if err != nil { t.Fatalf("err %v", err) } defer srv.Stop() if err := srv.ParseAndApplyDDL(ctx, dbName, f); err != nil { t.Fatal(err) } client, err := spanner.NewClient(ctx, dbName, option.WithGRPCConn(conn)) if err != nil { t.Fatalf("Connecting to in-memory fake: %v", err) } read := func(rows *spanner.RowIterator) ([]*Simple, error) { var results []*Simple err = rows.Do(func(row *spanner.Row) error { var s Simple if err := row.ToStruct(&s); err != nil { return err } results = append(results, &s) return nil }) if err != nil { return nil, fmt.Errorf("Iterating over all row read: %v", err) } return results, nil } expected1 := []*Simple{ {ID: 100, Value: "xxx"}, {ID: 101, Value: "yyy"}, } _, err = client.ReadWriteTransaction(ctx, func(ctx context.Context, tx *spanner.ReadWriteTransaction) error { stmt := spanner.NewStatement(`INSERT INTO Simple (Id, Value) VALUES(@id1, @value1), (@id2, @value2)`) stmt.Params = map[string]interface{}{ "id1": 100, "value1": "xxx", "id2": 101, "value2": "yyy", } if _, err := tx.Update(ctx, stmt); err != nil { return err } rows := tx.Read(ctx, "Simple", spanner.AllKeys(), []string{"Id", "Value"}) results, err := read(rows) if err != nil { return err } if diff := cmp.Diff(expected1, results); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } stmt2 := spanner.NewStatement(`UPDATE Simple SET Value = "200" WHERE Id = @id`) stmt2.Params = map[string]interface{}{ "id": 100, } if _, err := tx.Update(ctx, stmt2); err != nil { return err } stmt3 := spanner.NewStatement(`DELETE FROM Simple WHERE Id = @id`) stmt3.Params = map[string]interface{}{ "id": 101, } if _, err := tx.Update(ctx, stmt3); err != nil { return err } return nil }) if err != nil { t.Fatalf("ReadWriteTransaction: %v", err) } expected2 := []*Simple{ {ID: 100, Value: "200"}, } var results []*Simple rows := client.Single().Read(ctx, "Simple", spanner.AllKeys(), []string{"Id", "Value"}) results, err = read(rows) if err != nil { t.Fatalf("Read: %v", err) } if diff := cmp.Diff(expected2, results); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } } func TestIntegration_ReadWrite_BatchUpdate(t *testing.T) { ctx := context.Background() dbName := "projects/fake/instances/fake/databases/fake" f, err := os.Open("./testdata/schema.sql") if err != nil { t.Fatalf("err %v", err) } srv, conn, err := Run() if err != nil { t.Fatalf("err %v", err) } defer srv.Stop() if err := srv.ParseAndApplyDDL(ctx, dbName, f); err != nil { t.Fatal(err) } client, err := spanner.NewClient(ctx, dbName, option.WithGRPCConn(conn)) if err != nil { t.Fatalf("Connecting to in-memory fake: %v", err) } read := func(rows *spanner.RowIterator) ([]*Simple, error) { var results []*Simple err = rows.Do(func(row *spanner.Row) error { var s Simple if err := row.ToStruct(&s); err != nil { return err } results = append(results, &s) return nil }) if err != nil { return nil, fmt.Errorf("Iterating over all row read: %v", err) } return results, nil } expected1 := []*Simple{ {ID: 100, Value: "xxx"}, {ID: 101, Value: "yyy2"}, } _, err = client.ReadWriteTransaction(ctx, func(ctx context.Context, tx *spanner.ReadWriteTransaction) error { stmt := spanner.NewStatement(`INSERT INTO Simple (Id, Value) VALUES(@id1, @value1), (@id2, @value2)`) stmt.Params = map[string]interface{}{ "id1": 100, "value1": "xxx", "id2": 101, "value2": "yyy", } stmt2 := spanner.NewStatement(`UPDATE Simple SET Value = "yyy2" WHERE Id = 101`) affectedRows, err := tx.BatchUpdate(ctx, []spanner.Statement{stmt, stmt2}) if err != nil { return err } if diff := cmp.Diff([]int64{2, 1}, affectedRows); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } rows := tx.Read(ctx, "Simple", spanner.AllKeys(), []string{"Id", "Value"}) results, err := read(rows) if err != nil { return err } if diff := cmp.Diff(expected1, results); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } stmt3 := spanner.NewStatement(`UPDATE Simple SET Value = "200" WHERE Id = @id`) stmt3.Params = map[string]interface{}{ "id": 100, } stmt4 := spanner.NewStatement(`UPDAT`) affectedRows, err = tx.BatchUpdate(ctx, []spanner.Statement{stmt3, stmt4}) if err == nil { t.Errorf("unexpected success for batch update") } st := status.Convert(err) r := regexp.MustCompile(`Statement 1: .* is not valid DML`) if !r.MatchString(st.Message()) { t.Errorf("unexpected error message: %v", st.Message()) } if st.Code() != codes.InvalidArgument { t.Errorf("expect error code %v but got %v", codes.InvalidArgument, st.Code()) } if diff := cmp.Diff([]int64{1}, affectedRows); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } return nil }) if err != nil { t.Fatalf("ReadWriteTransaction: %v", err) } expected2 := []*Simple{ {ID: 100, Value: "200"}, {ID: 101, Value: "yyy2"}, } var results []*Simple rows := client.Single().Read(ctx, "Simple", spanner.AllKeys(), []string{"Id", "Value"}) results, err = read(rows) if err != nil { t.Fatalf("Read: %v", err) } if diff := cmp.Diff(expected2, results); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } } func TestIntegration_Query(t *testing.T) { ctx := context.Background() dbName := "projects/fake/instances/fake/databases/fake" f, err := os.Open("./testdata/schema.sql") if err != nil { t.Fatalf("err %v", err) } srv, conn, err := Run() if err != nil { t.Fatalf("err %v", err) } defer srv.Stop() if err := srv.ParseAndApplyDDL(ctx, dbName, f); err != nil { t.Fatal(err) } client, err := spanner.NewClient(ctx, dbName, option.WithGRPCConn(conn)) if err != nil { t.Fatalf("Connecting to in-memory fake: %v", err) } prepareSimpleData(t, ctx, client) table := []struct { sql string params map[string]interface{} expected []*Simple }{ { sql: "SELECT * FROM Simple", expected: []*Simple{ {ID: 100, Value: "xxx0"}, {ID: 101, Value: "xxx1"}, {ID: 102, Value: "xxx2"}, {ID: 200, Value: "yyy"}, {ID: 300, Value: "zzz"}, {ID: 400, Value: "zzz"}, }, }, { sql: "SELECT Id, Value FROM Simple", expected: []*Simple{ {ID: 100, Value: "xxx0"}, {ID: 101, Value: "xxx1"}, {ID: 102, Value: "xxx2"}, {ID: 200, Value: "yyy"}, {ID: 300, Value: "zzz"}, {ID: 400, Value: "zzz"}, }, }, { sql: "SELECT * FROM Simple WHERE Id = @id", params: map[string]interface{}{"id": 101}, expected: []*Simple{ {ID: 101, Value: "xxx1"}, }, }, { sql: "SELECT * FROM Simple WHERE Id IN UNNEST(@ids)", params: map[string]interface{}{ "ids": []int64{101, 102}, }, expected: []*Simple{ {ID: 101, Value: "xxx1"}, {ID: 102, Value: "xxx2"}, }, }, { sql: "SELECT * FROM Simple WHERE Id IN UNNEST(@ids)", params: map[string]interface{}{ "ids": []int64{}, }, expected: nil, }, { sql: "SELECT * FROM Simple WHERE Id IN UNNEST(@ids)", params: map[string]interface{}{ "ids": []int64(nil), }, expected: nil, }, { sql: `SELECT a.* FROM Simple AS a JOIN Simple AS b ON a.Id = b.Id WHERE a.Id = @id`, params: map[string]interface{}{ "id": 200, }, expected: []*Simple{ {ID: 200, Value: "yyy"}, }, }, { sql: "SELECT DISTINCT Value FROM Simple", expected: []*Simple{ {ID: 0, Value: "xxx0"}, {ID: 0, Value: "xxx1"}, {ID: 0, Value: "xxx2"}, {ID: 0, Value: "yyy"}, {ID: 0, Value: "zzz"}, }, }, } for _, tc := range table { ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() stmt := spanner.NewStatement(tc.sql) stmt.Params = tc.params var results []*Simple rows := client.Single().Query(ctx, stmt) err = rows.Do(func(row *spanner.Row) error { var s Simple if err := row.ToStruct(&s); err != nil { return err } results = append(results, &s) return nil }) if err != nil { t.Fatalf("Iterating over all row read: %v", err) } if diff := cmp.Diff(tc.expected, results); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } } } func TestIntegration_Query2(t *testing.T) { ctx := context.Background() dbName := "projects/fake/instances/fake/databases/fake" f, err := os.Open("./testdata/schema.sql") if err != nil { t.Fatalf("err %v", err) } srv, conn, err := Run() if err != nil { t.Fatalf("err %v", err) } defer srv.Stop() if err := srv.ParseAndApplyDDL(ctx, dbName, f); err != nil { t.Fatal(err) } client, err := spanner.NewClient(ctx, dbName, option.WithGRPCConn(conn)) if err != nil { t.Fatalf("Connecting to in-memory fake: %v", err) } now := time.Now() prepareFullTypeData(t, ctx, client, now) table := []struct { sql string params map[string]interface{} expected []*FullType }{ { sql: `SELECT PKey, FTInt, FTTimestamp FROM FullTypes WHERE FTInt = 100`, expected: []*FullType{ {PKey: "pkey0", FTInt: 100, FTTimestamp: now}, {PKey: "pkey1", FTInt: 100, FTTimestamp: now.Add(time.Second)}, {PKey: "pkey2", FTInt: 100, FTTimestamp: now.Add(2 * time.Second)}, }, }, { sql: `SELECT PKey, FTInt, FTTimestamp FROM FullTypes WHERE FTTimestamp BETWEEN @t1 AND @t2`, params: map[string]interface{}{ "t1": now, "t2": now.Add(2 * time.Second), }, expected: []*FullType{ {PKey: "pkey0", FTInt: 100, FTTimestamp: now}, {PKey: "pkey1", FTInt: 100, FTTimestamp: now.Add(time.Second)}, {PKey: "pkey2", FTInt: 100, FTTimestamp: now.Add(2 * time.Second)}, }, }, } for _, tc := range table { ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() stmt := spanner.NewStatement(tc.sql) stmt.Params = tc.params var results []*FullType rows := client.Single().Query(ctx, stmt) err = rows.Do(func(row *spanner.Row) error { var ft FullType if err := row.ToStruct(&ft); err != nil { return err } results = append(results, &ft) return nil }) if err != nil { t.Fatalf("Iterating over all row read: %v", err) } if diff := cmp.Diff(tc.expected, results); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } } } func TestIntegration_Query_Detail(t *testing.T) { ctx := context.Background() dbName := "projects/fake/instances/fake/databases/fake" f, err := os.Open("./testdata/schema.sql") if err != nil { t.Fatalf("err %v", err) } srv, conn, err := Run() if err != nil { t.Fatalf("err %v", err) } defer srv.Stop() if err := srv.ParseAndApplyDDL(ctx, dbName, f); err != nil { t.Fatal(err) } client, err := spanner.NewClient(ctx, dbName, option.WithGRPCConn(conn)) if err != nil { t.Fatalf("Connecting to in-memory fake: %v", err) } prepareSimpleData(t, ctx, client) table := []struct { sql string params map[string]interface{} names []string columns []interface{} expected [][]interface{} }{ { sql: "SELECT * FROM Simple", names: []string{"Id", "Value"}, columns: []interface{}{int64(0), ""}, expected: [][]interface{}{ {int64(100), string("xxx0")}, {int64(101), string("xxx1")}, {int64(102), string("xxx2")}, {int64(200), string("yyy")}, {int64(300), string("zzz")}, {int64(400), string("zzz")}, }, }, { sql: "SELECT COUNT(1) FROM Simple", names: []string{""}, // TODO columns: []interface{}{int64(0)}, expected: [][]interface{}{ {int64(6)}, }, }, { sql: "SELECT COUNT(1) AS count FROM Simple", names: []string{"count"}, columns: []interface{}{int64(0)}, expected: [][]interface{}{ {int64(6)}, }, }, { sql: "SELECT 10, -10, 010, 0x10, 0X10", names: []string{"", "", "", "", ""}, columns: []interface{}{ int64(0), int64(0), int64(0), int64(0), int64(0), }, expected: [][]interface{}{ {int64(10), int64(-10), int64(8), int64(16), int64(16)}, }, }, { sql: "SELECT 1.1, .1, 123.456e-67, .1E4, 58., 4e2", names: []string{"", "", "", "", "", ""}, columns: []interface{}{ float64(0), float64(0), float64(0), float64(0), float64(0), float64(0), }, expected: [][]interface{}{ {float64(1.1), float64(.1), float64(123.456e-67), float64(.1e4), float64(58.), float64(4e2)}, }, }, } for _, tc := range table { stmt := spanner.NewStatement(tc.sql) stmt.Params = tc.params var result [][]interface{} rows := client.Single().Query(ctx, stmt) err := rows.Do(func(row *spanner.Row) error { if diff := cmp.Diff(tc.names, row.ColumnNames()); diff != "" { t.Fatalf("(-got, +want)\n%s", diff) } var data []interface{} for i := range tc.columns { typ := reflect.New(reflect.TypeOf(tc.columns[i])) if err := row.Column(i, typ.Interface()); err != nil { t.Fatalf("Column error: %v", err) } data = append(data, reflect.Indirect(typ).Interface()) } result = append(result, data) return nil }) if err != nil { t.Fatalf("Iterating over all row read: %v", err) } if diff := cmp.Diff(tc.expected, result); diff != "" { t.Fatalf("(-got, +want)\n%s", diff) } } } func TestIntegration_CreateDatabase(t *testing.T) { ctx := context.Background() projectID, instanceID, databaseID := "fake", "fake", "fake" srv, conn, err := Run() if err != nil { t.Fatalf("err %v", err) } defer srv.Stop() adminclient, err := admindatabasev1.NewDatabaseAdminClient(ctx, option.WithGRPCConn(conn)) if err != nil { t.Fatalf("failed to connect fake spanner server: %v", err) } f, err := os.Open("./testdata/schema.sql") if err != nil { t.Fatalf("err %v", err) } b, err := ioutil.ReadAll(f) if err != nil { t.Fatalf("err %v", err) } var stmts []string for _, s := range strings.Split(string(b), ";") { s = strings.TrimSpace(s) if s == "" { continue } stmts = append(stmts, s) } op, err := adminclient.CreateDatabase(ctx, &databasepb.CreateDatabaseRequest{ Parent: fmt.Sprintf("projects/%s/instances/%s", projectID, instanceID), CreateStatement: fmt.Sprintf("CREATE DATABASE `%s`", databaseID), ExtraStatements: stmts, }) if err != nil { t.Fatal(err) } db, err := op.Wait(ctx) if err != nil { t.Fatalf("Wait err: %v", err) } if got, expect := db.GetName(), fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectID, instanceID, databaseID); got != expect { t.Fatalf("expected %s but got %s", expect, db.GetName()) } } func TestIntegration_UpdateDatbaseDdl(t *testing.T) { ctx := context.Background() dbName := "projects/fake/instances/fake/databases/fake" srv, conn, err := Run() if err != nil { t.Fatalf("err %v", err) } defer srv.Stop() adminclient, err := admindatabasev1.NewDatabaseAdminClient(ctx, option.WithGRPCConn(conn)) if err != nil { t.Fatalf("failed to connect fake spanner server: %v", err) } f, err := os.Open("./testdata/schema.sql") if err != nil { t.Fatalf("err %v", err) } b, err := ioutil.ReadAll(f) if err != nil { t.Fatalf("err %v", err) } var stmts []string for _, s := range strings.Split(string(b), ";") { s = strings.TrimSpace(s) if s == "" { continue } stmts = append(stmts, s) } op, err := adminclient.UpdateDatabaseDdl(ctx, &databasepb.UpdateDatabaseDdlRequest{ Database: dbName, Statements: stmts, }) if err != nil { t.Fatal(err) } if err := op.Wait(ctx); err != nil { t.Fatalf("Wait err: %v", err) } } func TestIntegration_DropDatabase(t *testing.T) { ctx := context.Background() projectID, instanceID, databaseID := "fake", "fake", "fake" srv, conn, err := Run() if err != nil { t.Fatalf("err %v", err) } defer srv.Stop() adminclient, err := admindatabasev1.NewDatabaseAdminClient(ctx, option.WithGRPCConn(conn)) if err != nil { t.Fatalf("failed to connect fake spanner server: %v", err) } // prepare database f, err := os.Open("./testdata/schema.sql") if err != nil { t.Fatalf("err %v", err) } b, err := ioutil.ReadAll(f) if err != nil { t.Fatalf("err %v", err) } var stmts []string for _, s := range strings.Split(string(b), ";") { s = strings.TrimSpace(s) if s == "" { continue } stmts = append(stmts, s) } if _, err := adminclient.CreateDatabase(ctx, &databasepb.CreateDatabaseRequest{ Parent: fmt.Sprintf("projects/%s/instances/%s", projectID, instanceID), CreateStatement: fmt.Sprintf("CREATE DATABASE `%s`", databaseID), ExtraStatements: stmts, }); err != nil { t.Fatal(err) } if err := adminclient.DropDatabase(ctx, &databasepb.DropDatabaseRequest{ Database: fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectID, instanceID, databaseID), }); err != nil { t.Fatal(err) } } ================================================ FILE: fake/server.go ================================================ // Copyright 2019 Masahiro Sano // // 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 fake import ( "context" "io" "io/ioutil" "net" "time" lropb "cloud.google.com/go/longrunning/autogen/longrunningpb" adminv1pb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" spannerpb "cloud.google.com/go/spanner/apiv1/spannerpb" "github.com/cloudspannerecosystem/memefish" "github.com/cloudspannerecosystem/memefish/ast" "github.com/cloudspannerecosystem/memefish/token" "github.com/gcpug/handy-spanner/server" "google.golang.org/grpc" channelzsvc "google.golang.org/grpc/channelz/service" "google.golang.org/grpc/reflection" ) type Server struct { addr string lis net.Listener grpcServer *grpc.Server srv server.FakeSpannerServer } func Run() (*Server, *grpc.ClientConn, error) { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { if l, err = net.Listen("tcp6", "[::1]:0"); err != nil { return nil, nil, err } } srv, err := New(l) if err != nil { return nil, nil, err } go func() { _ = srv.Start() }() ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() conn, err := grpc.DialContext(ctx, srv.Addr(), grpc.WithInsecure(), grpc.WithBlock()) if err != nil { srv.Stop() return nil, nil, err } return srv, conn, nil } // New returns Server for fake spanner. func New(lis net.Listener) (*Server, error) { s := &Server{ addr: lis.Addr().String(), lis: lis, grpcServer: grpc.NewServer(), srv: server.NewFakeServer(), } adminv1pb.RegisterDatabaseAdminServer(s.grpcServer, s.srv) spannerpb.RegisterSpannerServer(s.grpcServer, s.srv) lropb.RegisterOperationsServer(s.grpcServer, s.srv) reflection.Register(s.grpcServer) channelzsvc.RegisterChannelzServiceToServer(s.grpcServer) return s, nil } func (s *Server) Addr() string { return s.addr } func (s *Server) Start() error { return s.grpcServer.Serve(s.lis) } func (s *Server) Stop() { s.grpcServer.Stop() _ = s.lis.Close() } func (s *Server) ApplyDDL(ctx context.Context, databaseName string, ddl []ast.DDL) error { for _, stmt := range ddl { if err := s.srv.ApplyDDL(ctx, databaseName, stmt); err != nil { return err } } return nil } func (s *Server) ParseAndApplyDDL(ctx context.Context, databaseName string, r io.Reader) error { b, err := ioutil.ReadAll(r) if err != nil { return err } ddl, err := (&memefish.Parser{ Lexer: &memefish.Lexer{ File: &token.File{FilePath: "", Buffer: string(b)}, }, }).ParseDDLs() if err != nil { return err } for _, stmt := range ddl { if err := s.srv.ApplyDDL(ctx, databaseName, stmt); err != nil { return err } } return nil } ================================================ FILE: fake/testdata/schema.sql ================================================ CREATE TABLE Simple ( Id INT64 NOT NULL, Value STRING(MAX) NOT NULL, ) PRIMARY KEY(Id); CREATE TABLE CompositePrimaryKeys ( Id INT64 NOT NULL, PKey1 STRING(32) NOT NULL, PKey2 INT64 NOT NULL, Error INT64 NOT NULL, X STRING(32) NOT NULL, Y STRING(32) NOT NULL, Z STRING(32) NOT NULL, ) PRIMARY KEY(PKey1, PKey2); CREATE INDEX CompositePrimaryKeysByXY ON CompositePrimaryKeys(X, Y); CREATE INDEX CompositePrimaryKeysByError ON CompositePrimaryKeys(Error); CREATE TABLE FullTypes ( PKey STRING(32) NOT NULL, FTString STRING(32) NOT NULL, FTStringNull STRING(32), FTBool BOOL NOT NULL, FTBoolNull BOOL, FTBytes BYTES(32) NOT NULL, FTBytesNull BYTES(32), FTTimestamp TIMESTAMP NOT NULL, FTTimestampNull TIMESTAMP, FTInt INT64 NOT NULL, FTIntNull INT64, FTFloat FLOAT64 NOT NULL, FTFloatNull FLOAT64, FTDate DATE NOT NULL, FTDateNull DATE, ) PRIMARY KEY(PKey); CREATE UNIQUE INDEX FullTypesByFTString ON FullTypes(FTString); CREATE INDEX FullTypesByIntDate ON FullTypes(FTInt, FTDate); CREATE INDEX FullTypesByIntTimestamp ON FullTypes(FTInt, FTTimestamp); CREATE INDEX FullTypesByIntTimestampReverse ON FullTypes(FTInt, FTTimestamp DESC); CREATE INDEX FullTypesByTimestamp ON FullTypes(FTTimestamp); CREATE TABLE ArrayTypes ( Id INT64 NOT NULL, ArrayString ARRAY, ArrayBool ARRAY, ArrayBytes ARRAY, ArrayTimestamp ARRAY, ArrayInt ARRAY, ArrayFloat ARRAY, ArrayDate ARRAY, ) PRIMARY KEY(Id); CREATE CHANGE STREAM EverythingStream FOR ALL; ================================================ FILE: go.mod ================================================ module github.com/gcpug/handy-spanner go 1.23.0 toolchain go1.23.4 require ( cloud.google.com/go v0.118.2 cloud.google.com/go/iam v1.3.1 cloud.google.com/go/longrunning v0.6.4 cloud.google.com/go/spanner v1.75.0 github.com/cloudspannerecosystem/memefish v0.6.1 github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/mattn/go-sqlite3 v1.14.14 golang.org/x/sync v0.11.0 google.golang.org/api v0.220.0 google.golang.org/genproto v0.0.0-20250207221924-e9438ea467c6 google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 google.golang.org/grpc v1.70.0 google.golang.org/protobuf v1.36.5 ) require ( cel.dev/expr v0.19.2 // indirect cloud.google.com/go/auth v0.14.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect cloud.google.com/go/monitoring v1.24.0 // indirect github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect go.opentelemetry.io/otel v1.34.0 // indirect go.opentelemetry.io/otel/metric v1.34.0 // indirect go.opentelemetry.io/otel/sdk v1.34.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect go.opentelemetry.io/otel/trace v1.34.0 // indirect golang.org/x/crypto v0.33.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/oauth2 v0.26.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.10.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250207221924-e9438ea467c6 // indirect ) ================================================ FILE: go.sum ================================================ cel.dev/expr v0.19.2 h1:V354PbqIXr9IQdwy4SYA4xa0HXaWq1BUPAGzugBY5V4= cel.dev/expr v0.19.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= cloud.google.com/go v0.118.2 h1:bKXO7RXMFDkniAAvvuMrAPtQ/VHrs9e7J5UT3yrGdTY= cloud.google.com/go v0.118.2/go.mod h1:CFO4UPEPi8oV21xoezZCrd3d81K4fFkDTEJu4R8K+9M= cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k= cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A= cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI= cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= cloud.google.com/go/auth v0.14.1 h1:AwoJbzUdxA/whv1qj3TLKwh3XX5sikny2fc40wUl+h0= cloud.google.com/go/auth v0.14.1/go.mod h1:4JHUxlGXisL0AW8kXPtUF6ztuOksyfUQNFjfsOCXkPM= cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E= cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= cloud.google.com/go/iam v1.3.1 h1:KFf8SaT71yYq+sQtRISn90Gyhyf4X8RGgeAVC8XGf3E= cloud.google.com/go/iam v1.3.1/go.mod h1:3wMtuyT4NcbnYNPLMBzYRFiEfjKfJlLVLrisE7bwm34= cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo= cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= cloud.google.com/go/longrunning v0.6.4 h1:3tyw9rO3E2XVXzSApn1gyEEnH2K9SynNQjMlBi3uHLg= cloud.google.com/go/longrunning v0.6.4/go.mod h1:ttZpLCe6e7EXvn9OxpBRx7kZEB0efv8yBO6YnVMfhJs= cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= cloud.google.com/go/monitoring v1.24.0 h1:csSKiCJ+WVRgNkRzzz3BPoGjFhjPY23ZTcaenToJxMM= cloud.google.com/go/monitoring v1.24.0/go.mod h1:Bd1PRK5bmQBQNnuGwHBfUamAV1ys9049oEPHnn4pcsc= cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo= cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= cloud.google.com/go/spanner v1.75.0 h1:2zrltTJv/4P3pCgpYgde4Eb1vN8Cgy1fNy7pbTnOovg= cloud.google.com/go/spanner v1.75.0/go.mod h1:TLFZBvPQmx3We7sGh12eTk9lLsRLczzZaiweqfMpR80= cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 h1:DBjmt6/otSdULyJdVg2BlG0qGZO5tKL4VzOs0jpvw5Q= github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 h1:f2Qw/Ehhimh5uO1fayV0QIW7DShEQqhtUfhYc+cBPlw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/MakeNowJust/heredoc/v2 v2.0.1 h1:rlCHh70XXXv7toz95ajQWOWQnN4WNLt0TdpZYIR/J6A= github.com/MakeNowJust/heredoc/v2 v2.0.1/go.mod h1:6/2Abh5s+hc3g9nbWLe9ObDIOhaRrqsyY9MWy+4JdRM= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudspannerecosystem/memefish v0.6.1 h1:EJNZNq0E2vrYGBlu/xBs6jN7a5eq9ovF/wK+5Mo1iks= github.com/cloudspannerecosystem/memefish v0.6.1/go.mod h1:mVw0xBxy0yOgm990BuR0+nqP8J+yBAAf7N/2uL69rBU= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/k0kubun/pp/v3 v3.4.1 h1:1WdFZDRRqe8UsR61N/2RoOZ3ziTEqgTPVqKrHeb779Y= github.com/k0kubun/pp/v3 v3.4.1/go.mod h1:+SiNiqKnBfw1Nkj82Lh5bIeKQOAkPy6Xw9CAZUZ8npI= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw= github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= google.golang.org/api v0.220.0 h1:3oMI4gdBgB72WFVwE1nerDD8W3HUOS4kypK6rRLbGns= google.golang.org/api v0.220.0/go.mod h1:26ZAlY6aN/8WgpCzjPNy18QpYaz7Zgg1h0qe1GkZEmY= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20250207221924-e9438ea467c6 h1:SSk8oMbcHFbMwftDvX4PHbkqss3RkEZUF+k1h9d/sns= google.golang.org/genproto v0.0.0-20250207221924-e9438ea467c6/go.mod h1:wkQ2Aj/xvshAUDtO/JHvu9y+AaN9cqs28QuSVSHtZSY= google.golang.org/genproto/googleapis/api v0.0.0-20250207221924-e9438ea467c6 h1:L9JNMl/plZH9wmzQUHleO/ZZDSN+9Gh41wPczNy+5Fk= google.golang.org/genproto/googleapis/api v0.0.0-20250207221924-e9438ea467c6/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= ================================================ FILE: server/database.go ================================================ // Copyright 2019 Masahiro Sano // // 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 server import ( "bytes" "context" "database/sql" "fmt" "log" "strings" "sync" "sync/atomic" "time" "github.com/cloudspannerecosystem/memefish/ast" uuidpkg "github.com/google/uuid" sqlite "github.com/mattn/go-sqlite3" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" structpb "google.golang.org/protobuf/types/known/structpb" ) type Database interface { ApplyDDL(ctx context.Context, ddl ast.DDL) error Read(ctx context.Context, tx *transaction, tbl, index string, cols []string, keyset *KeySet, limit int64) (RowIterator, error) Query(ctx context.Context, tx *transaction, query *ast.QueryStatement, params map[string]Value) (RowIterator, error) Execute(ctx context.Context, tx *transaction, dml ast.DML, params map[string]Value) (int64, error) Insert(ctx context.Context, tx *transaction, tbl string, cols []string, values []*structpb.ListValue) error Update(ctx context.Context, tx *transaction, tbl string, cols []string, values []*structpb.ListValue) error Replace(ctx context.Context, tx *transaction, tbl string, cols []string, values []*structpb.ListValue) error InsertOrUpdate(ctx context.Context, tx *transaction, tbl string, cols []string, values []*structpb.ListValue) error Delete(ctx context.Context, tx *transaction, table string, keyset *KeySet) error BeginTransaction(tx *transaction) error Commit(tx *transaction) error Rollback(tx *transaction) error Close() error } type barrier struct { locked int32 condMu *sync.Mutex cond *sync.Cond } func newBarrier() *barrier { mu := new(sync.Mutex) cond := sync.NewCond(mu) return &barrier{ locked: 0, condMu: mu, cond: cond, } } // TryAcquire get a lock and blocks all other transactions enter critical section. // When fail to get a lock, returns false. Please use Wait to wait the lock is released and // try TryAcquire again. func (l *barrier) TryAcquire() bool { now := atomic.LoadInt32(&l.locked) if now == 1 { return false } ok := atomic.CompareAndSwapInt32(&l.locked, now, 1) return ok } // Release releases the lock and notifies to transactions that wait releasing th lock. func (l *barrier) Release() { atomic.StoreInt32(&l.locked, 0) l.cond.Broadcast() } // Wait waits until locked is released. // This does not ensure only one transaction enters critical section. // This makes sure one the lock is acquired by someone, other transactions are blocked until released. func (l *barrier) Wait() { now := atomic.LoadInt32(&l.locked) if now == 0 { return } l.condMu.Lock() l.cond.Wait() l.condMu.Unlock() } func (l *barrier) Released() bool { now := atomic.LoadInt32(&l.locked) return now == 0 } var _ Database = (*database)(nil) type database struct { db *sql.DB ctx context.Context cancel func() // schema level lock schemaMu sync.RWMutex tables map[string]*Table // transactions transactions map[string]*transaction transactionsMu sync.Mutex tablesInUse map[string]*tableTransaction tablesInUseMu sync.RWMutex // writeBarrier blocks other transactions try to write or commit writeBarrier *barrier writeTransaction *transaction writeTransactionMu sync.RWMutex } type tableTransaction struct { use sync.RWMutex lockHolder *transaction transactionsInUse map[string]*transaction transactionsInUseMu sync.Mutex } func (tt *tableTransaction) Dump() { if tt.lockHolder != nil { fmt.Printf("lock holder: %s status=%v\n", tt.lockHolder.Name(), tt.lockHolder.Status()) } else { fmt.Printf("lock holder: \n") } fmt.Printf("transactions in use\n") for _, tx := range tt.transactionsInUse { fmt.Printf(" - %s status=%v\n", tx.Name(), tx.Status()) } } func (tt *tableTransaction) Use(tx *transaction) { if IsDebug() { defer DebugStartEnd("[%s] tableTransaction.Use", tx.Name())() } // skip if the transction already holds the table lock tt.transactionsInUseMu.Lock() if tx.Equals(tt.lockHolder) { tt.transactionsInUseMu.Unlock() return } tt.transactionsInUseMu.Unlock() // try to get read lock tt.use.RLock() defer tt.use.RUnlock() tt.transactionsInUseMu.Lock() defer tt.transactionsInUseMu.Unlock() tt.transactionsInUse[tx.Name()] = tx } func (tt *tableTransaction) Lock(tx *transaction) { if IsDebug() { defer DebugStartEnd("[%s] tableTransaction.Lock", tx.Name())() } // skip if the transction already holds the table lock tt.transactionsInUseMu.Lock() if tx.Equals(tt.lockHolder) { tt.transactionsInUseMu.Unlock() return } tt.transactionsInUseMu.Unlock() // try to get write lock tt.use.Lock() var uses []*transaction func() { tt.transactionsInUseMu.Lock() defer tt.transactionsInUseMu.Unlock() if !tx.Available() { Debugf("[%s] transaction NOT AVAILABE in Lock: %v\n", tx.Name(), tx.Status()) panic(fmt.Sprintf("[%s] transaction NOT AVAILABE in Lock: %v\n", tx.Name(), tx.Status())) } tt.lockHolder = tx for _, tx := range tt.transactionsInUse { uses = append(uses, tx) } }() // Abort all ransactions which hold read lock for the table for _, tt := range uses { if !tx.Equals(tt) { tt.Done(TransactionAborted) } } } func (tt *tableTransaction) Release(tx *transaction) { if IsDebug() { defer DebugStartEnd("[%s] tableTransaction.Release", tx.Name())() } tt.transactionsInUseMu.Lock() defer tt.transactionsInUseMu.Unlock() delete(tt.transactionsInUse, tx.Name()) if !tx.Equals(tt.lockHolder) { return } tt.lockHolder = nil tt.use.Unlock() } func newTableTransaction() *tableTransaction { return &tableTransaction{ transactionsInUse: make(map[string]*transaction), } } func newDatabase() *database { uuid := uuidpkg.New().String() db, err := sql.Open("sqlite3_spanner", fmt.Sprintf("file:%s.db?cache=shared&mode=memory&_foreign_keys=true", uuid)) if err != nil { log.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) conn, err := db.Conn(ctx) if err != nil { log.Fatal(err) } // keep at least 1 active connection to keep database go func(conn *sql.Conn) { t := time.NewTicker(1 * time.Second) defer t.Stop() for { select { case <-ctx.Done(): return case <-t.C: } newConn, err := db.Conn(ctx) if err != nil { continue } conn.Close() conn = newConn } }(conn) d := &database{ tables: make(map[string]*Table), tablesInUse: make(map[string]*tableTransaction), db: db, ctx: ctx, cancel: cancel, transactions: make(map[string]*transaction), writeBarrier: newBarrier(), } if err := d.prepareMetaTables(ctx); err != nil { log.Fatalf("failed to prepare meta tables: %v", err) } return d } func (d *database) prepareMetaTables(ctx context.Context) error { for _, table := range metaTables { if err := d.CreateTable(ctx, table); err != nil { return fmt.Errorf("failed to create table for %s: %v", table.Name.Idents[0].Name, err) } } unixmicro := int64(time.Now().UnixNano() / 1000) initialData := []string{ fmt.Sprintf(`INSERT INTO __INFORMATION_SCHEMA__SCHEMATA VALUES("", "", %d)`, unixmicro), `INSERT INTO __INFORMATION_SCHEMA__SCHEMATA VALUES("", "INFORMATION_SCHEMA", NULL)`, `INSERT INTO __INFORMATION_SCHEMA__SCHEMATA VALUES("", "SPANNER_SYS", NULL)`, } for _, query := range initialData { if _, err := d.db.ExecContext(ctx, query); err != nil { return fmt.Errorf("failed to prepare initial data: %v", err) } } for _, table := range metaTables { if err := d.registerInformationSchemaTables(ctx, table); err != nil { return fmt.Errorf("failed to create table for %s: %v", table.Name.Idents[0].Name, err) } } return nil } // waitUntilReadable marks database is used for read. // If database is locked for write by other transaction, this function blocks until the lock is released. func (d *database) waitUntilReadable(ctx context.Context, tx *transaction) error { if IsDebug() { defer DebugStartEnd("[%s] database.waitUntilReadable", tx.Name())() } // Skip if the transaction already holds write lock d.writeTransactionMu.RLock() curTx := d.writeTransaction d.writeTransactionMu.RUnlock() if tx.Equals(curTx) { return nil } if d.writeBarrier.Released() { return nil } ch := make(chan struct{}, 0) go func() { d.writeBarrier.Wait() close(ch) }() select { case <-ch: case <-ctx.Done(): return status.FromContextError(ctx.Err()).Err() } if !tx.Available() { return ErrNotAvailableTransaction } return nil } // waitUntilWritable locks database for write. // This function does not ensure all other transactions don't have read lock. // Once locked, other transactions cannot newly get read or write lock. // // This function blocks until the lock is acquired. Break the block when the context // is done while waiting the lock. func (d *database) waitUntilWritable(ctx context.Context, tx *transaction) error { if IsDebug() { defer DebugStartEnd("[%s] database.waitUntilWritable", tx.Name())() } // Skip if the transaction already holds write lock d.writeTransactionMu.RLock() curTx := d.writeTransaction d.writeTransactionMu.RUnlock() if tx.Equals(curTx) { return nil } ch := make(chan struct{}, 0) go func() { for { select { case <-ctx.Done(): return default: } if !tx.Available() { close(ch) return } // Try to get lock ok := d.writeBarrier.TryAcquire() if ok { // save the holder of write lock d.writeTransactionMu.Lock() d.writeTransaction = tx d.writeTransactionMu.Unlock() close(ch) return } // Return if the lock is acrequired by own transaction d.writeTransactionMu.RLock() curTx := d.writeTransaction d.writeTransactionMu.RUnlock() if curTx != nil && bytes.Equal(curTx.ID(), tx.ID()) { close(ch) return } // Wait until the lock is released d.writeBarrier.Wait() } }() select { case <-ch: case <-ctx.Done(): return status.FromContextError(ctx.Err()).Err() } if !tx.Available() { d.releaseWriteLock(tx) return ErrNotAvailableTransaction } return nil } // releasewriteLock releases write lock for database // This function can be called by any transactions but transactions don't hold write lock are ignored. func (d *database) releaseWriteLock(tx *transaction) { if IsDebug() { defer DebugStartEnd("[%s] database.releaseWriteLock", tx.Name())() } d.writeTransactionMu.RLock() curTx := d.writeTransaction d.writeTransactionMu.RUnlock() if !curTx.Equals(tx) { return } d.writeTransactionMu.Lock() defer d.writeTransactionMu.Unlock() curTx = d.writeTransaction if curTx.Equals(tx) { d.writeTransaction = nil d.writeBarrier.Release() } } // useTable marks a table is used for read by the transaction. // If the table is locked by other transaction, this function blocks until the lock is released. func (d *database) useTable(tbl string, tx *transaction) (*Table, error) { d.schemaMu.RLock() defer d.schemaMu.RUnlock() d.tablesInUseMu.Lock() defer d.tablesInUseMu.Unlock() table, ok := d.tables[tbl] if !ok { return nil, newSpannerTableNotFoundError(tbl) } use, ok := d.tablesInUse[tbl] if !ok { use = newTableTransaction() d.tablesInUse[tbl] = use } use.Use(tx) if !tx.Available() { return nil, ErrNotAvailableTransaction } return table, nil } // useTableExclusive locks a table for write. // Other transactions cannot read or write the table and wait until the lock is released. // This function blocks until lock is acquired. // // When a transaction got the lock to a table, other transactions reading the table are // aborted immediately. func (d *database) useTableExclusive(tbl string, tx *transaction) (*Table, error) { table, ok := d.tables[tbl] if !ok { return nil, newSpannerTableNotFoundError(tbl) } var tt *tableTransaction func() { d.tablesInUseMu.Lock() defer d.tablesInUseMu.Unlock() use, ok := d.tablesInUse[tbl] if !ok { use = newTableTransaction() d.tablesInUse[tbl] = use } tt = use }() tt.Lock(tx) return table, nil } func (d *database) ApplyDDL(ctx context.Context, ddl ast.DDL) error { d.schemaMu.Lock() defer d.schemaMu.Unlock() switch val := ddl.(type) { case *ast.CreateTable: if err := d.CreateTable(ctx, val); err != nil { return status.Errorf(codes.Unknown, "%v", err) } return nil case *ast.CreateIndex: if err := d.CreateIndex(ctx, val); err != nil { return status.Errorf(codes.Unknown, "%v", err) } return nil case *ast.DropTable: return status.Errorf(codes.Unimplemented, "Drop Table is not supported yet") case *ast.DropIndex: return status.Errorf(codes.Unimplemented, "Drop Index is not supported yet") case *ast.AlterTable: return status.Errorf(codes.Unimplemented, "Alter Table is not supported yet") case *ast.CreateChangeStream: // skip CreateChangeStream return nil default: return status.Errorf(codes.Unknown, "unknown DDL statement: %v", val) } } func (d *database) Read(ctx context.Context, tx *transaction, tbl, idx string, cols []string, keyset *KeySet, limit int64) (RowIterator, error) { if keyset == nil { return nil, status.Errorf(codes.InvalidArgument, "Invalid StreamingRead request") } if tbl == "" { return nil, status.Errorf(codes.InvalidArgument, "Invalid StreamingRead request") } if len(cols) == 0 { return nil, status.Errorf(codes.InvalidArgument, "Invalid StreamingRead request") } if err := d.waitUntilReadable(ctx, tx); err != nil { return nil, err } table, err := d.useTable(tbl, tx) if err != nil { return nil, err } index, ok := table.TableIndex(idx) if !ok { return nil, newSpannerIndexnNotFoundError(tbl, idx) } columns, err := table.getColumnsByName(cols) if err != nil { return nil, err // getColumnsByName returns error with status code } // Check the index table has the specified columns for _, column := range columns { if !index.HasColumn(column.Name()) { return nil, status.Errorf(codes.Unimplemented, "Reading non-indexed columns using an index is not supported. Consider adding %s to the index using a STORING clause.", column.Name()) } } resultItems := make([]ResultItem, len(cols)) for i := range columns { resultItems[i] = createResultItemFromColumn(columns[i]) } indexColumnsName := strings.Join(QuoteStringSlice(index.IndexColumnNames()), ", ") indexColumns := index.IndexColumns() indexColumnDirs := index.IndexColumnDirections() colName := strings.Join(QuoteStringSlice(cols), ", ") var args []interface{} whereClause, whereArgs, err := buildWhereClauseFromKeySet(keyset, indexColumnsName, indexColumns, indexColumnDirs) if err != nil { return nil, err } args = append(args, whereArgs...) orderByItems := make([]string, len(indexColumns)) for i := range indexColumns { orderByItems[i] = fmt.Sprintf("%s %s", QuoteString(indexColumns[i].Name()), indexColumnDirs[i]) } orderByClause := strings.Join(orderByItems, ", ") query := fmt.Sprintf(`SELECT %s FROM %s %s ORDER BY %s`, colName, QuoteString(table.Name), whereClause, orderByClause) if limit > 0 { query += fmt.Sprintf(" LIMIT %d", limit) } var sqlRows *sql.Rows err = tx.ReadTransaction(func(ctx context.Context, dbtx databaseReader) error { r, err := dbtx.QueryContext(ctx, query, args...) if err != nil { return status.Errorf(codes.Unknown, "query failed: %v, query: %v", err, query) } sqlRows = r return nil }) if err != nil { return nil, err } return &rows{rows: sqlRows, resultItems: resultItems, transaction: tx}, nil } func (d *database) Query(ctx context.Context, tx *transaction, stmt *ast.QueryStatement, params map[string]Value) (RowIterator, error) { if err := d.waitUntilReadable(ctx, tx); err != nil { return nil, err } query, args, resultItems, err := BuildQuery(d, tx, stmt.Query, params, false) if err != nil { return nil, err } var sqlRows *sql.Rows err = tx.ReadTransaction(func(ctx context.Context, dbtx databaseReader) error { r, err := dbtx.QueryContext(ctx, query, args...) if err != nil { return status.Errorf(codes.Unknown, "query failed: %v, query: %v", err, query) } sqlRows = r return nil }) if err != nil { return nil, err } return &rows{rows: sqlRows, resultItems: resultItems, transaction: tx}, nil } func (d *database) Execute(ctx context.Context, tx *transaction, dml ast.DML, params map[string]Value) (int64, error) { if err := d.waitUntilWritable(ctx, tx); err != nil { return 0, err } if !tx.Available() { return 0, ErrNotAvailableTransaction } if tx.Status() == TransactionAborted { return 0, status.Errorf(codes.Aborted, "transaction aborted") } query, args, err := BuildDML(d, tx, dml, params) if err != nil { return 0, err } var affectedRows int64 err = tx.WriteTransaction(func(dbtx databaseWriter) error { r, err := dbtx.ExecContext(ctx, query, args...) if err != nil { if sqliteErr, ok := err.(sqlite.Error); ok { // This error should not be happend. // This error means a tx tries to write a table which another tx holds read-lock // to the table, or a tx tries to write a table which another tx holds global wite-lock // // It better to be panic but return Aborted to expect the client retries. if sqliteErr.Code == sqlite.ErrLocked { return status.Errorf(codes.Aborted, "transaction is aborted: database is locked") } } return status.Errorf(codes.Unknown, "failed to write into sqlite: %v", err) } affectedRows, err = r.RowsAffected() if err != nil { return err } return nil }) if err != nil { return 0, err } return affectedRows, nil } func (d *database) write(ctx context.Context, tx *transaction, tbl string, cols []string, values []*structpb.ListValue, nonNullCheck bool, affectedRowsCheck bool, buildQueryFn func(*Table, []*Column) string, buildArgsFn func(*Table, []*Column, []interface{}) []interface{}, ) error { if IsDebug() { defer DebugStartEnd("[%s] database.write", tx.Name())() } if err := d.waitUntilWritable(ctx, tx); err != nil { return err } if !tx.Available() { return ErrNotAvailableTransaction } if tx.Status() == TransactionAborted { return status.Errorf(codes.Aborted, "transaction aborted") } table, err := d.useTableExclusive(tbl, tx) if err != nil { return err } if IsDebug() { defer DebugStartEnd("[%s] database.write after write lock", tx.Name())() } primaryKey := table.primaryKey // Check columns are defined in the table columns, err := table.getColumnsByName(cols) if err != nil { return err // getColumnsByName returns error with status code } // Ensure multiple values are not specified usedColumns := make(map[string]struct{}, len(cols)) for _, c := range columns { n := c.Name() if _, ok := usedColumns[n]; ok { return status.Errorf(codes.InvalidArgument, "Multiple values for column %s", n) } usedColumns[n] = struct{}{} } // Check all primary keys are specified for _, colName := range primaryKey.IndexColumnNames() { if _, ok := usedColumns[colName]; !ok { return status.Errorf(codes.FailedPrecondition, "%s must not be NULL in table %s.", colName, tbl) } } // Check not nullable columns are specified for Insert/Replace if nonNullCheck { if exist, nonNullables := table.NonNullableAndNonGeneratedColumnsExist(cols); exist { columns := strings.Join(nonNullables, ", ") return status.Errorf(codes.FailedPrecondition, "A new row in table %s does not specify a non-null value for these NOT NULL columns: %s", tbl, columns, ) } } if len(values) == 0 { return nil } query := buildQueryFn(table, columns) if query == "" { return nil } err = tx.WriteTransaction(func(dbtx databaseWriter) error { for _, vs := range values { if len(vs.Values) != len(cols) { return status.Error(codes.InvalidArgument, "Mutation has mismatched number of columns and values.") } data := make([]interface{}, 0, len(cols)) for i, v := range vs.Values { col := columns[i] vv, err := spannerValue2DatabaseValue(v, *col) if err != nil { return status.Errorf(codes.InvalidArgument, "%v", err) } if !col.nullable && vv == nil { return status.Errorf(codes.FailedPrecondition, "%s must not be NULL in table %s.", col.Name(), tbl, ) } data = append(data, vv) } args := buildArgsFn(table, columns, data) r, err := dbtx.ExecContext(ctx, query, args...) if err != nil { if sqliteErr, ok := err.(sqlite.Error); ok { msg := sqliteErr.Error() switch sqliteErr.ExtendedCode { case sqlite.ErrConstraintPrimaryKey: return status.Errorf(codes.AlreadyExists, "Row %v in table %s already exists", data, tbl) case sqlite.ErrConstraintUnique: if n := strings.Index(msg, ": "); n > 0 { msg = msg[n+2:] } return status.Errorf(codes.AlreadyExists, "Unique index violation at index key [%v]. It conflicts with row %v in table %s", msg, data, tbl, ) case sqlite.ErrConstraintCheck: return status.Errorf(codes.FailedPrecondition, "%s", sqliteErr.Error()) } // This error should not be happend. // This error means a tx tries to write a table which another tx holds read-lock // to the table, or a tx tries to write a table which another tx holds global wite-lock // // It better to be panic but return Aborted to expect the client retries. if sqliteErr.Code == sqlite.ErrLocked { return status.Errorf(codes.Aborted, "transaction is aborted: database is locked") } } return status.Errorf(codes.Unknown, "failed to write into sqlite: %v", err) } if affectedRowsCheck { // Check rows are updated // When the row does not exist, sqlite returns success. // But spanner should return NotFound n, err := r.RowsAffected() if err != nil { return status.Errorf(codes.Unknown, "failed to get RowsAffected: %v", err) } if n == 0 { // no details in real spanner return status.Errorf(codes.NotFound, "Row %v in table %s is missing. Row cannot be updated.", data, tbl) } } } return nil }) if err != nil { return err } return nil } func (d *database) Insert(ctx context.Context, tx *transaction, tbl string, cols []string, values []*structpb.ListValue) error { buildQueryFn := func(table *Table, columns []*Column) string { columnName := strings.Join(QuoteStringSlice(cols), ", ") placeholder := "?" if len(cols) > 1 { placeholder += strings.Repeat(", ?", len(cols)-1) } return fmt.Sprintf(`INSERT INTO %s (%s) VALUES (%s)`, QuoteString(tbl), columnName, placeholder) } buildArgsFn := func(table *Table, columns []*Column, data []interface{}) []interface{} { return data } return d.write(ctx, tx, tbl, cols, values, true, false, buildQueryFn, buildArgsFn) } func (d *database) Update(ctx context.Context, tx *transaction, tbl string, cols []string, values []*structpb.ListValue) error { buildQueryFn := func(table *Table, columns []*Column) string { assigns := make([]string, 0, len(cols)) for _, c := range columns { if c.isPrimaryKey { continue } assigns = append(assigns, fmt.Sprintf("%s = ?", QuoteString(c.Name()))) } // If no columns to be updated exist, it should be no-op. if len(assigns) == 0 { return "" } setClause := strings.Join(assigns, ", ") pKeysNames := table.primaryKey.IndexColumnNames() pkeysAssign := make([]string, len(pKeysNames)) for i, col := range pKeysNames { pkeysAssign[i] = fmt.Sprintf("%s = ?", QuoteString(col)) } whereClause := strings.Join(pkeysAssign, " AND ") return fmt.Sprintf(`UPDATE %s SET %s WHERE %s`, QuoteString(tbl), setClause, whereClause) } buildArgsFn := func(table *Table, columns []*Column, data []interface{}) []interface{} { numPKeys := len(table.primaryKey.IndexColumns()) values := make([]interface{}, 0, len(cols)) pkeyValues := make([]interface{}, numPKeys) for i, column := range columns { if column.isPrimaryKey { pkeyValues[column.primaryKeyPos-1] = data[i] } else { values = append(values, data[i]) } } // First N(size=columns-pkeys) values are for SET clause // Last M(size=pkeys) values are for WHERE clause values = append(values, pkeyValues...) return values } return d.write(ctx, tx, tbl, cols, values, false, true, buildQueryFn, buildArgsFn) } func (d *database) Replace(ctx context.Context, tx *transaction, tbl string, cols []string, values []*structpb.ListValue) error { buildQueryFn := func(table *Table, columns []*Column) string { columnName := strings.Join(QuoteStringSlice(cols), ", ") placeholder := "?" if len(cols) > 1 { placeholder += strings.Repeat(", ?", len(cols)-1) } return fmt.Sprintf(`REPLACE INTO %s (%s) VALUES (%s)`, QuoteString(tbl), columnName, placeholder) } buildArgsFn := func(table *Table, columns []*Column, data []interface{}) []interface{} { return data } return d.write(ctx, tx, tbl, cols, values, true, false, buildQueryFn, buildArgsFn) } func (d *database) InsertOrUpdate(ctx context.Context, tx *transaction, tbl string, cols []string, values []*structpb.ListValue) error { buildQueryFn := func(table *Table, columns []*Column) string { assigns := make([]string, 0, len(cols)) for _, c := range columns { if c.isPrimaryKey { continue } assigns = append(assigns, fmt.Sprintf("%s = ?", QuoteString(c.Name()))) } setClause := strings.Join(assigns, ", ") pkeysNamesSlice := table.primaryKey.IndexColumnNames() pkeysNames := strings.Join(QuoteStringSlice(pkeysNamesSlice), ", ") columnName := strings.Join(QuoteStringSlice(cols), ", ") placeholder := "?" if len(cols) > 1 { placeholder += strings.Repeat(", ?", len(cols)-1) } return fmt.Sprintf(`INSERT INTO %s (%s) VALUES (%s) ON CONFLICT (%s) DO UPDATE SET %s`, QuoteString(tbl), columnName, placeholder, pkeysNames, setClause) } buildArgsFn := func(table *Table, columns []*Column, data []interface{}) []interface{} { setValues := make([]interface{}, 0, len(cols)) for i, column := range columns { if !column.isPrimaryKey { setValues = append(setValues, data[i]) } } // First N(size=columns) values are for VALUES clause // Last M(size=columns-pkeys) values are for SET clause data = append(data, setValues...) return data } return d.write(ctx, tx, tbl, cols, values, false, false, buildQueryFn, buildArgsFn) } func (d *database) Delete(ctx context.Context, tx *transaction, tbl string, keyset *KeySet) error { if err := d.waitUntilWritable(ctx, tx); err != nil { return err } table, err := d.useTableExclusive(tbl, tx) if err != nil { return err } index := table.primaryKey indexColumnsName := strings.Join(QuoteStringSlice(index.IndexColumnNames()), ", ") indexColumns := index.IndexColumns() indexColumnDirs := index.IndexColumnDirections() whereClause, args, err := buildWhereClauseFromKeySet(keyset, indexColumnsName, indexColumns, indexColumnDirs) if err != nil { return err } err = tx.WriteTransaction(func(dbtx databaseWriter) error { query := fmt.Sprintf("DELETE FROM %s %s", QuoteString(tbl), whereClause) if _, err := dbtx.ExecContext(ctx, query, args...); err != nil { return status.Errorf(codes.Unknown, "failed to delete: %v", err) } return nil }) if err != nil { return err } return nil } func (d *database) BeginTransaction(tx *transaction) error { if tx == nil { return fmt.Errorf("invalid transaction: nil") } dbtx, err := d.db.BeginTx(tx.Context(), nil) if err != nil { return err } d.transactionsMu.Lock() defer d.transactionsMu.Unlock() if err := tx.SetTransaction(dbtx, d.endTransaction); err != nil { dbtx.Rollback() return err } d.transactions[tx.Name()] = tx return nil } func (d *database) endTransaction(tx *transaction, dbtx *sql.Tx) { if tx == nil { return } if IsDebug() { defer DebugStartEnd("[%s] database.endTransaction", tx.Name())() } // always try to do rollback to make sure the transaction finished if dbtx != nil { var lastErr error for i := 0; i < 3; i++ { err := dbtx.Rollback() if err == nil || err == sql.ErrTxDone { lastErr = nil break } lastErr = err time.Sleep(time.Millisecond) } if lastErr != nil { panic(fmt.Sprintf("endTransaction err: %T %v", lastErr, lastErr)) } } d.transactionsMu.Lock() defer d.transactionsMu.Unlock() delete(d.transactions, tx.Name()) for _, tt := range d.tablesInUse { tt.Release(tx) } d.releaseWriteLock(tx) return } func (d *database) Commit(tx *transaction) error { if IsDebug() { defer DebugStartEnd("[%s] database.Commit", tx.Name())() } d.writeTransactionMu.RLock() defer d.writeTransactionMu.RUnlock() err := tx.WriteTransaction(func(dbtx databaseWriter) error { if err := dbtx.Commit(); err != nil { return fmt.Errorf("failed to commit: %v", err) } return nil }) if err != nil { return err } return nil } func (d *database) Rollback(tx *transaction) error { if IsDebug() { defer DebugStartEnd("[%s] database.Rollback", tx.Name())() } d.writeTransactionMu.RLock() defer d.writeTransactionMu.RUnlock() err := tx.WriteTransaction(func(dbtx databaseWriter) error { if err := dbtx.Rollback(); err != nil { return fmt.Errorf("failed to rollback: %v", err) } return nil }) if err != nil { return err } return nil } func joinIdentNames(idents []*ast.Ident) string { names := make([]string, 0, len(idents)) for _, ident := range idents { names = append(names, ident.Name) } return strings.Join(QuoteStringSlice(names), ",") } func (db *database) CreateTable(ctx context.Context, stmt *ast.CreateTable) error { if _, ok := db.tables[stmt.Name.Idents[0].Name]; ok { return fmt.Errorf("duplicated table: %v", stmt.Name.Idents[0].Name) } t, err := createTableFromAST(stmt) if err != nil { return status.Errorf(codes.InvalidArgument, "CreateTable failed: %v", err) } db.transactionsMu.Lock() defer db.transactionsMu.Unlock() db.tables[stmt.Name.Idents[0].Name] = t db.tablesInUseMu.Lock() db.tablesInUse[stmt.Name.Idents[0].Name] = newTableTransaction() db.tablesInUseMu.Unlock() var columnDefs []string for _, col := range t.columns { s := fmt.Sprintf(" %s %s", QuoteString(col.Name()), col.dbDataType) if !col.nullable { // Array data type is not supported for now // so values for array data type are handled as null always if !col.isArray { s += " NOT NULL" } } if col.ast != nil && col.ast.DefaultSemantics != nil { if genCol, ok := col.ast.DefaultSemantics.(*ast.GeneratedColumnExpr); ok { s += fmt.Sprintf(" %s", genCol.SQL()) } } if col.ast != nil && col.ast.DefaultSemantics != nil { if defExpr, ok := col.ast.DefaultSemantics.(*ast.ColumnDefaultExpr); ok { if _, ok := defExpr.Expr.(*ast.ArrayLiteral); ok { // TODO: support array literal for default expression s += ` DEFAULT "[]"` } else { defSQL := defExpr.Expr.SQL() // workaround: convert default expression for sqlite switch exp := defExpr.Expr.(type) { case *ast.CallExpr: switch strings.ToUpper(exp.Func.Idents[0].Name) { case "CURRENT_TIMESTAMP": defSQL = "CURRENT_TIMESTAMP" case "CURRENT_DATE": defSQL = "CURRENT_DATE" } } s += fmt.Sprintf(" DEFAULT %s", defSQL) } } } if col.valueType.Code == TCString && col.isSized && !col.isMax { s += fmt.Sprintf(" CHECK(LENGTH(%s) <= %d)", QuoteString(col.Name()), col.size) } columnDefs = append(columnDefs, s) } columnDefsQuery := strings.Join(columnDefs, ",\n") primaryKeysQuery := strings.Join(QuoteStringSlice(t.primaryKey.IndexColumnNames()), ", ") constraints := "" if stmt.Cluster != nil { parentTableName := stmt.Cluster.TableName.Idents[0].Name parentStmt, ok := db.tables[parentTableName] if !ok { return fmt.Errorf("could not find parent table for interleaving: %v", stmt.Name.Idents[0].Name) } columns := strings.Join(QuoteStringSlice(parentStmt.primaryKey.IndexColumnNames()), ",") constraints += fmt.Sprintf(",\n FOREIGN KEY(%s) REFERENCES %s(%s) %s", columns, QuoteString(parentTableName), columns, stmt.Cluster.OnDelete) } for _, cnst := range stmt.TableConstraints { switch cnst := cnst.Constraint.(type) { case *ast.ForeignKey: columns := joinIdentNames(cnst.Columns) refColumns := joinIdentNames(cnst.ReferenceColumns) constraints += fmt.Sprintf( ",\n FOREIGN KEY(%s) REFERENCES %s(%s) %s", columns, QuoteString(cnst.ReferenceTable.Idents[0].Name), refColumns, cnst.OnDelete, ) case *ast.Check: // TODO: implement, currently just it is ignored. // following kind code is needed for this. // constraints += fmt.Sprintf(",\n CHECK(%s)", QueryBuilder{}.buildExpr(cnst.Expr).Raw) } } query := fmt.Sprintf("CREATE TABLE %s (\n%s,\n PRIMARY KEY (%s)%s\n)", QuoteString(t.Name), columnDefsQuery, primaryKeysQuery, constraints) if _, err := db.db.ExecContext(ctx, query); err != nil { return fmt.Errorf("failed to create table for %s: %v", t.Name, err) } // register the table information INFORMATION_SCHEMA.TABLES exept for handy-spanner preserved tables if !strings.HasPrefix(t.Name, "__") { if err := db.registerInformationSchemaTables(ctx, stmt); err != nil { return err } } return nil } func (db *database) registerInformationSchemaTables(ctx context.Context, stmt *ast.CreateTable) error { table, ok := db.tables[stmt.Name.Idents[0].Name] if !ok { return fmt.Errorf("unexpected error: table not found: %v", stmt.Name.Idents[0].Name) } schemaName := "" tableName := stmt.Name.Idents[0].Name if schema, ok := metaTablesReverseMap[tableName]; ok { schemaName = schema[0] tableName = schema[1] } parentTableName := "NULL" onDeleteAction := "NULL" state := "NULL" if schemaName == "" { state = `"COMMITTED"` } if stmt.Cluster != nil { parentTableName = fmt.Sprintf("%q", stmt.Cluster.TableName.Idents[0].Name) switch stmt.Cluster.OnDelete { case ast.OnDeleteCascade: onDeleteAction = `"CASCADE"` case ast.OnDeleteNoAction: onDeleteAction = `"NO ACTION"` } } for i, tcnst := range stmt.TableConstraints { switch cnst := tcnst.Constraint.(type) { case *ast.ForeignKey: var onDeleteAction string var constraintName string switch cnst.OnDelete { case ast.OnDeleteCascade: onDeleteAction = `"CASCADE"` default: onDeleteAction = `"NO ACTION"` } if tcnst.Name != nil { constraintName = tcnst.Name.Name } else { constraintName = fmt.Sprintf("FK_%s_%s_%d", tableName, cnst.ReferenceTable.Idents[0].Name, i) } query := fmt.Sprintf( `INSERT INTO __INFORMATION_SCHEMA__REFERENTIAL_CONSTRAINTS VALUES("", "", %q, "", "", %q, "SIMPLE", "NO ACTION", %s, "COMMITTED")`, constraintName, cnst.ReferenceTable.Idents[0].Name, onDeleteAction, ) if _, err := db.db.ExecContext(ctx, query); err != nil { return fmt.Errorf("failed to insert into INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS for table %s: %v", tableName, err) } case *ast.Check: // TODO: implement } } // register INFORMATION_SCHEMA.TABLES query := fmt.Sprintf(`INSERT INTO __INFORMATION_SCHEMA__TABLES VALUES("", %q, %q, %s, %s, %s)`, schemaName, tableName, parentTableName, onDeleteAction, state) if _, err := db.db.ExecContext(ctx, query); err != nil { return fmt.Errorf("failed to insert into INFORMATION_SCHEMA.TABLES for table %s: %v", tableName, err) } // register meta schema for columns for pos, column := range stmt.Columns { nullable := "YES" if column.NotNull { nullable = "NO" } spannerType := schemaTypetoTypString(column.Type) // register INFORMATION_SCHEMA.COLUMNS query := fmt.Sprintf(`INSERT INTO __INFORMATION_SCHEMA__COLUMNS VALUES("", %q, %q, %q, %d, NULL, NULL, %q, %q)`, schemaName, tableName, column.Name.Name, pos+1, nullable, spannerType) if _, err := db.db.ExecContext(ctx, query); err != nil { return fmt.Errorf("failed to insert into INFORMATION_SCHEMA.COLUMNS for table %s: %v", tableName, err) } // register meta schema for column options if column.Options != nil { var hasAllowCommitTimestamp bool if val, err := column.Options.BoolField("allow_commit_timestamp"); err == nil && val != nil { hasAllowCommitTimestamp = *val } if hasAllowCommitTimestamp { // register INFORMATION_SCHEMA.COLUMN_OPTIONS for `allow_commit_timestamp` option query := fmt.Sprintf(`INSERT INTO __INFORMATION_SCHEMA__COLUMN_OPTIONS VALUES("", %q, %q, %q, %q, %q, %q)`, schemaName, tableName, column.Name.Name, "allow_commit_timestamp", "BOOL", "TRUE") if _, err := db.db.ExecContext(ctx, query); err != nil { return fmt.Errorf("failed to insert into INFORMATION_SCHEMA.COLUMN_OPTIONS for table %s: %v", tableName, err) } } } } if err := db.registerInformationSchemaIndexes(ctx, table, &ast.CreateIndex{ Unique: true, NullFiltered: false, Name: &ast.Path{Idents: []*ast.Ident{{Name: "PRIMARY_KEY"}}}, TableName: &ast.Path{Idents: []*ast.Ident{{Name: stmt.Name.Idents[0].Name}}}, Keys: stmt.PrimaryKeys, Storing: nil, InterleaveIn: nil, }); err != nil { return err } return nil } func (db *database) CreateIndex(ctx context.Context, stmt *ast.CreateIndex) error { table, ok := db.tables[stmt.TableName.Idents[0].Name] if !ok { return fmt.Errorf("table does not exist: %v", stmt.Name.Idents[0].Name) } index, err := table.createIndex(stmt) if err != nil { return err } idxType := "INDEX" if index.unique { idxType = "UNIQUE INDEX" } columnsName := strings.Join(QuoteStringSlice(index.IndexColumnNames()), ", ") query := fmt.Sprintf("CREATE %s %s ON %s (%s)", idxType, QuoteString(index.Name()), QuoteString(table.Name), columnsName) if _, err := db.db.ExecContext(ctx, query); err != nil { return fmt.Errorf("failed to create index for %s: %v", index.Name(), err) } if err := db.registerInformationSchemaIndexes(ctx, table, stmt); err != nil { return err } return nil } func (db *database) registerInformationSchemaIndexes(ctx context.Context, table *Table, stmt *ast.CreateIndex) error { schemaName := "" tableName := stmt.TableName.Idents[0].Name if schema, ok := metaTablesReverseMap[tableName]; ok { schemaName = schema[0] tableName = schema[1] } indexName := stmt.Name.Idents[0].Name isUnique := "FALSE" if stmt.Unique { isUnique = "TRUE" } isNullFiltered := "FALSE" if stmt.NullFiltered { isNullFiltered = "TRUE" } indexType := stmt.Name.Idents[0].Name indexState := "NULL" if indexName != "PRIMARY_KEY" { indexType = "INDEX" indexState = `"READ_WRITE"` } managed := "FALSE" // fixed parentTableName := "" if stmt.InterleaveIn != nil { parentTableName = stmt.InterleaveIn.TableName.Name } // register INFORMATION_SCHEMA.INDEXES query := fmt.Sprintf(`INSERT INTO __INFORMATION_SCHEMA__INDEXES VALUES("", %q, %q, %q, %q, %q, %s, %s, %s, %s)`, schemaName, tableName, indexName, indexType, parentTableName, isUnique, isNullFiltered, indexState, managed) if _, err := db.db.ExecContext(ctx, query); err != nil { return fmt.Errorf("failed to insert into INFORMATION_SCHEMA.INDEXES for table %s: %v", tableName, err) } for pos, key := range stmt.Keys { columnName := key.Name.Name column, err := table.getColumn(columnName) if err != nil { return fmt.Errorf("unexpected error: %v", err) } ordering := "ASC" switch key.Dir { case ast.DirectionAsc: ordering = "ASC" case ast.DirectionDesc: ordering = "DESC" } isNullable := "NO" if column.nullable { isNullable = "YES" } spannerType := "" if column.ast != nil { spannerType = schemaTypetoTypString(column.ast.Type) } // register INFORMATION_SCHEMA.INDEX_COLUMNS query := fmt.Sprintf(`INSERT INTO __INFORMATION_SCHEMA__INDEX_COLUMNS VALUES("", %q, %q, %q, %q, %q, %d, %q, %q, %q)`, schemaName, tableName, indexName, indexType, columnName, pos+1, ordering, isNullable, spannerType) if _, err := db.db.ExecContext(ctx, query); err != nil { return fmt.Errorf("failed to insert into INFORMATION_SCHEMA.INDEX_COLUMNS for table %s: %v", tableName, err) } } if stmt.Storing != nil { for _, ident := range stmt.Storing.Columns { columnName := ident.Name column, err := table.getColumn(columnName) if err != nil { return fmt.Errorf("unexpected error: %v", err) } isNullable := "NO" if column.nullable { isNullable = "YES" } spannerType := "" if column.ast != nil { spannerType = schemaTypetoTypString(column.ast.Type) } // register INFORMATION_SCHEMA.INDEX_COLUMNS query := fmt.Sprintf(`INSERT INTO __INFORMATION_SCHEMA__INDEX_COLUMNS VALUES("", %q, %q, %q, %q, %q, %s, %s, %q, %q)`, schemaName, tableName, indexName, indexType, columnName, "NULL", "NULL", isNullable, spannerType) if _, err := db.db.ExecContext(ctx, query); err != nil { return fmt.Errorf("failed to insert into INFORMATION_SCHEMA.INDEX_COLUMNS for table %s: %v", tableName, err) } } } return nil } func (d *database) Close() error { d.cancel() if d.db == nil { return nil } d.schemaMu.Lock() defer d.schemaMu.Unlock() if err := d.db.Close(); err != nil { return err } d.db = nil return nil } func QuoteString(s string) string { return fmt.Sprintf("`%s`", s) } func QuoteStringSlice(ss []string) []string { ss2 := make([]string, len(ss)) for i := range ss { ss2[i] = QuoteString(ss[i]) } return ss2 } ================================================ FILE: server/database_function.go ================================================ // Copyright 2019 Masahiro Sano // // 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 server import ( "database/sql" "fmt" "math" "strconv" "strings" "time" "github.com/dgryski/go-farm" sqlite "github.com/mattn/go-sqlite3" ) func init() { sql.Register("sqlite3_spanner", &sqlite.SQLiteDriver{ ConnectHook: func(conn *sqlite.SQLiteConn) error { for name, fn := range customFunctions { name = strings.ToLower(name) if fn.Func == nil { continue } if err := conn.RegisterFunc(name, fn.Func, true); err != nil { return err } } return nil }, }) } type CustomFunction struct { Name string Func interface{} NArgs int ArgTypes func([]ValueType) bool ReturnType func([]ValueType) ValueType } func exactMatchArgumentTypes(vts ...ValueType) func([]ValueType) bool { return func(vts2 []ValueType) bool { if len(vts) != len(vts2) { return false } for i := range vts { ok := compareValueType(vts[i], vts2[i]) if !ok { return false } } return true } } func staticReturnType(vt ValueType) func([]ValueType) ValueType { return func([]ValueType) ValueType { return vt } } const SqliteArgumentRuntimeErrorPrefix = "_sqlite_argument_runtime_error_: " const SqliteOutOfRangeRuntimeErrorPrefix = "_sqlite_ooo_runtime_error_: " // sqliteArgumentRuntimeError is runtime error to run query in sqlite. // Runtime error only can be returned as string of error in sqlite. // To distinguish error as InvalidArgument or InternalError, the error message // by this has a specific prefix. RowsIterator checks the prefix and returns the error // as InvalidArgument. type sqliteArgumentRuntimeError struct { msg string } func (e *sqliteArgumentRuntimeError) Error() string { return SqliteArgumentRuntimeErrorPrefix + e.msg } type sqliteOutOfRangeRuntimeError struct { msg string } func (e *sqliteOutOfRangeRuntimeError) Error() string { return SqliteOutOfRangeRuntimeErrorPrefix + e.msg } // customFunctionNamesMap is naming map from Spanner's function to sqlite's custom function. // This is used to avoid conflict of reserved name in sqlite. var customFunctionNamesMap map[string]string = map[string]string{ "CURRENT_TIMESTAMP": "___CURRENT_TIMESTAMP", } // customFunctions is functions for spanner. // // sqlite cannot register function which returns interface{}. // If spanner function may return different type value, we need to register multiple functions // for each returned type. var customFunctions map[string]CustomFunction = map[string]CustomFunction{ "SIGN": { Func: sqlite3FnSign, NArgs: 1, ArgTypes: exactMatchArgumentTypes( ValueType{Code: TCInt64}, ), ReturnType: staticReturnType(ValueType{ Code: TCInt64, }), }, "STARTS_WITH": { Func: sqlite3FnStartsWith, NArgs: 2, ArgTypes: exactMatchArgumentTypes( ValueType{Code: TCString}, ValueType{Code: TCString}, ), ReturnType: staticReturnType(ValueType{ Code: TCBool, }), }, "ENDS_WITH": { Func: sqlite3FnEndsWith, NArgs: 2, ArgTypes: exactMatchArgumentTypes( ValueType{Code: TCString}, ValueType{Code: TCString}, ), ReturnType: staticReturnType(ValueType{ Code: TCBool, }), }, "CONCAT": { Func: sqlite3FnConcat, NArgs: -1, ArgTypes: func(vts []ValueType) bool { if len(vts) == 0 { return false } vt := vts[0] for i := range vts { if !compareValueType(vt, vts[i]) { return false } } if vt.Code != TCString && vt.Code != TCBytes { return false } return true }, ReturnType: func(vts []ValueType) ValueType { return vts[0] }, }, "COUNT": { Func: nil, NArgs: 1, ArgTypes: func(vts []ValueType) bool { return true }, ReturnType: staticReturnType(ValueType{ Code: TCInt64, }), }, "MAX": { Func: nil, NArgs: 1, ArgTypes: func(vts []ValueType) bool { code := vts[0].Code if code == TCArray || code == TCStruct { return false } return true }, ReturnType: func(vts []ValueType) ValueType { return vts[0] }, }, "MIN": { Func: nil, NArgs: 1, ArgTypes: func(vts []ValueType) bool { code := vts[0].Code if code == TCArray || code == TCStruct { return false } return true }, ReturnType: func(vts []ValueType) ValueType { return vts[0] }, }, "AVG": { Func: nil, NArgs: -1, ArgTypes: func(vts []ValueType) bool { for _, vt := range vts { if vt.Code != TCInt64 && vt.Code == TCFloat64 { return false } } return true }, ReturnType: staticReturnType(ValueType{ Code: TCFloat64, }), }, "SUM": { Func: nil, NArgs: -1, ArgTypes: func(vts []ValueType) bool { for _, vt := range vts { if vt.Code != TCInt64 && vt.Code == TCFloat64 { return false } } return true }, ReturnType: func(vts []ValueType) ValueType { for _, vt := range vts { if vt.Code == TCFloat64 { return ValueType{Code: TCFloat64} } } return ValueType{Code: TCInt64} }, }, "___EXTRACT_FROM_TIMESTAMP": { Func: sqlite3FnExtractFromTimestamp, NArgs: 3, ArgTypes: func(vts []ValueType) bool { return vts[0].Code == TCString && vts[1].Code == TCTimestamp && vts[2].Code == TCString }, ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCInt64} }, }, "___EXTRACT_FROM_DATE": { Func: sqlite3FnExtractFromDate, NArgs: 2, ArgTypes: func(vts []ValueType) bool { return vts[0].Code == TCString && vts[1].Code == TCDate }, ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCInt64} }, }, "___CAST_INT64_TO_BOOL": { Func: sqlite3FnCastInt64ToBool, NArgs: 1, ArgTypes: func(vts []ValueType) bool { return vts[0].Code == TCInt64 }, ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCBool} }, }, "___CAST_INT64_TO_STRING": { Func: sqlite3FnCastInt64ToString, NArgs: 1, ArgTypes: func(vts []ValueType) bool { return vts[0].Code == TCInt64 }, ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCString} }, }, "___CAST_INT64_TO_FLOAT64": { Func: sqlite3FnCastInt64ToFloat64, NArgs: 1, ArgTypes: func(vts []ValueType) bool { return vts[0].Code == TCInt64 }, ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCFloat64} }, }, "___CAST_FLOAT64_TO_STRING": { Func: sqlite3FnCastFloat64ToString, NArgs: 1, ArgTypes: func(vts []ValueType) bool { return vts[0].Code == TCFloat64 }, ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCString} }, }, "___CAST_FLOAT64_TO_INT64": { Func: sqlite3FnCastFloat64ToInt64, NArgs: 1, ArgTypes: func(vts []ValueType) bool { return vts[0].Code == TCFloat64 }, ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCInt64} }, }, "___CAST_BOOL_TO_STRING": { Func: sqlite3FnCastBoolToString, NArgs: 1, ArgTypes: func(vts []ValueType) bool { return vts[0].Code == TCBool }, ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCString} }, }, "___CAST_BOOL_TO_INT64": { Func: sqlite3FnCastBoolToInt64, NArgs: 1, ArgTypes: func(vts []ValueType) bool { return vts[0].Code == TCBool }, ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCInt64} }, }, "___CAST_STRING_TO_BOOL": { Func: sqlite3FnCastStringToBool, NArgs: 1, ArgTypes: func(vts []ValueType) bool { return vts[0].Code == TCString }, ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCBool} }, }, "___CAST_STRING_TO_INT64": { Func: sqlite3FnCastStringToInt64, NArgs: 1, ArgTypes: func(vts []ValueType) bool { return vts[0].Code == TCString }, ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCInt64} }, }, "___CAST_STRING_TO_FLOAT64": { Func: sqlite3FnCastStringToFloat64, NArgs: 1, ArgTypes: func(vts []ValueType) bool { return vts[0].Code == TCString }, ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCFloat64} }, }, "___CAST_STRING_TO_DATE": { Func: sqlite3FnCastStringToDate, NArgs: 1, ArgTypes: func(vts []ValueType) bool { return vts[0].Code == TCString }, ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCDate} }, }, "___CAST_STRING_TO_TIMESTAMP": { Func: sqlite3FnCastStringToTimestamp, NArgs: 1, ArgTypes: func(vts []ValueType) bool { return vts[0].Code == TCString }, ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCTimestamp} }, }, "___CAST_DATE_TO_STRING": { Func: sqlite3FnCastDateToString, NArgs: 1, ArgTypes: func(vts []ValueType) bool { return vts[0].Code == TCDate }, ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCString} }, }, "___CAST_DATE_TO_TIMESTAMP": { Func: sqlite3FnCastDateToTimestamp, NArgs: 1, ArgTypes: func(vts []ValueType) bool { return vts[0].Code == TCDate }, ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCTimestamp} }, }, "___CAST_TIMESTAMP_TO_STRING": { Func: sqlite3FnCastTimestampToString, NArgs: 1, ArgTypes: func(vts []ValueType) bool { return vts[0].Code == TCTimestamp }, ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCString} }, }, "___CAST_TIMESTAMP_TO_DATE": { Func: sqlite3FnCastTimestampToDate, NArgs: 1, ArgTypes: func(vts []ValueType) bool { return vts[0].Code == TCTimestamp }, ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCDate} }, }, "PENDING_COMMIT_TIMESTAMP": getCustomFunctionForCurrentTime(), "___CURRENT_TIMESTAMP": getCustomFunctionForCurrentTime(), "IFNULL": { Func: nil, NArgs: 2, ArgTypes: func(vts []ValueType) bool { if len(vts) == 0 { return false } vt := vts[0] for i := range vts { if !compareValueType(vt, vts[i]) { return false } } return true }, ReturnType: func(vts []ValueType) ValueType { return vts[0] }, }, "NULLIF": { Func: nil, NArgs: 2, ArgTypes: func(vts []ValueType) bool { if len(vts) == 0 { return false } vt := vts[0] for i := range vts { if !compareValueType(vt, vts[i]) { return false } } return true }, ReturnType: func(vts []ValueType) ValueType { return vts[0] }, }, "FARM_FINGERPRINT": { Func: sqlite3FnFarmFingerprint, NArgs: 1, ArgTypes: func(vts []ValueType) bool { return vts[0].Code == TCString }, ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCInt64} }, }, "MOD": { Func: sqlite3FnMod, NArgs: 2, ArgTypes: exactMatchArgumentTypes( ValueType{Code: TCInt64}, ValueType{Code: TCInt64}, ), ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCInt64} }, }, "GENERATE_ARRAY": { Func: sqlite3FnGenerateArray, NArgs: -2, ArgTypes: func(vts []ValueType) bool { if len(vts) < 2 || len(vts) > 3 { return false } if vts[0].Code == TCInt64 && vts[1].Code == TCInt64 { if len(vts) == 3 { return vts[2].Code == TCInt64 } return true } return false }, ReturnType: func(vts []ValueType) ValueType { return ValueType{ Code: TCArray, ArrayType: &ValueType{Code: TCInt64}, } }, }, } func sqlite3FnGenerateArray(xs ...int64) []byte { step := int64(1) if len(xs) == 3 { step = xs[2] } a, b := xs[0], xs[1] res := make([]byte, 0, b-a) for i := a; i <= b; i += step { res = append(res, byte(i)) } return res } func sqlite3FnMod(a, b int64) int64 { return a % b } func sqlite3FnFarmFingerprint(s string) int64 { return int64(farm.Fingerprint64([]byte(s))) } func sqlite3FnSign(x int64) int64 { if x > 0 { return 1 } if x < 0 { return -1 } return 0 } func sqlite3FnStartsWith(a, b string) bool { return strings.HasPrefix(a, b) } func sqlite3FnEndsWith(a, b string) bool { return strings.HasSuffix(a, b) } func sqlite3FnConcat(xs ...string) string { return strings.Join(xs, "") } // sqlite3FnExtractFromTimestamp is simulation function of EXTRACT. // It supports except DATE part. func sqlite3FnExtractFromTimestamp(part string, timestamp string, tz string) (int64, error) { tzErr := &sqliteOutOfRangeRuntimeError{msg: fmt.Sprintf("Invalid time zone: %s", tz)} if len(tz) == 0 { return 0, tzErr } var location *time.Location // (+|-)H[H][:M[M]] if tz[0] == '-' || tz[0] == '+' { s := tz var neg bool if s[0] == '-' { neg = true } s = s[1:] var colonFound bool hour := -1 min := 0 for _, c := range s { if c == ':' { min = -1 colonFound = true continue } if c < '0' && '9' < c { return 0, tzErr } n := int(c - '0') if colonFound { if min == -1 { min = n } else { min = 10*min + n } } else { if hour == -1 { hour = n } else { hour = 10*hour + n } } } if hour == -1 || min == -1 { return 0, tzErr } if hour > 24 || min > 60 { return 0, tzErr } offset := hour*3600 + min*60 if neg { offset = -offset } location = time.FixedZone(tz, offset) } else { loc, err := time.LoadLocation(tz) if err != nil { return 0, tzErr } location = loc } t, err := time.Parse(time.RFC3339Nano, timestamp) if err != nil { // Must not happen return 0, fmt.Errorf("___EXTRACT_FROM_TIMESTAMP: unexpected format %q as timestamp: %v", timestamp, err) } t = t.In(location) switch strings.ToUpper(part) { case "NANOSECOND": return int64(t.Nanosecond()), nil case "MICROSECOND": return int64(t.Nanosecond() / 1000), nil case "MILLISECOND": return int64(t.Nanosecond() / 1000000), nil case "SECOND": return int64(t.Second()), nil case "MINUTE": return int64(t.Minute()), nil case "HOUR": return int64(t.Hour()), nil case "DAYOFWEEK": return int64(t.Weekday()), nil case "DAY": return int64(t.Day()), nil case "DAYOFYEAR": return int64(t.YearDay()), nil case "WEEK": // TODO: calculate week from timestamp return 0, fmt.Errorf("___EXTRACT_FROM_TIMESTAMP: WEEK not supported for now") case "ISOWEEK": _, w := t.ISOWeek() return int64(w), nil case "MONTH": return int64(t.Month()), nil case "QUARTER": // 1 for Jan-Mar, 2 for Apr-Jun, 3 for Jul-Sep, 4 for Oct-Dec m := t.Month() return int64(m/4 + 1), nil case "YEAR": return int64(t.Year()), nil case "ISOYEAR": y, _ := t.ISOWeek() return int64(y), nil default: // Must not happen return 0, fmt.Errorf("___EXTRACT_FROM_TIMESTAMP: unexpected part: %s", part) } } // sqlite3FnExtractFromDate is simulation function of EXTRACT. func sqlite3FnExtractFromDate(part string, date string) (int64, error) { t, err := time.Parse("2006-01-02", date) if err != nil { // Must not happen return 0, fmt.Errorf("___EXTRACT_FROM_DATE: unexpected format %q as date: %v", date, err) } switch strings.ToUpper(part) { case "DAYOFWEEK": return int64(t.Weekday()), nil case "DAY": return int64(t.Day()), nil case "DAYOFYEAR": return int64(t.YearDay()), nil case "WEEK": // TODO: calculate week from timestamp return 0, fmt.Errorf("___EXTRACT_FROM_DATE: WEEK not supported for now") case "ISOWEEK": _, w := t.ISOWeek() return int64(w), nil case "MONTH": return int64(t.Month()), nil case "QUARTER": // 1 for Jan-Mar, 2 for Apr-Jun, 3 for Jul-Sep, 4 for Oct-Dec m := t.Month() return int64(m/4 + 1), nil case "YEAR": return int64(t.Year()), nil case "ISOYEAR": y, _ := t.ISOWeek() return int64(y), nil default: // Must not happen return 0, fmt.Errorf("___EXTRACT_FROM_DATE: unexpected part: %s", part) } } func sqlite3FnCastInt64ToBool(i int64) bool { return i != 0 } func sqlite3FnCastInt64ToString(i int64) string { return strconv.FormatInt(i, 10) } func sqlite3FnCastInt64ToFloat64(i int64) float64 { return float64(i) } func sqlite3FnCastFloat64ToString(f float64) string { if math.IsInf(f, 0) { if f < 0 { return "-inf" } return "inf" } return strconv.FormatFloat(f, 'g', -1, 64) } func sqlite3FnCastFloat64ToInt64(f float64) (int64, error) { if math.IsNaN(f) { // OutOfRange error msg := "Illegal conversion of non-finite floating point number to an integer: nan" return 0, &sqliteOutOfRangeRuntimeError{msg: msg} } if math.IsInf(f, 0) { // OutOfRange error inf := "inf" if f < 0 { inf = "-inf" } msg := "Illegal conversion of non-finite floating point number to an integer: " + inf return 0, &sqliteOutOfRangeRuntimeError{msg: msg} } if f < 0 { return int64(f - 0.5), nil } return int64(f + 0.5), nil } func sqlite3FnCastBoolToString(b bool) string { if b { return "TRUE" } return "FALSE" } func sqlite3FnCastBoolToInt64(b bool) int64 { if b { return 1 } return 0 } func sqlite3FnCastStringToBool(s string) (bool, error) { ss := strings.ToUpper(s) if ss == "TRUE" { return true, nil } if ss == "FALSE" { return false, nil } // OutOfRange error return false, &sqliteOutOfRangeRuntimeError{msg: fmt.Sprintf("Bad bool value: %s", s)} } func sqlite3FnCastStringToInt64(s string) (int64, error) { if s == "" { // OutOfRange error return 0, &sqliteOutOfRangeRuntimeError{msg: fmt.Sprintf("Bad int64 value: %s", s)} } orig := s var neg bool if s[0] == '+' { s = s[1:] } else if s[0] == '-' { neg = true s = s[1:] } // Base is available only for 10 or 16 in spanner. base := 10 if len(s) > 2 && s[0] == '0' && s[1] == 'x' { base = 16 s = s[2:] } n, err := strconv.ParseInt(s, base, 64) if err != nil { // OutOfRange error return 0, &sqliteOutOfRangeRuntimeError{msg: fmt.Sprintf("Bad int64 value: %s", orig)} } if n < 0 { // OutOfRange error return 0, &sqliteOutOfRangeRuntimeError{msg: fmt.Sprintf("Bad int64 value: %s", orig)} } if neg { n = -n } return n, nil } func sqlite3FnCastStringToFloat64(s string) (float64, error) { n, err := strconv.ParseFloat(s, 64) if err != nil { // OutOfRange error return 0, &sqliteOutOfRangeRuntimeError{msg: fmt.Sprintf("Bad double value: %s", s)} } return n, nil } func sqlite3FnCastStringToDate(s string) (string, error) { t, ok := parseDateLiteral(s) if !ok { // InvalidArgument error return "", &sqliteArgumentRuntimeError{msg: fmt.Sprintf("Could not cast literal %q to type DATE", s)} } return t.Format("2006-01-02"), nil } func sqlite3FnCastStringToTimestamp(s string) (string, error) { t, ok := parseTimestampLiteral(s) if !ok { // InvalidArgument error return "", &sqliteArgumentRuntimeError{msg: fmt.Sprintf("Could not cast literal %q to type TIMESTAMP", s)} } return t.Format(time.RFC3339Nano), nil } func sqlite3FnCastDateToString(s string) (string, error) { t, ok := parseDateLiteral(s) if !ok { // InvalidArgument error return "", &sqliteArgumentRuntimeError{msg: fmt.Sprintf("Could not cast literal %q to type STRING", s)} } return t.Format("2006-01-02"), nil } func sqlite3FnCastDateToTimestamp(s string) (string, error) { t, ok := parseDateLiteral(s) if !ok { // InvalidArgument error return "", &sqliteArgumentRuntimeError{msg: fmt.Sprintf("Could not cast literal %q to type TIMESTAMP", s)} } return t.UTC().Format(time.RFC3339Nano), nil } func sqlite3FnCastTimestampToString(s string) (string, error) { t, ok := parseTimestampLiteral(s) if !ok { // InvalidArgument error return "", &sqliteArgumentRuntimeError{msg: fmt.Sprintf("Could not cast literal %q to type STRING", s)} } return t.In(parseLocation).Format("2006-01-02 15:04:05.999999999-07"), nil } func sqlite3FnCastTimestampToDate(s string) (string, error) { t, ok := parseTimestampLiteral(s) if !ok { // InvalidArgument error return "", &sqliteArgumentRuntimeError{msg: fmt.Sprintf("Could not cast literal %q to type DATE", s)} } return t.In(parseLocation).Format("2006-01-02"), nil } func sqlite3FnCurrentTimestamp() string { return time.Now().UTC().Format(time.RFC3339Nano) } func getCustomFunctionForCurrentTime() CustomFunction { return CustomFunction{ Func: sqlite3FnCurrentTimestamp, NArgs: 0, ArgTypes: func(vts []ValueType) bool { return true }, ReturnType: func(vts []ValueType) ValueType { return ValueType{Code: TCTimestamp} }, } } ================================================ FILE: server/database_json.go ================================================ // Copyright 2020 Masahiro Sano // // 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. // +build json1 package server func useSqliteJSON() { return } ================================================ FILE: server/database_nonjson.go ================================================ // Copyright 2020 Masahiro Sano // // 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. // +build !json1 package server func useSqliteJSON() { panic(`Require "json1" build tag to use some features`) } ================================================ FILE: server/database_query_test.go ================================================ // Copyright 2019 Masahiro Sano // // 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 server import ( "context" "math" "regexp" "testing" "time" "github.com/cloudspannerecosystem/memefish" "github.com/cloudspannerecosystem/memefish/token" cmp "github.com/google/go-cmp/cmp" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func TestQuery(t *testing.T) { ctx := context.Background() db := newDatabase() for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } for _, query := range []string{ `INSERT INTO Simple VALUES(100, "xxx")`, `INSERT INTO Simple VALUES(200, "yyy")`, `INSERT INTO Simple VALUES(300, "zzz")`, `INSERT INTO CompositePrimaryKeys VALUES(1, "aaa", 1, 0, "x1", "y1", "z")`, `INSERT INTO CompositePrimaryKeys VALUES(2, "bbb", 2, 0, "x1", "y2", "z")`, `INSERT INTO CompositePrimaryKeys VALUES(3, "bbb", 3, 0, "x1", "y3", "z")`, `INSERT INTO CompositePrimaryKeys VALUES(4, "ccc", 3, 0, "x2", "y4", "z")`, `INSERT INTO CompositePrimaryKeys VALUES(5, "ccc", 4, 0, "x2", "y5", "z")`, `INSERT INTO FullTypes VALUES("xxx", "xxx", "xxx", true, true, "eHl6", "eHl6", "2012-03-04T12:34:56.123456789Z", "2012-03-04T12:34:56.123456789Z", 100, 100, 0.5, 0.5, "2012-03-04", "2012-03-04" )`, `INSERT INTO FullTypes VALUES("yyy", "yyy", "yyy", false, false, "eHl6", "eHl6", "2012-03-04T12:34:56.123456789Z", "2012-03-04T12:34:56.123456789Z", 200, 200, 0.5, 0.5, "2012-03-05", "2012-03-05" )`, `INSERT INTO ArrayTypes VALUES(100, json_array("xxx1", "xxx2"), json_array(true, false), json_array("eHl6", "eHl6"), json_array("2012-03-04T12:34:56.123456789Z", "2012-03-04T12:34:56.999999999Z"), json_array(1, 2), json_array(0.1, 0.2), json_array("2012-03-04", "2012-03-05") )`, `INSERT INTO JoinA VALUES(100, "xxx")`, `INSERT INTO JoinA VALUES(200, "yyy")`, `INSERT INTO JoinA VALUES(300, "zzz")`, `INSERT INTO JoinB VALUES(100, "aaa")`, `INSERT INTO JoinB VALUES(200, "bbb")`, `INSERT INTO JoinB VALUES(400, "ddd")`, "INSERT INTO `From` VALUES(1, 1, 1)", } { if _, err := db.db.ExecContext(ctx, query); err != nil { t.Fatalf("Insert failed: %v", err) } } table := map[string][]struct { name string sql string params map[string]Value expected [][]interface{} names []string code codes.Code msg *regexp.Regexp }{ "Simple": { { name: "NoTable_IntLiteral", sql: `SELECT 1`, expected: [][]interface{}{ []interface{}{int64(1)}, }, }, { name: "NoTable_StringLiteral", sql: `SELECT "foo"`, expected: [][]interface{}{ []interface{}{"foo"}, }, }, { name: "NoTable_NullLiteral", sql: `SELECT NULL`, expected: [][]interface{}{ []interface{}{nil}, }, }, { name: "NoTable_Params_Int", sql: `SELECT @foo`, params: map[string]Value{ "foo": makeTestValue(int64(100)), }, expected: [][]interface{}{ []interface{}{int64(100)}, }, }, { name: "NoTable_Params_String", sql: `SELECT @foo`, params: map[string]Value{ "foo": makeTestValue("xxx"), }, expected: [][]interface{}{ []interface{}{"xxx"}, }, }, { name: "Simple_Star", sql: "SELECT * FROM Simple", expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "yyy"}, []interface{}{int64(300), "zzz"}, }, }, { name: "TableNotFound", sql: "SELECT * FROM xxx", code: codes.InvalidArgument, msg: regexp.MustCompile(`Table not found: xxx`), }, // TODO: memefish cannot parse this sql // { // name: "QueryParam_TableName", // sql: "SELECT 1 FROM @table", // code: codes.InvalidArgument, // msg: regexp.MustCompile(`Query parameters cannot be used in place of table names`), // }, { name: "Star_WithoutFromClause", sql: "SELECT *", code: codes.InvalidArgument, msg: regexp.MustCompile(`SELECT \* must have a FROM clause`), }, { name: "Simple_Identifer", sql: "SELECT Value, Id FROM Simple", expected: [][]interface{}{ []interface{}{"xxx", int64(100)}, []interface{}{"yyy", int64(200)}, []interface{}{"zzz", int64(300)}, }, }, { name: "Identifer_NotFound", sql: "SELECT x.* FROM Simple a", code: codes.InvalidArgument, msg: regexp.MustCompile(`Unrecognized name: x`), }, { name: "Identifer_NotFound2", sql: "SELECT foo FROM Simple", code: codes.InvalidArgument, msg: regexp.MustCompile(`Unrecognized name: foo`), }, { name: "Identifer_NotFound3", sql: "SELECT a.foo FROM Simple a", code: codes.InvalidArgument, msg: regexp.MustCompile("Name foo not found inside a"), }, { name: "Identifer_NotFound_WithoutFromClause", sql: "SELECT x", code: codes.InvalidArgument, msg: regexp.MustCompile(`Unrecognized name: x`), }, { name: "PathIdentifer_InvalidFieldAccess", sql: "SELECT a.Id.zzz FROM Simple a", code: codes.InvalidArgument, msg: regexp.MustCompile(`Cannot access field zzz on a value with type INT64`), }, { name: "PathIdentifer_NotFound_WithoutFromClause", sql: "SELECT x.*", code: codes.InvalidArgument, msg: regexp.MustCompile(`Unrecognized name: x`), }, { name: "Simple_Alias", sql: "SELECT Id a, Value b FROM Simple", expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "yyy"}, []interface{}{int64(300), "zzz"}, }, }, { name: "Alias_DuplicateTableAlias", sql: "SELECT 1 FROM Simple a, Simple a", // code: codes.InvalidArgument, code: codes.Unknown, // TODO msg: regexp.MustCompile(`Duplicate table alias a in the same FROM clause`), }, { name: "Alias_AmbiguousColumn", sql: "SELECT Id FROM Simple a, Simple b", code: codes.InvalidArgument, msg: regexp.MustCompile(`Column name Id is ambiguous`), }, { name: "ValueTypes_FullTypes", sql: `SELECT * FROM FullTypes`, expected: [][]interface{}{ { "xxx", "xxx", "xxx", true, true, []byte("eHl6"), []byte("eHl6"), "2012-03-04T12:34:56.123456789Z", "2012-03-04T12:34:56.123456789Z", int64(100), int64(100), float64(0.5), float64(0.5), "2012-03-04", "2012-03-04", }, { "yyy", "yyy", "yyy", false, false, []byte("eHl6"), []byte("eHl6"), "2012-03-04T12:34:56.123456789Z", "2012-03-04T12:34:56.123456789Z", int64(200), int64(200), float64(0.5), float64(0.5), "2012-03-05", "2012-03-05", }, }, }, { name: "ValueTypes_ArrayTypes", sql: `SELECT * FROM ArrayTypes`, expected: [][]interface{}{ { int64(100), makeTestArray(TCString, "xxx1", "xxx2"), makeTestArray(TCBool, true, false), [][]uint8{{0x78, 0x79, 0x7a}, {0x78, 0x79, 0x7a}}, makeTestArray(TCString, "2012-03-04T12:34:56.123456789Z", "2012-03-04T12:34:56.999999999Z"), makeTestArray(TCInt64, int64(1), int64(2)), makeTestArray(TCFloat64, float64(0.1), float64(0.2)), makeTestArray(TCString, "2012-03-04", "2012-03-05"), }, }, }, { name: "EscapeKeyword_Table", sql: "SELECT * FROM `From`", names: []string{"ALL", "CAST", "JOIN"}, expected: [][]interface{}{{int64(1), int64(1), int64(1)}}, }, { name: "EscapeKeyword_TableAlias", sql: "SELECT * FROM `From` AS `ALL`", names: []string{"ALL", "CAST", "JOIN"}, expected: [][]interface{}{{int64(1), int64(1), int64(1)}}, }, { name: "EscapeKeyword_Columns", sql: "SELECT `ALL`, `CAST`, `JOIN` FROM `From`", names: []string{"ALL", "CAST", "JOIN"}, expected: [][]interface{}{{int64(1), int64(1), int64(1)}}, }, { name: "EscapeKeyword_ColumnsWithAlias", sql: "SELECT `ALL`.`ALL`, `ALL`.`CAST`, `ALL`.`JOIN` FROM `From` AS `ALL`", names: []string{"ALL", "CAST", "JOIN"}, expected: [][]interface{}{{int64(1), int64(1), int64(1)}}, }, { name: "EscapeKeyword_ColumnAlias", sql: "SELECT `ALL` AS `AND`, `CAST` `OR`, `JOIN` AS `IF` FROM `From`", names: []string{"AND", "OR", "IF"}, expected: [][]interface{}{{int64(1), int64(1), int64(1)}}, }, }, "SimpleWhere": { { name: "Simple_Where_IntLiteral", sql: "SELECT Id, Value FROM Simple WHERE Id = 200", expected: [][]interface{}{ []interface{}{int64(200), "yyy"}, }, }, { name: "Simple_Where_IntLiteral2", sql: "SELECT Id, Value FROM Simple WHERE Id >= 200", expected: [][]interface{}{ []interface{}{int64(200), "yyy"}, []interface{}{int64(300), "zzz"}, }, }, { name: "Simple_Where_IntLiteral3", sql: "SELECT Id, Value FROM Simple WHERE Id = +200", expected: [][]interface{}{ []interface{}{int64(200), "yyy"}, }, }, { name: "Simple_Where_IntLiteral4", sql: "SELECT Id, Value FROM Simple WHERE Id = -200", expected: nil, }, { name: "Simple_Where_StringLiteral", sql: `SELECT Id, Value FROM Simple WHERE Value = "xxx"`, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, { name: "Simple_Where_StringLiteral2", sql: `SELECT Id, Value FROM Simple WHERE Value > "xxx"`, expected: [][]interface{}{ []interface{}{int64(200), "yyy"}, []interface{}{int64(300), "zzz"}, }, }, { name: "Simple_Where_Param", sql: `SELECT Id, Value FROM Simple WHERE Id = @id`, params: map[string]Value{ "id": makeTestValue(100), }, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, { name: "Simple_Where_AND", sql: `SELECT Id, Value FROM Simple WHERE Id > @id AND Value = @val`, params: map[string]Value{ "id": makeTestValue(100), "val": makeTestValue("zzz"), }, expected: [][]interface{}{ []interface{}{int64(300), "zzz"}, }, }, { name: "Simple_Where_Paren", sql: `SELECT Id, Value FROM Simple WHERE Id >= @id AND (Value = @val1 OR Value = @val2)`, params: map[string]Value{ "id": makeTestValue(100), "val1": makeTestValue("zzz"), "val2": makeTestValue("xxx"), }, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(300), "zzz"}, }, }, { name: "Simple_Where_LIKE", sql: `SELECT Id, Value FROM Simple WHERE Value LIKE "x%"`, params: map[string]Value{}, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, { name: "Simple_Where_NOT_LIKE", sql: `SELECT Id, Value FROM Simple WHERE Value NOT LIKE "x%"`, params: map[string]Value{}, expected: [][]interface{}{ []interface{}{int64(200), "yyy"}, []interface{}{int64(300), "zzz"}, }, }, { name: "Simple_Where_IN", sql: `SELECT Id, Value FROM Simple WHERE Id IN (100, 300)`, params: map[string]Value{}, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(300), "zzz"}, }, }, { name: "Simple_Where_NOT_IN", sql: `SELECT Id, Value FROM Simple WHERE Id NOT IN (100, 300)`, params: map[string]Value{}, expected: [][]interface{}{ []interface{}{int64(200), "yyy"}, }, }, { name: "Simple_Where_IS_NULL", sql: `SELECT Id, Value FROM Simple WHERE Value IS NULL`, params: map[string]Value{}, expected: nil, }, { name: "Simple_Where_IS_NOT_NULL", sql: `SELECT Id, Value FROM Simple WHERE Value IS NOT NULL`, params: map[string]Value{}, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "yyy"}, []interface{}{int64(300), "zzz"}, }, }, { name: "Simple_Where_IS_BOOL", sql: `SELECT Id, Value FROM Simple WHERE Id IS TRUE`, params: map[string]Value{}, expected: nil, }, { name: "Simple_Where_IS_NOT_BOOL", sql: `SELECT Id, Value FROM Simple WHERE Id IS NOT TRUE`, params: map[string]Value{}, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "yyy"}, []interface{}{int64(300), "zzz"}, }, }, { name: "Simple_Where_BETWEEN", sql: `SELECT Id, Value FROM Simple WHERE Id BETWEEN 200 AND 300`, params: map[string]Value{}, expected: [][]interface{}{ []interface{}{int64(200), "yyy"}, []interface{}{int64(300), "zzz"}, }, }, { name: "Simple_Where_NOT_BETWEEN", sql: `SELECT Id, Value FROM Simple WHERE Id NOT BETWEEN 200 AND 300`, params: map[string]Value{}, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, { name: "Simple_Where_STARTS_WITH", sql: `SELECT Id, Value FROM Simple WHERE STARTS_WITH(Value, "x")`, params: map[string]Value{}, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, { name: "Simple_Where_STARTS_WITH_PARAM", sql: `SELECT Id, Value FROM Simple WHERE STARTS_WITH(Value, @x)`, params: map[string]Value{ "x": makeTestValue("xx"), }, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, }, "SimpleGroup": { { name: "Simple_GROUP", sql: `SELECT Id, Value FROM Simple GROUP BY Value`, params: map[string]Value{}, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "yyy"}, []interface{}{int64(300), "zzz"}, }, }, { name: "Simple_GROUP_HAVING", sql: `SELECT Id, Value FROM Simple GROUP BY Value HAVING Value > "xxx"`, params: map[string]Value{}, expected: [][]interface{}{ []interface{}{int64(200), "yyy"}, []interface{}{int64(300), "zzz"}, }, }, }, "LimitOffset": { { name: "Simple_LIMIT", sql: `SELECT Id, Value FROM Simple LIMIT 1`, params: map[string]Value{}, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, { name: "Simple_LIMIT_Param", sql: `SELECT Id, Value FROM Simple LIMIT @limit`, params: map[string]Value{ "limit": makeTestValue(2), }, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "yyy"}, }, }, { name: "Simple_LIMIT_OFFSET", sql: `SELECT Id, Value FROM Simple LIMIT 1 OFFSET 1`, params: map[string]Value{}, expected: [][]interface{}{ []interface{}{int64(200), "yyy"}, }, }, { name: "Simple_LIMIT_OFFSET_Param", sql: `SELECT Id, Value FROM Simple LIMIT @limit OFFSET @offset`, params: map[string]Value{ "limit": makeTestValue(1), "offset": makeTestValue(1), }, expected: [][]interface{}{ []interface{}{int64(200), "yyy"}, }, }, { name: "Limit_InvalidType", sql: `SELECT 1 FROM Simple LIMIT @foo`, params: map[string]Value{ "foo": makeTestValue("xx"), }, code: codes.InvalidArgument, msg: regexp.MustCompile(`^LIMIT expects an integer literal or parameter`), }, { name: "Offset_InvalidType", sql: `SELECT 1 FROM Simple LIMIT 1 OFFSET @foo`, params: map[string]Value{ "foo": makeTestValue("xx"), }, code: codes.InvalidArgument, msg: regexp.MustCompile(`^OFFSET expects an integer literal or parameter`), }, }, "OrderBy": { { name: "Simple_ORDER", sql: `SELECT Id, Value FROM Simple ORDER BY Id DESC`, params: map[string]Value{}, expected: [][]interface{}{ []interface{}{int64(300), "zzz"}, []interface{}{int64(200), "yyy"}, []interface{}{int64(100), "xxx"}, }, }, { name: "Simple_ORDER2", sql: `SELECT Id, Value FROM Simple ORDER BY Id DESC, Value`, params: map[string]Value{}, expected: [][]interface{}{ []interface{}{int64(300), "zzz"}, []interface{}{int64(200), "yyy"}, []interface{}{int64(100), "xxx"}, }, }, { name: "EscapeKeyword", sql: "SELECT * FROM `From` ORDER BY `ALL`", names: []string{"ALL", "CAST", "JOIN"}, expected: [][]interface{}{{int64(1), int64(1), int64(1)}}, }, }, "Array": { { name: "ArrayIndex_OFFSET1", sql: `SELECT [1, 2, 3][OFFSET(0)]`, expected: [][]interface{}{ []interface{}{int64(1)}, }, }, { name: "ArrayIndex_OFFSET2", sql: `SELECT [1, 2, 3][OFFSET(2)]`, expected: [][]interface{}{ []interface{}{int64(3)}, }, }, { name: "ArrayIndex_OFFSET_Param", sql: `SELECT [1, 2, 3][OFFSET(@foo)]`, params: map[string]Value{ "foo": makeTestValue(1), }, expected: [][]interface{}{ []interface{}{int64(2)}, }, }, { name: "ArrayIndex_ORDINAL1", sql: `SELECT [1, 2, 3][ORDINAL(1)]`, expected: [][]interface{}{ []interface{}{int64(1)}, }, }, { name: "ArrayIndex_ORDINAL3", sql: `SELECT [1, 2, 3][ORDINAL(3)]`, expected: [][]interface{}{ []interface{}{int64(3)}, }, }, { name: "ArrayIndex_ORDINAL_Param", sql: `SELECT [1, 2, 3][ORDINAL(@foo)]`, params: map[string]Value{ "foo": makeTestValue(1), }, expected: [][]interface{}{ []interface{}{int64(1)}, }, }, { name: "ArrayLiteral_Empty", sql: `SELECT []`, expected: [][]interface{}{ []interface{}{makeTestArray(TCInt64)}, }, }, { name: "ArrayLiteral_IntLiteral", sql: `SELECT [1, 2, 3]`, expected: [][]interface{}{ []interface{}{makeTestArray(TCInt64, 1, 2, 3)}, }, }, { name: "ArrayLiteral_Ident", sql: `SELECT [1, Id] FROM Simple`, expected: [][]interface{}{ []interface{}{makeTestArray(TCInt64, 1, 100)}, []interface{}{makeTestArray(TCInt64, 1, 200)}, []interface{}{makeTestArray(TCInt64, 1, 300)}, }, }, { name: "ArrayLiteral_Params", sql: `SELECT ["xxx", @foo]`, params: map[string]Value{ "foo": makeTestValue("yyy"), }, expected: [][]interface{}{ []interface{}{makeTestArray(TCString, "xxx", "yyy")}, }, }, { name: "ArrayLiteral_IncompatibleElements", sql: `SELECT [100, "xxx"]`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Array elements of types {.*} do not have a common supertype`), }, { name: "ArrayLiteral_Unnest_ImcompatibleElements", sql: `SELECT * FROM UNNEST (["xxx", 1])`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Array elements of types {.*} do not have a common supertype`), }, { name: "ArrayLiteral_Unnest_Bool", sql: `SELECT 1 FROM UNNEST (true)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Values referenced in UNNEST must be arrays. UNNEST contains expression of type BOOL`), }, { name: "ArrayLiteral_In_Unnest_Bool", sql: `SELECT 1 FROM Simple WHERE 1 IN UNNEST (true)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Second argument of IN UNNEST must be an array but was BOOL`), }, // { // name: "ArrayLiteral_IntAndFloat", // sql: `SELECT [100, 0.1]`, // expected: [][]interface{}{ // []interface{}{makeTestArray(TCFloat64, 0.1, 0.1)}, // }, // }, { name: "ArraySubquery_Simple", sql: `SELECT ARRAY(SELECT Id FROM Simple)`, expected: [][]interface{}{ []interface{}{makeTestArray(TCInt64, int64(100), int64(200), int64(300))}, }, }, { name: "ArraySubquery_Simple_MultiColumns", sql: `SELECT ARRAY(SELECT Id, Id FROM Simple)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^ARRAY subquery cannot have more than one column unless using SELECT AS STRUCT to build STRUCT values`), }, { name: "ArraySubquery_Star", sql: `SELECT x FROM (SELECT ARRAY(SELECT * FROM (SELECT Id FROM Simple)) x)`, expected: [][]interface{}{ []interface{}{makeTestArray(TCInt64, int64(100), int64(200), int64(300))}, }, }, { name: "ArraySubquery_NestedArray", sql: `SELECT ARRAY(SELECT [1,2,3])`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Cannot use array subquery with column of type ARRAY because nested arrays are not supported`), }, { name: "ArrayLiteral_NestedArray", sql: `SELECT [(SELECT [1,2,3])]`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Cannot construct array with element type ARRAY because nested arrays are not supported`), }, }, "Timestamp": { { name: "TimestampLiteral_Date", sql: `SELECT TIMESTAMP "1999-01-02"`, expected: [][]interface{}{ []interface{}{"1999-01-02T08:00:00Z"}, }, }, { name: "TimestampLiteral_Date2", sql: `SELECT TIMESTAMP "1999-1-2"`, expected: [][]interface{}{ []interface{}{"1999-01-02T08:00:00Z"}, }, }, { name: "TimestampLiteral_WithoutNano", sql: `SELECT TIMESTAMP "1999-01-02 12:34:56"`, expected: [][]interface{}{ []interface{}{"1999-01-02T20:34:56Z"}, }, }, { name: "TimestampLiteral_WithoutNano2", sql: `SELECT TIMESTAMP "1999-01-02 1:2:3"`, expected: [][]interface{}{ []interface{}{"1999-01-02T09:02:03Z"}, }, }, { name: "TimestampLiteral_WithoutNano_T", sql: `SELECT TIMESTAMP "1999-01-02T12:34:56"`, expected: [][]interface{}{ []interface{}{"1999-01-02T20:34:56Z"}, }, }, { name: "TimestampLiteral_WithNano", sql: `SELECT TIMESTAMP "1999-01-02 1:2:3.123456789"`, expected: [][]interface{}{ []interface{}{"1999-01-02T09:02:03.123456789Z"}, }, }, { name: "TimestampLiteral_WithNano2", sql: `SELECT TIMESTAMP "1999-01-02 01:02:03.123456"`, expected: [][]interface{}{ []interface{}{"1999-01-02T09:02:03.123456Z"}, }, }, { name: "TimestampLiteral_WithNano3", sql: `SELECT TIMESTAMP "1999-01-02 01:02:03.12"`, expected: [][]interface{}{ []interface{}{"1999-01-02T09:02:03.12Z"}, }, }, { name: "TimestampLiteral_WithNano_T", sql: `SELECT TIMESTAMP "1999-01-02T1:2:3.123456789"`, expected: [][]interface{}{ []interface{}{"1999-01-02T09:02:03.123456789Z"}, }, }, { name: "TimestampLiteral_Timezone", sql: `SELECT TIMESTAMP "1999-01-02 12:02:03Z"`, expected: [][]interface{}{ []interface{}{"1999-01-02T12:02:03Z"}, }, }, { name: "TimestampLiteral_Timezone2", sql: `SELECT TIMESTAMP "1999-01-02 12:02:03+01"`, expected: [][]interface{}{ []interface{}{"1999-01-02T11:02:03Z"}, }, }, { name: "TimestampLiteral_Timezone3", sql: `SELECT TIMESTAMP "1999-01-02 12:02:03-01"`, expected: [][]interface{}{ []interface{}{"1999-01-02T13:02:03Z"}, }, }, { name: "TimestampLiteral_Timezone4", sql: `SELECT TIMESTAMP "1999-01-02 12:02:03+01:30"`, expected: [][]interface{}{ []interface{}{"1999-01-02T10:32:03Z"}, }, }, { name: "TimestampLiteral_Timezone_T", sql: `SELECT TIMESTAMP "1999-01-02T12:02:03Z"`, expected: [][]interface{}{ []interface{}{"1999-01-02T12:02:03Z"}, }, }, { name: "TimestampLiteral_WithNano_Timezone", sql: `SELECT TIMESTAMP "1999-01-02 12:02:03.123456789Z"`, expected: [][]interface{}{ []interface{}{"1999-01-02T12:02:03.123456789Z"}, }, }, { name: "TimestampLiteral_WithNano_Timezone2", sql: `SELECT TIMESTAMP "1999-01-02 12:02:03.123456789+03"`, expected: [][]interface{}{ []interface{}{"1999-01-02T09:02:03.123456789Z"}, }, }, { name: "TimestampLiteral_WithNano_Timezone_T", sql: `SELECT TIMESTAMP "1999-01-02T12:02:03.123456789Z"`, expected: [][]interface{}{ []interface{}{"1999-01-02T12:02:03.123456789Z"}, }, }, { name: "DateLiteral", sql: `SELECT DATE "1999-01-02"`, expected: [][]interface{}{ []interface{}{"1999-01-02"}, }, }, { name: "DateLiteral2", sql: `SELECT DATE "1999-1-2"`, expected: [][]interface{}{ []interface{}{"1999-01-02"}, }, }, { name: "BytesLiteral", sql: `SELECT B'xyz'`, expected: [][]interface{}{ []interface{}{[]byte("xyz")}, }, }, }, "InUnnest": { { name: "Bind", sql: `SELECT Id, Value FROM Simple WHERE Id IN UNNEST (@foo)`, params: map[string]Value{ "foo": makeTestValue([]int64{100, 200}), }, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "yyy"}, }, }, { name: "Bind2", sql: `SELECT Id, Value FROM Simple WHERE Id IN UNNEST (@foo)`, params: map[string]Value{ "foo": makeTestValue([]int64{}), }, expected: nil, }, { name: "Bind3", sql: `SELECT Id, Value FROM Simple WHERE Value IN UNNEST (@foo)`, params: map[string]Value{ "foo": makeTestValue([]string{"yyy"}), }, expected: [][]interface{}{ []interface{}{int64(200), "yyy"}, }, }, { name: "ArrayLiteral", sql: `SELECT Id, Value FROM Simple WHERE Id IN UNNEST ([100 ,200, 300])`, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "yyy"}, []interface{}{int64(300), "zzz"}, }, }, { name: "ArrayLiteral2", sql: `SELECT Id, Value FROM Simple WHERE Value IN UNNEST (["xxx"])`, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, { name: "Ident", sql: `SELECT Id FROM ArrayTypes WHERE 1 IN UNNEST (ArrayInt)`, expected: [][]interface{}{ []interface{}{int64(100)}, }, }, { name: "Ident2", sql: `SELECT Id FROM ArrayTypes WHERE 1 NOT IN UNNEST (ArrayInt)`, expected: nil, }, { name: "Path", sql: `SELECT Id FROM ArrayTypes a WHERE 1 IN UNNEST (a.ArrayInt)`, expected: [][]interface{}{ []interface{}{int64(100)}, }, }, { name: "ArrayStructLiteral_Named", sql: `SELECT Id, Value FROM Simple WHERE (Id, Value) IN UNNEST(ARRAY>[(100, "xxx"), (200, "yyy")])`, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "yyy"}, }, }, { name: "ArrayStructLiteral_Unnamed", sql: `SELECT Id, Value FROM Simple WHERE (Id, Value) IN UNNEST(ARRAY>[(100, "xxx"), (200, "yyy")])`, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "yyy"}, }, }, }, "FromUnnest": { { name: "Lietral_String", sql: `SELECT * FROM UNNEST (["xxx", "yyy"])`, expected: [][]interface{}{ []interface{}{"xxx"}, []interface{}{"yyy"}, }, }, { name: "Literal_Int", sql: `SELECT * FROM UNNEST ([1,2,3])`, expected: [][]interface{}{ []interface{}{int64(1)}, []interface{}{int64(2)}, []interface{}{int64(3)}, }, }, { name: "Literal_Params", sql: `SELECT * FROM UNNEST ([ @a, @b, @c])`, params: map[string]Value{ "a": makeTestValue(1), "b": makeTestValue(2), "c": makeTestValue(3), }, expected: [][]interface{}{ []interface{}{int64(1)}, []interface{}{int64(2)}, []interface{}{int64(3)}, }, }, { name: "Literal_Params_Array", sql: `SELECT * FROM UNNEST (@foo)`, params: map[string]Value{ "foo": makeTestValue([]int64{3, 4}), }, expected: [][]interface{}{ []interface{}{int64(3)}, []interface{}{int64(4)}, }, }, { name: "Literal_Params_Array_WithOffset", sql: `SELECT * FROM UNNEST (@foo) WITH OFFSET`, params: map[string]Value{ "foo": makeTestValue([]int64{3, 4}), }, names: []string{"", ""}, expected: [][]interface{}{ []interface{}{int64(3), int64(0)}, []interface{}{int64(4), int64(1)}, }, }, { name: "Literal_Params_Alias", sql: `SELECT x FROM UNNEST (@foo) AS x`, params: map[string]Value{ "foo": makeTestValue([]int64{3, 4}), }, names: []string{"x"}, expected: [][]interface{}{ []interface{}{int64(3)}, []interface{}{int64(4)}, }, }, { name: "Literal_Params_WithOffset_Alias", sql: `SELECT x, y FROM UNNEST (@foo) AS x WITH OFFSET y`, params: map[string]Value{ "foo": makeTestValue([]int64{3, 4}), }, names: []string{"x", "y"}, expected: [][]interface{}{ []interface{}{int64(3), int64(0)}, []interface{}{int64(4), int64(1)}, }, }, { name: "Literal_Alias_Star", sql: `SELECT * FROM UNNEST ([1,2,3]) AS xxx`, names: []string{"xxx"}, expected: [][]interface{}{ []interface{}{int64(1)}, []interface{}{int64(2)}, []interface{}{int64(3)}, }, }, { name: "Literal_Alias_Star_WithOffset", sql: `SELECT * FROM UNNEST ([1,2,3]) AS xxx WITH OFFSET AS yyy`, names: []string{"xxx", "yyy"}, expected: [][]interface{}{ []interface{}{int64(1), int64(0)}, []interface{}{int64(2), int64(1)}, []interface{}{int64(3), int64(2)}, }, }, { name: "Literal_Alias_Ident", sql: `SELECT xxx FROM UNNEST ([1,2,3]) AS xxx`, names: []string{"xxx"}, expected: [][]interface{}{ []interface{}{int64(1)}, []interface{}{int64(2)}, []interface{}{int64(3)}, }, }, { name: "Literal_Alias_WithOffset_Ident", sql: `SELECT xxx, yyy FROM UNNEST ([1,2,3]) AS xxx WITH OFFSET AS yyy`, names: []string{"xxx", "yyy"}, expected: [][]interface{}{ []interface{}{int64(1), int64(0)}, []interface{}{int64(2), int64(1)}, []interface{}{int64(3), int64(2)}, }, }, // TODO: To be able to use Ident and Path in FROM clause // { // name: "Join", // sql: `SELECT Id, flatten FROM ArrayTypes, UNNEST (ArrayTypes.ArrayString) AS flatten`, // expected: [][]interface{}{ // []interface{}{int64(1), int64(0)}, // []interface{}{int64(1), int64(1)}, // }, // }, // { // name: "Join2", // sql: `SELECT Id, flatten FROM ArrayTypes, UNNEST (ArrayString) AS flatten`, // expected: [][]interface{}{ // []interface{}{int64(1), int64(0)}, // []interface{}{int64(1), int64(1)}, // }, // }, // { // name: "Join3", // sql: `SELECT a.Id, flatten FROM ArrayTypes a, UNNEST (a.ArrayString) AS flatten`, // expected: [][]interface{}{ // []interface{}{int64(1), int64(0)}, // []interface{}{int64(1), int64(1)}, // }, // }, { name: "SubQuery_Array", sql: `SELECT * FROM UNNEST(ARRAY(SELECT Id FROM Simple))`, names: []string{""}, expected: [][]interface{}{ []interface{}{int64(100)}, []interface{}{int64(200)}, []interface{}{int64(300)}, }, }, { name: "SubQuery_Array_WithoutColumnAlias", sql: `SELECT * FROM UNNEST(ARRAY(SELECT Id+1 FROM Simple))`, names: []string{""}, expected: [][]interface{}{ []interface{}{int64(101)}, []interface{}{int64(201)}, []interface{}{int64(301)}, }, }, { name: "SubQuery_Array_WithColumnAlias", sql: `SELECT * FROM UNNEST(ARRAY(SELECT Id As x FROM Simple))`, names: []string{""}, expected: [][]interface{}{ []interface{}{int64(100)}, []interface{}{int64(200)}, []interface{}{int64(300)}, }, }, { name: "SubQuery_Array_AsStruct", sql: `SELECT * FROM UNNEST(ARRAY(SELECT AS STRUCT Id, Value FROM Simple))`, names: []string{"Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "yyy"}, []interface{}{int64(300), "zzz"}, }, }, { name: "Literal_Struct_Star", sql: `SELECT * FROM UNNEST(ARRAY>[(1, 'foo'), (3, 'bar')])`, names: []string{"x", "y"}, expected: [][]interface{}{ []interface{}{int64(1), "foo"}, []interface{}{int64(3), "bar"}, }, }, { name: "Literal_Struct_WithOffset_Star", sql: `SELECT * FROM UNNEST(ARRAY>[(1, 'foo'), (3, 'bar')]) WITH OFFSET`, names: []string{"x", "y", ""}, expected: [][]interface{}{ []interface{}{int64(1), "foo", int64(0)}, []interface{}{int64(3), "bar", int64(1)}, }, }, { name: "Literal_Struct_WithOffsetAlias_Star", sql: `SELECT * FROM UNNEST(ARRAY>[(1, 'foo'), (3, 'bar')]) WITH OFFSET AS offset`, names: []string{"x", "y", "offset"}, expected: [][]interface{}{ []interface{}{int64(1), "foo", int64(0)}, []interface{}{int64(3), "bar", int64(1)}, }, }, { name: "Literal_Struct_WithOffsetAlias_Identifer", sql: `SELECT x, y, offset FROM UNNEST(ARRAY>[(1, 'foo'), (3, 'bar')]) WITH OFFSET AS offset`, names: []string{"x", "y", "offset"}, expected: [][]interface{}{ []interface{}{int64(1), "foo", int64(0)}, []interface{}{int64(3), "bar", int64(1)}, }, }, { name: "Literal_Struct_Alias_Star", sql: `SELECT * FROM UNNEST(ARRAY>[(1, 'foo'), (3, 'bar')]) AS foo WITH OFFSET`, names: []string{"x", "y", ""}, expected: [][]interface{}{ []interface{}{int64(1), "foo", int64(0)}, []interface{}{int64(3), "bar", int64(1)}, }, }, // { // name: "Literal_Struct_Alias_DotStar", // sql: `SELECT foo.* FROM UNNEST(ARRAY>[(1, 'foo'), (3, 'bar')]) AS foo WITH OFFSET`, // names: []string{"x", "y"}, // expected: [][]interface{}{ // []interface{}{int64(1), "foo"}, // []interface{}{int64(3), "bar"}, // }, // }, // { // name: "Literal_Struct_Alias_Identifer", // sql: `SELECT foo.x, foo.y, offset FROM UNNEST(ARRAY>[(1, 'foo'), (3, 'bar')]) WITH OFFSET AS offset`, // names: []string{"x", "y", "offset"}, // expected: [][]interface{}{ // []interface{}{int64(1), "foo", int64(0)}, // []interface{}{int64(3), "bar", int64(1)}, // }, // }, }, "Struct": { { name: "StructLiteral", sql: `SELECT ARRAY(SELECT (1,"xx") x)`, expected: [][]interface{}{ []interface{}{ []*StructValue{ { Keys: []string{"", ""}, Values: []interface{}{int64(1), string("xx")}, }, }, }, }, }, { name: "StructLiteral_WithField", sql: `SELECT ARRAY(SELECT STRUCT(1,"xx") x)`, expected: [][]interface{}{ []interface{}{ []*StructValue{ { Keys: []string{"Id", "Value"}, Values: []interface{}{int64(1), string("xx")}, }, }, }, }, }, { name: "StructLiteral_WithField_WithoutName", sql: `SELECT ARRAY(SELECT STRUCT(1,"xx") x)`, expected: [][]interface{}{ []interface{}{ []*StructValue{ { Keys: []string{"", "Value"}, Values: []interface{}{int64(1), string("xx")}, }, }, }, }, }, { name: "StructLiteral_WithArrayLiteral_OK", sql: `SELECT [(SELECT STRUCT(1,"xx"))]`, expected: [][]interface{}{ []interface{}{ []*StructValue{ { Keys: []string{"Id", "Value"}, Values: []interface{}{int64(1), string("xx")}, }, }, }, }, }, // TODO: this should be error in spanner { name: "StructLiteral_WithArrayLiteral_NG", sql: `SELECT [STRUCT(1,"xx")]`, expected: [][]interface{}{ []interface{}{ []*StructValue{ { Keys: []string{"Id", "Value"}, Values: []interface{}{int64(1), string("xx")}, }, }, }, }, }, // Parse error // { // name: "StructLiteral_WithField_WithoutType", // sql: `SELECT ARRAY(SELECT STRUCT(1,"xx") x)`, // }, { name: "StructLiteral_InvalidFieldSize", sql: `SELECT ARRAY(SELECT STRUCT(1,"xx") x)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^STRUCT type has 1 fields but constructor call has 2 fields`), }, { name: "StructLiteral_InvalidFieldSize", sql: `SELECT ARRAY(SELECT STRUCT<>(1,"xx") x)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^STRUCT type has 0 fields but constructor call has 2 fields`), }, { name: "StructLiteral_IncompatibleType", sql: `SELECT ARRAY(SELECT STRUCT(1,"xx") x)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Struct field 2 has type literal STRING which does not coerce to INT64`), }, { name: "PathIdentifier_StructField_BeginFromTableAlias", sql: `SELECT y.x.Id, y.x.Value FROM (SELECT STRUCT(1, "xx") x) y`, expected: [][]interface{}{ []interface{}{int64(1), string("xx")}, }, }, { name: "PathIdentifier_StructField_BeginFromStructColumnAlias", sql: `SELECT x.Id, x.Value FROM (SELECT STRUCT(1, "xx") x)`, expected: [][]interface{}{ []interface{}{int64(1), string("xx")}, }, }, { name: "PathIdentifier_StructField_Subquery_NotFound", sql: `SELECT y.x.zzz FROM (SELECT STRUCT(1, "xx") x) y`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Field name zzz does not exist in STRUCT`), }, { name: "Selector_ArrayOfStruct", sql: `SELECT z.y[OFFSET(0)].Id FROM (SELECT ARRAY(SELECT STRUCT(1,"xx") x) y) z`, expected: [][]interface{}{ []interface{}{int64(1)}, }, }, { name: "DotStar_Ident_StructField", sql: `SELECT x.* FROM (SELECT STRUCT(1, "xx") x)`, names: []string{"Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(1), string("xx")}, }, }, { name: "DotStar_Path_StructField", sql: `SELECT y.x.* FROM (SELECT STRUCT(1, "xx") x) y`, names: []string{"Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(1), string("xx")}, }, }, { name: "SelectAsStruct", sql: `SELECT ARRAY(SELECT AS STRUCT Id, Value FROM Simple)`, names: []string{""}, expected: [][]interface{}{ []interface{}{ []*StructValue{ { Keys: []string{"Id", "Value"}, Values: []interface{}{int64(100), string("xxx")}, }, { Keys: []string{"Id", "Value"}, Values: []interface{}{int64(200), string("yyy")}, }, { Keys: []string{"Id", "Value"}, Values: []interface{}{int64(300), string("zzz")}, }, }, }, }, }, { name: "SelectAsStruct_Star", sql: `SELECT ARRAY(SELECT AS STRUCT * FROM Simple)`, names: []string{""}, expected: [][]interface{}{ []interface{}{ []*StructValue{ { Keys: []string{"Id", "Value"}, Values: []interface{}{int64(100), string("xxx")}, }, { Keys: []string{"Id", "Value"}, Values: []interface{}{int64(200), string("yyy")}, }, { Keys: []string{"Id", "Value"}, Values: []interface{}{int64(300), string("zzz")}, }, }, }, }, }, { name: "SelectAsStruct_Star_SubQuery", sql: `SELECT ARRAY(SELECT AS STRUCT * FROM (SELECT Id+1, CONCAT(Value, "a") FROM Simple))`, names: []string{""}, expected: [][]interface{}{ []interface{}{ []*StructValue{ { Keys: []string{"", ""}, Values: []interface{}{int64(101), string("xxxa")}, }, { Keys: []string{"", ""}, Values: []interface{}{int64(201), string("yyya")}, }, { Keys: []string{"", ""}, Values: []interface{}{int64(301), string("zzza")}, }, }, }, }, }, { name: "SelectAsStruct_DotStar", sql: `SELECT ARRAY(SELECT AS STRUCT a.* FROM Simple a)`, expected: [][]interface{}{ []interface{}{ []*StructValue{ { Keys: []string{"Id", "Value"}, Values: []interface{}{int64(100), string("xxx")}, }, { Keys: []string{"Id", "Value"}, Values: []interface{}{int64(200), string("yyy")}, }, { Keys: []string{"Id", "Value"}, Values: []interface{}{int64(300), string("zzz")}, }, }, }, }, }, { name: "SelectAsStruct_NoColumnName", sql: `SELECT ARRAY(SELECT AS STRUCT Id+1, Value FROM Simple)`, expected: [][]interface{}{ []interface{}{ []*StructValue{ { Keys: []string{"", "Value"}, Values: []interface{}{int64(101), string("xxx")}, }, { Keys: []string{"", "Value"}, Values: []interface{}{int64(201), string("yyy")}, }, { Keys: []string{"", "Value"}, Values: []interface{}{int64(301), string("zzz")}, }, }, }, }, }, { name: "SelectAsStruct_WithColumnAlias", sql: `SELECT ARRAY(SELECT AS STRUCT Id+1 AS XX, Value FROM Simple)`, expected: [][]interface{}{ []interface{}{ []*StructValue{ { Keys: []string{"XX", "Value"}, Values: []interface{}{int64(101), string("xxx")}, }, { Keys: []string{"XX", "Value"}, Values: []interface{}{int64(201), string("yyy")}, }, { Keys: []string{"XX", "Value"}, Values: []interface{}{int64(301), string("zzz")}, }, }, }, }, }, { name: "ValueTypes_FullTypes", sql: `SELECT ARRAY(SELECT AS STRUCT * FROM FullTypes)`, expected: [][]interface{}{ []interface{}{ []*StructValue{ { Keys: fullTypesKeys, Values: []interface{}{ "xxx", "xxx", "xxx", true, true, []byte("xyz"), []byte("xyz"), "2012-03-04T12:34:56.123456789Z", "2012-03-04T12:34:56.123456789Z", int64(100), int64(100), float64(0.5), float64(0.5), "2012-03-04", "2012-03-04", }, }, { Keys: fullTypesKeys, Values: []interface{}{ "yyy", "yyy", "yyy", false, false, []byte("xyz"), []byte("xyz"), "2012-03-04T12:34:56.123456789Z", "2012-03-04T12:34:56.123456789Z", int64(200), int64(200), float64(0.5), float64(0.5), "2012-03-05", "2012-03-05", }, }, }, }, }, }, { name: "ValueTypes_ArrayTypes", sql: `SELECT ARRAY(SELECT AS STRUCT * FROM ArrayTypes)`, expected: [][]interface{}{ []interface{}{ []*StructValue{ { Keys: arrayTypesKeys, Values: []interface{}{ int64(100), makeTestArray(TCString, "xxx1", "xxx2"), makeTestArray(TCBool, true, false), [][]uint8{{0x78, 0x79, 0x7a}, {0x78, 0x79, 0x7a}}, makeTestArray(TCString, "2012-03-04T12:34:56.123456789Z", "2012-03-04T12:34:56.999999999Z"), makeTestArray(TCInt64, int64(1), int64(2)), makeTestArray(TCFloat64, float64(0.1), float64(0.2)), makeTestArray(TCString, "2012-03-04", "2012-03-05"), }, }, }, }, }, }, { name: "ValueTypes_ArrayTypes", sql: `SELECT ARRAY(SELECT AS STRUCT * FROM ArrayTypes)`, expected: [][]interface{}{ []interface{}{ []*StructValue{ { Keys: arrayTypesKeys, Values: []interface{}{ int64(100), makeTestArray(TCString, "xxx1", "xxx2"), makeTestArray(TCBool, true, false), [][]uint8{{0x78, 0x79, 0x7a}, {0x78, 0x79, 0x7a}}, makeTestArray(TCString, "2012-03-04T12:34:56.123456789Z", "2012-03-04T12:34:56.999999999Z"), makeTestArray(TCInt64, int64(1), int64(2)), makeTestArray(TCFloat64, float64(0.1), float64(0.2)), makeTestArray(TCString, "2012-03-04", "2012-03-05"), }, }, }, }, }, }, { name: "ValueTypes_NestedArrayOfStruct", sql: `SELECT ARRAY(SELECT AS STRUCT ARRAY(SELECT AS STRUCT * FROM ArrayTypes) xx) yy`, expected: [][]interface{}{ []interface{}{ []*StructValue{ { Keys: []string{"xx"}, Values: []interface{}{ []*StructValue{ { Keys: arrayTypesKeys, Values: []interface{}{ int64(100), makeTestArray(TCString, "xxx1", "xxx2"), makeTestArray(TCBool, true, false), [][]uint8{{0x78, 0x79, 0x7a}, {0x78, 0x79, 0x7a}}, makeTestArray(TCString, "2012-03-04T12:34:56.123456789Z", "2012-03-04T12:34:56.999999999Z"), makeTestArray(TCInt64, int64(1), int64(2)), makeTestArray(TCFloat64, float64(0.1), float64(0.2)), makeTestArray(TCString, "2012-03-04", "2012-03-05"), }, }, }, }, }, }, }, }, }, { name: "StructOfStruct", sql: `SELECT [(SELECT STRUCT>(1, (SELECT AS STRUCT Id FROM Simple)))]`, code: codes.Unimplemented, msg: regexp.MustCompile(`^Unsupported query shape: A struct value cannot be returned as a column value. Rewrite the query to flatten the struct fields in the result.`), }, { name: "Where_Compare_Untyped", sql: `SELECT Id, Value FROM Simple WHERE (Id, Value) = (100, "xxx")`, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, { name: "Where_Compare_TypedWithName", sql: `SELECT Id, Value FROM Simple WHERE (Id, Value) = STRUCT(100, "xxx")`, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, { name: "Where_Compare_TypedWithName2", sql: `SELECT Id, Value FROM Simple WHERE STRUCT(Id, Value) = STRUCT(100, "xxx")`, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, { name: "Where_Compare_TypedWithoutName", sql: `SELECT Id, Value FROM Simple WHERE (Id, Value) = STRUCT(100, "xxx")`, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, { name: "Where_In_Untyped", sql: `SELECT Id, Value FROM Simple WHERE (Id, Value) IN ((100, "xxx"), (200, "yyy"))`, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "yyy"}, }, }, { name: "Where_In_TypedWithName", sql: `SELECT Id, Value FROM Simple WHERE STRUCT(Id, Value) IN ((100, "xxx"), (200, "yyy"))`, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "yyy"}, }, }, { name: "Where_In_TypedWithName2", sql: `SELECT Id, Value FROM Simple WHERE (Id, Value) IN (STRUCT(100, "xxx"), STRUCT(200, "yyy"))`, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "yyy"}, }, }, { name: "Where_NotIn", sql: `SELECT Id, Value FROM Simple WHERE (Id, Value) NOT IN ((100, "xxx"), (200, "yyy"))`, expected: [][]interface{}{ []interface{}{int64(300), "zzz"}, }, }, }, "FromJoin": { { name: "InnerJoin", sql: `SELECT a.*, b.* FROM JoinA a INNER JOIN JoinB b USING (Id)`, names: []string{"Id", "Value", "Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "xxx", int64(100), "aaa"}, []interface{}{int64(200), "yyy", int64(200), "bbb"}, }, }, { name: "CrossJoin", sql: `SELECT a.*, b.* FROM JoinA a CROSS JOIN JoinB b`, names: []string{"Id", "Value", "Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "xxx", int64(100), "aaa"}, []interface{}{int64(100), "xxx", int64(200), "bbb"}, []interface{}{int64(100), "xxx", int64(400), "ddd"}, []interface{}{int64(200), "yyy", int64(100), "aaa"}, []interface{}{int64(200), "yyy", int64(200), "bbb"}, []interface{}{int64(200), "yyy", int64(400), "ddd"}, []interface{}{int64(300), "zzz", int64(100), "aaa"}, []interface{}{int64(300), "zzz", int64(200), "bbb"}, []interface{}{int64(300), "zzz", int64(400), "ddd"}, }, }, { name: "CommaJoin", sql: `SELECT a.*, b.* FROM JoinA a, JoinB b`, names: []string{"Id", "Value", "Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "xxx", int64(100), "aaa"}, []interface{}{int64(100), "xxx", int64(200), "bbb"}, []interface{}{int64(100), "xxx", int64(400), "ddd"}, []interface{}{int64(200), "yyy", int64(100), "aaa"}, []interface{}{int64(200), "yyy", int64(200), "bbb"}, []interface{}{int64(200), "yyy", int64(400), "ddd"}, []interface{}{int64(300), "zzz", int64(100), "aaa"}, []interface{}{int64(300), "zzz", int64(200), "bbb"}, []interface{}{int64(300), "zzz", int64(400), "ddd"}, }, }, { name: "LeftOuterJoin", sql: `SELECT a.*, b.* FROM JoinA a LEFT OUTER JOIN JoinB b USING (Id)`, names: []string{"Id", "Value", "Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "xxx", int64(100), "aaa"}, []interface{}{int64(200), "yyy", int64(200), "bbb"}, []interface{}{int64(300), "zzz", nil, nil}, }, }, { name: "RightOuterJoin", sql: `SELECT a.*, b.* FROM JoinA a Right OUTER JOIN JoinB b USING (Id)`, names: []string{"Id", "Value", "Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "xxx", int64(100), "aaa"}, []interface{}{int64(200), "yyy", int64(200), "bbb"}, []interface{}{nil, nil, int64(400), "ddd"}, }, }, { name: "FullOuterJoin", sql: `SELECT * FROM JoinA a FULL OUTER JOIN JoinB b USING (Id)`, names: []string{"Id", "Value", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "xxx", "aaa"}, []interface{}{int64(200), "yyy", "bbb"}, []interface{}{int64(300), "zzz", nil}, []interface{}{int64(400), nil, "ddd"}, }, }, { name: "FullOuterJoin_UsingMultiIdentifer", sql: `SELECT * FROM JoinA a FULL OUTER JOIN JoinB b USING (Id, Value)`, names: []string{"Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "yyy"}, []interface{}{int64(300), "zzz"}, []interface{}{int64(100), "aaa"}, []interface{}{int64(200), "bbb"}, []interface{}{int64(400), "ddd"}, }, }, // TODO: FullOuterJoin is simulated only for USING // { // name: "FullOuterJoin_On", // sql: `SELECT * FROM JoinA a FULL OUTER JOIN JoinB b ON a.Id = b.Id`, // names: []string{"Id", "Value", "Id", "Value"}, // expected: [][]interface{}{ // []interface{}{int64(100), "xxx", int64(100), "aaa"}, // []interface{}{int64(200), "yyy", int64(200), "bbb"}, // []interface{}{int64(300), "zzz", nil, nil}, // []interface{}{nil, nil, int64(400), "ddd"}, // }, // }, // TODO: FullOuterJoin is simulated by using subquery with UNION, so table alias cannot be used // { // name: "FullOuterJoin_WithColumnNames", // sql: `SELECT a.Id, a.Value, b.Value FROM JoinA a FULL OUTER JOIN JoinB b USING (Id)`, // names: []string{"Id", "Value", "Value"}, // expected: [][]interface{}{ // []interface{}{int64(100), "xxx", "aaa"}, // []interface{}{int64(200), "yyy", "bbb"}, // []interface{}{int64(300), "zzz", nil}, // []interface{}{int64(400), nil, "ddd"}, // }, // }, // { // name: "FullOuterJoin_WithTableAlias", // sql: `SELECT a.*, b.* FROM JoinA a FULL OUTER JOIN JoinB b USING (Id)`, // names: []string{"Id", "Value"}, // expected: [][]interface{}{ // []interface{}{int64(100), "xxx", int64(100), "aaa"}, // []interface{}{int64(200), "yyy", int64(200), "bbb"}, // []interface{}{int64(300), "zzz", nil, nil}, // []interface{}{nil, nil, int64(400), "ddd"}, // }, // }, { name: "Cond_ON", sql: `SELECT a.* FROM JoinA AS a JOIN JoinB AS b ON a.Id = b.Id WHERE b.Value = "aaa"`, names: []string{"Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, { name: "Cond_ON1", sql: `SELECT a.Id, a.Value FROM JoinA AS a JOIN JoinB AS b ON a.Id = b.Id WHERE b.Value = "aaa"`, names: []string{"Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, { name: "Cond_ON2", sql: `SELECT * FROM JoinA AS a JOIN JoinB AS b ON a.Id = b.Id WHERE a.Id = @id`, params: map[string]Value{ "id": makeTestValue(200), }, names: []string{"Id", "Value", "Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(200), "yyy", int64(200), "bbb"}, }, }, { name: "Cond_ON3", sql: `SELECT * FROM JoinA AS a JOIN JoinB AS b ON a.Id = b.Id WHERE a.Id = @id`, params: map[string]Value{ "id": makeTestValue(200), }, names: []string{"Id", "Value", "Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(200), "yyy", int64(200), "bbb"}, }, }, { name: "Cond_Using_SingleIdentifier", sql: `SELECT * FROM JoinA AS a JOIN JoinB AS b USING (Id) WHERE b.Value = "aaa"`, names: []string{"Id", "Value", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "xxx", "aaa"}, }, }, { name: "Cond_Using_MultiIdentifer", sql: `SELECT * FROM JoinA AS a JOIN Simple AS b USING (Id, Value)`, names: []string{"Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "yyy"}, []interface{}{int64(300), "zzz"}, }, }, { name: "Cond_Using_ColumnNotExist", sql: `SELECT 1 FROM JoinA a JOIN JoinB b USING (foo)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Column foo in USING clause not found on left side of join`), }, { name: "Cond_Using_ColumnNotExist_RightSide", sql: `SELECT 1 FROM JoinA a JOIN CompositePrimaryKeys b USING (Value)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Column Value in USING clause not found on right side of join`), }, { name: "Cond_Using_ColumnNotExist_Subquery", sql: `SELECT 1 FROM JoinA a JOIN (SELECT Value FROM JoinB) b USING (Id)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Column Id in USING clause not found on right side of join`), }, { name: "Cond_USING_EscapeKeyword", sql: "SELECT `AND`.*, `OR`.* FROM `From` `AND` JOIN `From` `OR` USING (`ALL`)", names: []string{"ALL", "CAST", "JOIN", "ALL", "CAST", "JOIN"}, expected: [][]interface{}{{int64(1), int64(1), int64(1), int64(1), int64(1), int64(1)}}, }, { name: "Subquery_USING", sql: `SELECT a.* FROM JoinA AS a JOIN (SELECT Id FROM JoinB) AS b USING (Id) WHERE a.Value = "xxx"`, names: []string{"Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, { name: "From_Join_Paren", sql: `SELECT a.Id FROM (JoinA AS a JOIN JoinB AS b USING (Id))`, names: []string{"Id"}, expected: [][]interface{}{ []interface{}{int64(100)}, []interface{}{int64(200)}, }, }, { name: "From_Subquery_Simple", sql: `SELECT s.* FROM (SELECT 1) s`, names: []string{""}, expected: [][]interface{}{ []interface{}{int64(1)}, }, }, { name: "From_Subquery_Table", sql: `SELECT s.* FROM (SELECT Id FROM Simple WHERE Value = "xxx") s`, names: []string{"Id"}, expected: [][]interface{}{ []interface{}{int64(100)}, }, }, }, "Subquery": { { name: "SubQuery_Scalar_Simple", sql: `SELECT Id FROM Simple WHERE 1 = (SELECT 1)`, expected: [][]interface{}{ []interface{}{int64(100)}, []interface{}{int64(200)}, []interface{}{int64(300)}, }, }, { name: "Scalar_WithMultiColumns", sql: `SELECT 1 WHERE 1 = (SELECT 1, "abc")`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Scalar subquery cannot have more than one column unless using SELECT AS STRUCT to build STRUCT values`), }, // TODO: sqlite implicitly extract 1 row if subquery returns multi rows // { // name: "Scalar_ReturnedMultiRows", // sql: "SELECT 1 FROM Simple WHERE 1 = (SELECT 1 FROM Simple)", // code: codes.InvalidArgument, // msg: regexp.MustCompile(`A scalar subquery returned more than one row.`), // }, { name: "SubQuery_Scalar_Table", sql: `SELECT Id FROM Simple WHERE Id = (SELECT Id From Simple WHERE Id = 100)`, expected: [][]interface{}{ []interface{}{int64(100)}, }, }, { name: "SubQuery_In", sql: `SELECT Id FROM Simple WHERE Id IN (SELECT Id From Simple WHERE Id > 100)`, expected: [][]interface{}{ []interface{}{int64(200)}, []interface{}{int64(300)}, }, }, { name: "SubQuery_NotIn", sql: `SELECT Id FROM Simple WHERE Id NOT IN (SELECT Id From Simple WHERE Id > 100)`, expected: [][]interface{}{ []interface{}{int64(100)}, }, }, { name: "SubQuery_In_WithMultiColumns", sql: `SELECT 1 WHERE 1 IN (SELECT 1, "abc")`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Subquery of type IN must have only one output column`), }, // TODO: need to expand struct like buildUnnestView // { // name: "SubQuery_In_AsStruct", // sql: `SELECT Id FROM Simple WHERE (Id) IN (SELECT AS STRUCT Id From Simple WHERE Id > 100)`, // expected: [][]interface{}{ // []interface{}{int64(200)}, // []interface{}{int64(300)}, // }, // }, { name: "SubQuery_EXISTS", sql: `SELECT Id FROM Simple WHERE EXISTS(SELECT 1, "xx")`, expected: [][]interface{}{ []interface{}{int64(100)}, []interface{}{int64(200)}, []interface{}{int64(300)}, }, }, { name: "SubQuery_EXISTS_NoMatch", sql: `SELECT Id FROM Simple WHERE EXISTS(SELECT * FROM Simple WHERE Id = 1000)`, expected: nil, }, { name: "SubQuery_ColumnAlias", sql: `SELECT Id, foo, bar FROM (SELECT Id, Id AS foo, Id bar FROM Simple)`, names: []string{"Id", "foo", "bar"}, expected: [][]interface{}{ []interface{}{int64(100), int64(100), int64(100)}, []interface{}{int64(200), int64(200), int64(200)}, []interface{}{int64(300), int64(300), int64(300)}, }, }, { name: "SubQuery_ColumnAlias2", sql: `SELECT Id +1, foo +1, bar +1 FROM (SELECT Id, Id AS foo, Id bar FROM Simple)`, names: []string{"", "", ""}, expected: [][]interface{}{ []interface{}{int64(101), int64(101), int64(101)}, []interface{}{int64(201), int64(201), int64(201)}, []interface{}{int64(301), int64(301), int64(301)}, }, }, { name: "SubQuery_ColumnAlias_AsAlias", sql: `SELECT x AS y FROM (SELECT Id x FROM Simple)`, names: []string{"y"}, expected: [][]interface{}{ []interface{}{int64(100)}, []interface{}{int64(200)}, []interface{}{int64(300)}, }, }, { name: "SubQuery_TableAlias", sql: `SELECT xx.Id FROM (SELECT Id FROM Simple) AS xx`, expected: [][]interface{}{ []interface{}{int64(100)}, []interface{}{int64(200)}, []interface{}{int64(300)}, }, }, { name: "Compound_Union_Distinct", sql: `SELECT Id FROM Simple UNION DISTINCT SELECT Id FROM Simple`, expected: [][]interface{}{ []interface{}{int64(100)}, []interface{}{int64(200)}, []interface{}{int64(300)}, }, }, { name: "Compound_Union_All", sql: `SELECT Id FROM Simple UNION ALL SELECT Id FROM Simple`, expected: [][]interface{}{ []interface{}{int64(100)}, []interface{}{int64(200)}, []interface{}{int64(300)}, []interface{}{int64(100)}, []interface{}{int64(200)}, []interface{}{int64(300)}, }, }, { name: "Compound_Union_All_SelectLimit", sql: `SELECT Id FROM Simple UNION ALL (SELECT Id FROM Simple LIMIT 1)`, expected: [][]interface{}{ []interface{}{int64(100)}, []interface{}{int64(200)}, []interface{}{int64(300)}, []interface{}{int64(100)}, }, }, { name: "Compound_Union_All_UnionLimit", sql: `SELECT Id FROM Simple UNION ALL (SELECT Id FROM Simple) LIMIT 5`, expected: [][]interface{}{ []interface{}{int64(100)}, []interface{}{int64(200)}, []interface{}{int64(300)}, []interface{}{int64(100)}, []interface{}{int64(200)}, }, }, { name: "Compound_Union_All_OrderBy", sql: `SELECT Id FROM Simple UNION ALL (SELECT Id FROM Simple) ORDER BY Id`, expected: [][]interface{}{ []interface{}{int64(100)}, []interface{}{int64(100)}, []interface{}{int64(200)}, []interface{}{int64(200)}, []interface{}{int64(300)}, []interface{}{int64(300)}, }, }, { name: "Compound_Union_All_OrderBy_Limit", sql: `SELECT Id FROM Simple UNION ALL (SELECT Id FROM Simple) ORDER BY Id LIMIT 3`, expected: [][]interface{}{ []interface{}{int64(100)}, []interface{}{int64(100)}, []interface{}{int64(200)}, }, }, { name: "Compound_Intersect_Distinct", sql: `SELECT Id FROM Simple INTERSECT DISTINCT SELECT Id FROM Simple`, expected: [][]interface{}{ []interface{}{int64(100)}, []interface{}{int64(200)}, []interface{}{int64(300)}, }, }, { name: "Compound_Intersect_Distinct2", sql: `SELECT Id FROM Simple INTERSECT DISTINCT (SELECT Id FROM Simple WHERE Id IN (100, 300))`, expected: [][]interface{}{ []interface{}{int64(100)}, []interface{}{int64(300)}, }, }, { name: "Compound_Intersect_Distinct_Limit", sql: `SELECT Id FROM Simple INTERSECT DISTINCT (SELECT Id FROM Simple) LIMIT 2`, expected: [][]interface{}{ []interface{}{int64(100)}, []interface{}{int64(200)}, }, }, { name: "Compound_Intersect_Distinct_OrderBy", sql: `SELECT Id FROM Simple INTERSECT DISTINCT (SELECT Id FROM Simple) ORDER BY Id DESC`, expected: [][]interface{}{ []interface{}{int64(300)}, []interface{}{int64(200)}, []interface{}{int64(100)}, }, }, { name: "Compound_Except_Distinct", sql: `SELECT Id FROM Simple EXCEPT DISTINCT SELECT Id FROM Simple`, expected: nil, }, { name: "Compound_Except_Distinct2", sql: `SELECT Id FROM Simple EXCEPT DISTINCT (SELECT Id FROM Simple WHERE Id IN (100, 300))`, expected: [][]interface{}{ []interface{}{int64(200)}, }, }, { name: "Compound_Except_Distinct_Limit", sql: `SELECT Id FROM Simple EXCEPT DISTINCT (SELECT Id FROM Simple WHERE Id = 200) LIMIT 1`, expected: [][]interface{}{ []interface{}{int64(100)}, }, }, { name: "Compound_Except_Distinct_OrderBy", sql: `SELECT Id FROM Simple EXCEPT DISTINCT (SELECT Id FROM Simple WHERE Id = 200) ORDER BY Id DESC`, expected: [][]interface{}{ []interface{}{int64(300)}, []interface{}{int64(100)}, }, }, { name: "Compound_Complex1", sql: `SELECT Id FROM Simple UNION ALL SELECT Id+2 FROM Simple UNION ALL SELECT Id+1 FROM Simple`, expected: [][]interface{}{ []interface{}{int64(100)}, []interface{}{int64(200)}, []interface{}{int64(300)}, []interface{}{int64(102)}, []interface{}{int64(202)}, []interface{}{int64(302)}, []interface{}{int64(101)}, []interface{}{int64(201)}, []interface{}{int64(301)}, }, }, { name: "Compound_Complex2", sql: `SELECT Id FROM Simple UNION ALL SELECT Id+1 FROM Simple UNION ALL SELECT Id+2 FROM Simple ORDER BY Id DESC`, expected: [][]interface{}{ []interface{}{int64(302)}, []interface{}{int64(301)}, []interface{}{int64(300)}, []interface{}{int64(202)}, []interface{}{int64(201)}, []interface{}{int64(200)}, []interface{}{int64(102)}, []interface{}{int64(101)}, []interface{}{int64(100)}, }, }, { name: "Compound_Complex3", sql: `(SELECT Id FROM Simple UNION ALL SELECT Id+1 FROM Simple) INTERSECT DISTINCT SELECT Id FROM Simple`, expected: [][]interface{}{ []interface{}{int64(100)}, []interface{}{int64(200)}, []interface{}{int64(300)}, }, }, // TODO: INT64 and FLOAT64 are compatible value type // { // name: "Compound_MergeIntFloat", // sql: `SELECT 1 UNION ALL SELECT 0.1`, // expected: [][]interface{}{ // []interface{}{float64(1)}, // []interface{}{float64(0.1)}, // }, // }, // { // name: "Compound_MergeIntFloat2", // sql: `SELECT 1 UNION DISTINCT SELECT 1.0`, // expected: [][]interface{}{ // []interface{}{float64(1.0)}, // }, // }, { name: "Compound_ColumnsNum", sql: `SELECT 1 UNION ALL SELECT 1, 2`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Queries in UNION ALL have mismatched column count; query 1 has 1 column, query 2 has 2 columns`), }, { name: "Compound_ColumnsNum2", sql: `SELECT 1, 2 INTERSECT DISTINCT SELECT 1`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Queries in INTERSECT DISTINCT have mismatched column count; query 1 has 2 column, query 2 has 1 columns`), }, { name: "Compound_ColumnsType", sql: `SELECT 1 EXCEPT DISTINCT SELECT "xx"`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Column 1 in EXCEPT DISTINCT has incompatible types: INT64, STRING`), }, { name: "Compound_ColumnsType2", sql: `SELECT 1, 0.1 EXCEPT DISTINCT SELECT 1, true`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Column 2 in EXCEPT DISTINCT has incompatible types: FLOAT64, BOOL`), }, }, "BinaryOp": { { name: "Equal_IntInt", sql: `SELECT 1 = 1`, expected: [][]interface{}{ []interface{}{true}, }, }, { name: "Equal_IntFloat1", sql: `SELECT 1 = 1.0`, expected: [][]interface{}{ []interface{}{true}, }, }, { name: "Equal_IntFloat2", sql: `SELECT 1 = 1.000000000000000000000000000000000000001`, expected: [][]interface{}{ []interface{}{true}, }, }, { name: "Equal_IntFloat3", sql: `SELECT 1 = 1.1`, expected: [][]interface{}{ []interface{}{false}, }, }, { name: "Equal_IntString", sql: `SELECT 1 = "1"`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^No matching signature for operator = for argument types: INT64, STRING.`), }, { name: "Equal_IntBool", sql: `SELECT 1 = TRUE`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^No matching signature for operator = for argument types: INT64, BOOL.`), }, { name: "Equal_FloatString", sql: `SELECT 1.0 = "1.0"`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^No matching signature for operator = for argument types: FLOAT64, STRING.`), }, { name: "Equal_StringFloat", sql: `SELECT "1.0" = 1.0`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^No matching signature for operator = for argument types: STRING, FLOAT64.`), }, { name: "Equal_StringFloat", sql: `SELECT "1.0" = 1.0`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^No matching signature for operator = for argument types: STRING, FLOAT64.`), }, { name: "Equal_StringBool", sql: `SELECT "TRUE" = TRUE`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^No matching signature for operator = for argument types: STRING, BOOL.`), }, { name: "Equal_StringBytes", sql: `SELECT "xxx" = B'xxx'`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^No matching signature for operator = for argument types: STRING, BYTES.`), }, // { // name: "Less_StructStruct_Coerce", // sql: `SELECT (100) < (101)`, // expected: [][]interface{}{ // []interface{}{false}, // }, // }, { name: "Less_StructStruct", sql: `SELECT (100, 100) < (100, 100)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Less than is not defined for arguments of type STRUCT`), }, { name: "LessEqual_StructStruct", sql: `SELECT (100, 100) <= (100, 100)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Less than is not defined for arguments of type STRUCT`), }, { name: "Greater_StructStruct", sql: `SELECT (100, 100) > (100, 100)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Greater than is not defined for arguments of type STRUCT`), }, { name: "GreaterEqual_StructStruct", sql: `SELECT (100, 100) >= (100, 100)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Greater than is not defined for arguments of type STRUCT`), }, }, "Arithmetic": { { name: "Arithmetic_Add", sql: `SELECT 1 + 2`, expected: [][]interface{}{ []interface{}{int64(3)}, }, }, { name: "Arithmetic_Add_Float", sql: `SELECT 1.5 + 2.5`, expected: [][]interface{}{ []interface{}{float64(4)}, }, }, { name: "Arithmetic_Sub", sql: `SELECT 1 - 2`, expected: [][]interface{}{ []interface{}{int64(-1)}, }, }, { name: "Arithmetic_Sub_Float", sql: `SELECT 1.5 - 2`, expected: [][]interface{}{ []interface{}{float64(-0.5)}, }, }, { name: "Arithmetic_Mult", sql: `SELECT 2 * 2`, expected: [][]interface{}{ []interface{}{int64(4)}, }, }, { name: "Arithmetic_Mult_Float", sql: `SELECT 2.5 * 2`, expected: [][]interface{}{ []interface{}{float64(5)}, }, }, { name: "Arithmetic_Div", sql: `SELECT 3 / 2`, expected: [][]interface{}{ []interface{}{float64(1.5)}, }, }, { name: "Arithmetic_BitOr", sql: `SELECT 0x11 | 0x04`, expected: [][]interface{}{ []interface{}{int64(0x15)}, }, }, { name: "Arithmetic_BitXor", sql: `SELECT 0x11 ^ 0x01`, expected: [][]interface{}{ []interface{}{int64(0x10)}, }, }, { name: "Arithmetic_BitAnd", sql: `SELECT 0x13 & 0x01`, expected: [][]interface{}{ []interface{}{int64(0x01)}, }, }, { name: "Arithmetic_BitLeftShift", sql: `SELECT 0x3 << 3`, expected: [][]interface{}{ []interface{}{int64(24)}, }, }, { name: "Arithmetic_BitRightShift", sql: `SELECT 0xf0 >> 2`, expected: [][]interface{}{ []interface{}{int64(60)}, }, }, { name: "Unary_Int", sql: `SELECT - -1`, expected: [][]interface{}{ []interface{}{int64(1)}, }, }, { name: "Unary_Float", sql: `SELECT - -0.1`, expected: [][]interface{}{ []interface{}{float64(0.1)}, }, }, { name: "Unary_BitNot", sql: `SELECT ~ -3`, expected: [][]interface{}{ []interface{}{int64(2)}, }, }, { name: "Unary_Not", sql: `SELECT NOT true`, expected: [][]interface{}{ []interface{}{false}, }, }, }, "Cast": { { name: "Cast_Int64_String", sql: `SELECT CAST(100 AS STRING)`, expected: [][]interface{}{ []interface{}{"100"}, }, }, { name: "Cast_Int64_String_Neg", sql: `SELECT CAST(-100 AS STRING)`, expected: [][]interface{}{ []interface{}{"-100"}, }, }, { name: "Cast_Int64_Bool_True", sql: `SELECT CAST(100 AS BOOL)`, expected: [][]interface{}{ []interface{}{true}, }, }, { name: "Cast_Int64_Bool_True2", sql: `SELECT CAST(-100 AS BOOL)`, expected: [][]interface{}{ []interface{}{true}, }, }, { name: "Cast_Int64_Bool_False", sql: `SELECT CAST(0 AS BOOL)`, expected: [][]interface{}{ []interface{}{false}, }, }, { name: "Cast_Int64_Float64_1", sql: `SELECT CAST(0 AS FLOAT64)`, expected: [][]interface{}{ []interface{}{float64(0)}, }, }, { name: "Cast_Int64_Float64_2", sql: `SELECT CAST(-1 AS FLOAT64)`, expected: [][]interface{}{ []interface{}{float64(-1.0)}, }, }, { name: "Cast_Float64_String_Big_Small", sql: `SELECT CAST(@foo AS STRING)`, params: map[string]Value{ "foo": makeTestValue(2.3), }, expected: [][]interface{}{ []interface{}{"2.3"}, }, }, { name: "Cast_Float64_String_Big_Small_neg", sql: `SELECT CAST(@foo AS STRING)`, params: map[string]Value{ "foo": makeTestValue(-2.3), }, expected: [][]interface{}{ []interface{}{"-2.3"}, }, }, { name: "Cast_Float64_String_Big", sql: `SELECT CAST(@foo AS STRING)`, params: map[string]Value{ "foo": makeTestValue(383260575764816448.0), }, expected: [][]interface{}{ []interface{}{"3.8326057576481645e+17"}, }, }, { name: "Cast_Float64_String_Inf", sql: `SELECT CAST(@foo AS STRING)`, params: map[string]Value{ "foo": makeTestValue(math.Inf(0)), }, expected: [][]interface{}{ []interface{}{"inf"}, }, }, { name: "Cast_Float64_String_Inf_Neg", sql: `SELECT CAST(@foo AS STRING)`, params: map[string]Value{ "foo": makeTestValue(math.Inf(-1)), }, expected: [][]interface{}{ []interface{}{"-inf"}, }, }, // TODO // { // name: "Cast_Float64_String_NaN", // sql: `SELECT CAST(@foo AS STRING)`, // params: map[string]Value{ // "foo": makeTestValue(math.NaN()), // }, // expected: [][]interface{}{ // []interface{}{"nan"}, // }, // }, { name: "Cast_Float64_Int64_Big_Small", sql: `SELECT CAST(@foo AS INT64)`, params: map[string]Value{ "foo": makeTestValue(2.3), }, expected: [][]interface{}{ []interface{}{int64(2)}, }, }, { name: "Cast_Float64_Int64_Big_Small_neg", sql: `SELECT CAST(@foo AS INT64)`, params: map[string]Value{ "foo": makeTestValue(-2.3), }, expected: [][]interface{}{ []interface{}{int64(-2)}, }, }, { name: "Cast_Float64_Int64_Big", sql: `SELECT CAST(@foo AS INT64)`, params: map[string]Value{ "foo": makeTestValue(383260575764816448.0), }, expected: [][]interface{}{ []interface{}{int64(383260575764816448)}, }, }, { name: "Cast_Float64_Int64_Round_Pos", sql: `SELECT CAST(@foo AS INT64)`, params: map[string]Value{ "foo": makeTestValue(1.5), }, expected: [][]interface{}{ []interface{}{int64(2)}, }, }, { name: "Cast_Float64_Int64_Round_Neg", sql: `SELECT CAST(@foo AS INT64)`, params: map[string]Value{ "foo": makeTestValue(-0.5), }, expected: [][]interface{}{ []interface{}{int64(-1)}, }, }, { name: "Cast_Float64_Int64_Inf", sql: `SELECT CAST(@foo AS INT64)`, params: map[string]Value{ "foo": makeTestValue(math.Inf(0)), }, code: codes.OutOfRange, msg: regexp.MustCompile(`^Illegal conversion of non-finite floating point number to an integer: inf`), }, { name: "Cast_Float64_Int64_Inf_Neg", sql: `SELECT CAST(@foo AS INT64)`, params: map[string]Value{ "foo": makeTestValue(math.Inf(-1)), }, code: codes.OutOfRange, msg: regexp.MustCompile(`^Illegal conversion of non-finite floating point number to an integer: -inf`), }, // TODO // { // name: "Cast_Float64_Int64_NaN", // sql: `SELECT CAST(@foo AS INT64)`, // params: map[string]Value{ // "foo": makeTestValue(math.NaN()), // }, // code: codes.OutOfRange, // msg: regexp.MustCompile(`^Illegal conversion of non-finite floating point number to an integer: nan`), // }, { name: "Cast_Bool_String_True", sql: `SELECT CAST(true AS STRING)`, expected: [][]interface{}{ []interface{}{"TRUE"}, }, }, { name: "Cast_Bool_String_False", sql: `SELECT CAST(false AS STRING)`, expected: [][]interface{}{ []interface{}{"FALSE"}, }, }, { name: "Cast_Bool_Int64_True", sql: `SELECT CAST(true AS INT64)`, expected: [][]interface{}{ []interface{}{int64(1)}, }, }, { name: "Cast_Bool_Int64_False", sql: `SELECT CAST(false AS INT64)`, expected: [][]interface{}{ []interface{}{int64(0)}, }, }, { name: "Cast_String_Bool_True", sql: `SELECT CAST("TRUE" AS BOOL)`, expected: [][]interface{}{ []interface{}{true}, }, }, { name: "Cast_String_Bool_True2", sql: `SELECT CAST("TrUe" AS BOOL)`, expected: [][]interface{}{ []interface{}{true}, }, }, { name: "Cast_String_Bool_False", sql: `SELECT CAST("FALSE" AS BOOL)`, expected: [][]interface{}{ []interface{}{false}, }, }, { name: "Cast_String_Bool_False2", sql: `SELECT CAST("faLsE" AS BOOL)`, expected: [][]interface{}{ []interface{}{false}, }, }, { name: "Cast_String_Bool_Invalid", sql: `SELECT CAST("xx" AS BOOL)`, code: codes.OutOfRange, msg: regexp.MustCompile(`^Bad bool value: xx`), }, { name: "Cast_String_Int64_Base10_1", sql: `SELECT CAST("100" AS INT64)`, expected: [][]interface{}{ []interface{}{int64(100)}, }, }, { name: "Cast_String_Int64_Base10_2", sql: `SELECT CAST("-100" AS INT64)`, expected: [][]interface{}{ []interface{}{int64(-100)}, }, }, { name: "Cast_String_Int64_Base10_3", sql: `SELECT CAST("+100" AS INT64)`, expected: [][]interface{}{ []interface{}{int64(100)}, }, }, { name: "Cast_String_Int64_Base10_4", sql: `SELECT CAST("0100" AS INT64)`, expected: [][]interface{}{ []interface{}{int64(100)}, }, }, { name: "Cast_String_Int64_Base10_5", sql: `SELECT CAST("00100" AS INT64)`, expected: [][]interface{}{ []interface{}{int64(100)}, }, }, { name: "Cast_String_Int64_Base16_1", sql: `SELECT CAST("0x100" AS INT64)`, expected: [][]interface{}{ []interface{}{int64(256)}, }, }, { name: "Cast_String_Int64_Base16_2", sql: `SELECT CAST("-0x100" AS INT64)`, expected: [][]interface{}{ []interface{}{int64(-256)}, }, }, { name: "Cast_String_Int64_Base16_3", sql: `SELECT CAST("0xABC" AS INT64)`, expected: [][]interface{}{ []interface{}{int64(2748)}, }, }, { name: "Cast_String_Int64_Invalid1", sql: `SELECT CAST("- 100" AS INT64)`, code: codes.OutOfRange, msg: regexp.MustCompile(`^Bad int64 value: - 100`), }, { name: "Cast_String_Int64_Invalid2", sql: `SELECT CAST("00x100" AS INT64)`, code: codes.OutOfRange, msg: regexp.MustCompile(`^Bad int64 value: 00x100`), }, { name: "Cast_String_Float64_1", sql: `SELECT CAST("123.456e-67" AS FLOAT64)`, expected: [][]interface{}{ []interface{}{float64(1.23456e-65)}, }, }, { name: "Cast_String_Float64_2", sql: `SELECT CAST(".1E4" AS FLOAT64)`, expected: [][]interface{}{ []interface{}{float64(1000)}, }, }, { name: "Cast_String_Float64_3", sql: `SELECT CAST("58." AS FLOAT64)`, expected: [][]interface{}{ []interface{}{float64(58)}, }, }, { name: "Cast_String_Float64_4", sql: `SELECT CAST("4e2" AS FLOAT64)`, expected: [][]interface{}{ []interface{}{float64(400)}, }, }, // { // name: "Cast_String_Float64_Nan1", // sql: `SELECT CAST("NaN" AS FLOAT64)`, // expected: [][]interface{}{ // []interface{}{math.NaN()}, // }, // }, // { // name: "Cast_String_Float64_Nan2", // sql: `SELECT CAST("nan" AS FLOAT64)`, // expected: [][]interface{}{ // []interface{}{math.NaN()}, // }, // }, { name: "Cast_String_Float64_Inf1", sql: `SELECT CAST("inf" AS FLOAT64)`, expected: [][]interface{}{ []interface{}{math.Inf(0)}, }, }, { name: "Cast_String_Float64_Inf2", sql: `SELECT CAST("+inf" AS FLOAT64)`, expected: [][]interface{}{ []interface{}{math.Inf(0)}, }, }, { name: "Cast_String_Float64_Inf3", sql: `SELECT CAST("-inf" AS FLOAT64)`, expected: [][]interface{}{ []interface{}{math.Inf(-1)}, }, }, { name: "Cast_String_Float64_Inf4", sql: `SELECT CAST("-Inf" AS FLOAT64)`, expected: [][]interface{}{ []interface{}{math.Inf(-1)}, }, }, { name: "Cast_String_Float64_Invalid", sql: `SELECT CAST("xx" AS FLOAT64)`, code: codes.OutOfRange, msg: regexp.MustCompile(`^Bad double value: xx`), }, { name: "Cast_String_Date_Valid", sql: `SELECT CAST("1999-01-01" AS DATE)`, expected: [][]interface{}{ []interface{}{"1999-01-01"}, }, }, { name: "Cast_String_Date_Valid2", sql: `SELECT CAST("1999-4-5" AS DATE)`, expected: [][]interface{}{ []interface{}{"1999-04-05"}, }, }, { name: "Cast_String_DATE_Invalid", sql: `SELECT CAST("x" AS DATE)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Could not cast literal "x" to type DATE`), }, { name: "Cast_String_DATE_Invalid2", sql: `SELECT CAST("1999" AS DATE)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Could not cast literal "1999" to type DATE`), }, { name: "Cast_String_DATE_Invalid3", sql: `SELECT CAST("1999-01-50" AS DATE)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Could not cast literal "1999-01-50" to type DATE`), }, { name: "Cast_String_Timestamp_Valid", sql: `SELECT CAST("1999-01-02T20:34:56Z" AS TIMESTAMP)`, expected: [][]interface{}{ []interface{}{"1999-01-02T20:34:56Z"}, }, }, { name: "Cast_String_Timestamp_Valid2", sql: `SELECT CAST("1999-01-02 01:02:03.123456" AS TIMESTAMP)`, expected: [][]interface{}{ []interface{}{"1999-01-02T09:02:03.123456Z"}, }, }, { name: "Cast_String_TIMESTAMP_Invalid", sql: `SELECT CAST("x" AS TIMESTAMP)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Could not cast literal "x" to type TIMESTAMP`), }, { name: "Cast_String_TIMESTAMP_Invalid2", sql: `SELECT CAST("1999" AS TIMESTAMP)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Could not cast literal "1999" to type TIMESTAMP`), }, { name: "Cast_String_TIMESTAMP_Invalid3", sql: `SELECT CAST("1999-01-50" AS TIMESTAMP)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Could not cast literal "1999-01-50" to type TIMESTAMP`), }, { name: "Cast_Date_String_Valid", sql: `SELECT CAST(DATE "1999-1-1" AS STRING)`, expected: [][]interface{}{ []interface{}{"1999-01-01"}, }, }, { name: "Cast_Date_Timestamp_Valid", sql: `SELECT CAST(DATE "1999-01-01" AS TIMESTAMP)`, expected: [][]interface{}{ []interface{}{"1999-01-01T08:00:00Z"}, }, }, { name: "Cast_Timestamp_String_Valid", sql: `SELECT CAST(TIMESTAMP "1999-01-02 00:00:00" AS STRING)`, expected: [][]interface{}{ []interface{}{"1999-01-02 00:00:00-08"}, }, }, { name: "Cast_Timestamp_String_Valid2", sql: `SELECT CAST(TIMESTAMP "1999-01-02 00:00:00Z" AS STRING)`, expected: [][]interface{}{ []interface{}{"1999-01-01 16:00:00-08"}, }, }, { name: "Cast_Timestamp_String_Valid3", sql: `SELECT CAST(TIMESTAMP "1999-01-02 00:00:00.123456789Z" AS STRING)`, expected: [][]interface{}{ []interface{}{"1999-01-01 16:00:00.123456789-08"}, }, }, { name: "Cast_Timestamp_Date_Valid", sql: `SELECT CAST(TIMESTAMP "1999-01-02 00:00:00" AS DATE)`, expected: [][]interface{}{ []interface{}{"1999-01-02"}, }, }, { name: "Cast_Timestamp_Date_Valid2", sql: `SELECT CAST(TIMESTAMP "1999-01-02 00:00:00Z" AS DATE)`, expected: [][]interface{}{ []interface{}{"1999-01-01"}, }, }, }, "Function": { { name: "Function_Count", sql: `SELECT COUNT(1) FROM Simple`, expected: [][]interface{}{ []interface{}{int64(3)}, }, }, { name: "Function_Count2", sql: `SELECT COUNT(Id) FROM Simple`, expected: [][]interface{}{ []interface{}{int64(3)}, }, }, { name: "Function_Count3", sql: `SELECT COUNT(Id) AS count FROM Simple`, expected: [][]interface{}{ []interface{}{int64(3)}, }, }, { name: "Function_Count4", sql: `SELECT COUNT("x") FROM Simple`, expected: [][]interface{}{ []interface{}{int64(3)}, }, }, { name: "Function_Count5", sql: `SELECT COUNT(NULL) FROM Simple`, expected: [][]interface{}{ []interface{}{int64(0)}, }, }, { name: "Function_Count_Param", sql: `SELECT COUNT(@foo) FROM Simple`, params: map[string]Value{ "foo": makeTestValue(200), }, expected: [][]interface{}{ []interface{}{int64(3)}, }, }, { name: "Function_Sign", sql: `SELECT SIGN(1)`, expected: [][]interface{}{ []interface{}{int64(1)}, }, }, { name: "Function_Sign2", sql: `SELECT SIGN(-1)`, expected: [][]interface{}{ []interface{}{int64(-1)}, }, }, { name: "Function_Sign3", sql: `SELECT SIGN(0)`, expected: [][]interface{}{ []interface{}{int64(0)}, }, }, { name: "Function_StartsWith", sql: `SELECT STARTS_WITH("abc", "ab")`, expected: [][]interface{}{ []interface{}{true}, }, }, { name: "Function_StartsWith2", sql: `SELECT STARTS_WITH("abc", "xx")`, expected: [][]interface{}{ []interface{}{false}, }, }, { name: "Function_StartsWith_Param", sql: `SELECT STARTS_WITH(@a, @b)`, params: map[string]Value{ "a": makeTestValue("xyz"), "b": makeTestValue("xy"), }, expected: [][]interface{}{ []interface{}{true}, }, }, { name: "Function_Max_Int", sql: `SELECT MAX(x) FROM UNNEST([100, 200, 300]) AS x`, expected: [][]interface{}{ []interface{}{int64(300)}, }, }, { name: "Function_Max_String", sql: `SELECT MAX(x) FROM UNNEST(["xxx", "zz", "yy"]) AS x`, expected: [][]interface{}{ []interface{}{"zz"}, }, }, { name: "Function_Min_Int", sql: `SELECT MIN(x) FROM UNNEST([100, 200, 300]) AS x`, expected: [][]interface{}{ []interface{}{int64(100)}, }, }, { name: "Function_Min_String", sql: `SELECT MIN(x) FROM UNNEST(["xxx", "zz", "yy"]) AS x`, expected: [][]interface{}{ []interface{}{"xxx"}, }, }, { name: "Function_Avg", sql: `SELECT AVG(x) as avg FROM UNNEST([100, 200, NULL, 300, 100]) AS x`, expected: [][]interface{}{ []interface{}{float64(175)}, }, }, { name: "Function_Avg_Distinct", sql: `SELECT AVG(DISTINCT x) as avg FROM UNNEST([100, 200, NULL, 300, 100]) AS x`, expected: [][]interface{}{ []interface{}{float64(200)}, }, }, { name: "Function_Sum", sql: `SELECT SUM(x) as avg FROM UNNEST([100, 200, 300]) AS x`, expected: [][]interface{}{ []interface{}{int64(600)}, }, }, // TODO: SUM with float { name: "Function_Concat", sql: `SELECT CONCAT("xx", "yy")`, expected: [][]interface{}{ []interface{}{"xxyy"}, }, }, { name: "Function_Extract_Timestamp", sql: `SELECT` + ` EXTRACT(NANOSECOND FROM t AT TIME ZONE "UTC"),` + ` EXTRACT(MICROSECOND FROM t AT TIME ZONE "UTC"),` + ` EXTRACT(MILLISECOND FROM t AT TIME ZONE "UTC"),` + ` EXTRACT(SECOND FROM t AT TIME ZONE "UTC"),` + ` EXTRACT(MINUTE FROM t AT TIME ZONE "UTC"),` + ` EXTRACT(HOUR FROM t AT TIME ZONE "UTC"),` + ` EXTRACT(DAYOFWEEK FROM t AT TIME ZONE "UTC"),` + ` EXTRACT(DAY FROM t AT TIME ZONE "UTC"),` + ` EXTRACT(DAYOFYEAR FROM t AT TIME ZONE "UTC"),` + ` EXTRACT(ISOWEEK FROM t AT TIME ZONE "UTC"),` + ` EXTRACT(QUARTER FROM t AT TIME ZONE "UTC"),` + ` EXTRACT(YEAR FROM t AT TIME ZONE "UTC"),` + ` EXTRACT(ISOYEAR FROM t AT TIME ZONE "UTC"),` + ` EXTRACT(DATE FROM t AT TIME ZONE "UTC")` + ` FROM (SELECT TIMESTAMP '2012-01-02 12:34:56.987654321Z' t)`, expected: [][]interface{}{ []interface{}{ int64(987654321), int64(987654), int64(987), int64(56), int64(34), int64(12), // sec,min,hour int64(1), int64(2), int64(2), // dayofweek, day, dayofyear int64(1), // isoweek int64(1), int64(2012), int64(2012), // quarter, year, isoyear "2012-01-02", // date }, }, }, { name: "Function_Extract_TimeZone", sql: `SELECT` + ` EXTRACT(DAY FROM t AT TIME ZONE "UTC"),` + ` EXTRACT(HOUR FROM t AT TIME ZONE "UTC"),` + ` EXTRACT(DAY FROM t AT TIME ZONE "Asia/Tokyo"),` + ` EXTRACT(HOUR FROM t AT TIME ZONE "Asia/Tokyo"),` + ` EXTRACT(DAY FROM t AT TIME ZONE "America/Los_Angeles"),` + ` EXTRACT(HOUR FROM t AT TIME ZONE "America/Los_Angeles")` + ` FROM (SELECT TIMESTAMP '2012-01-01 00:00:00.999999999Z' t)`, expected: [][]interface{}{ []interface{}{int64(1), int64(0), int64(1), int64(9), int64(31), int64(16)}, }, }, { name: "Function_Extract_TimeZone_Invalid", sql: `SELECT EXTRACT(HOUR FROM t AT TIME ZONE "xxx")` + ` FROM (SELECT TIMESTAMP '2012-01-01 00:00:00.999999999Z' t)`, code: codes.OutOfRange, msg: regexp.MustCompile(`^Invalid time zone: xxx`), }, { name: "Function_Extract_FixedTimeZone", sql: `SELECT` + ` EXTRACT(HOUR FROM t AT TIME ZONE "+01:20"),` + ` EXTRACT(MINUTE FROM t AT TIME ZONE "+01:20"),` + ` EXTRACT(HOUR FROM t AT TIME ZONE "-01:20"),` + ` EXTRACT(MINUTE FROM t AT TIME ZONE "-01:20")` + ` FROM (SELECT TIMESTAMP '2012-01-01 12:00:00.999999999Z' t)`, expected: [][]interface{}{ []interface{}{int64(13), int64(20), int64(10), int64(40)}, }, }, { name: "Function_Extract_FixedTimeZone2", sql: `SELECT` + ` EXTRACT(HOUR FROM t AT TIME ZONE "+1"),` + ` EXTRACT(MINUTE FROM t AT TIME ZONE "+1"),` + ` EXTRACT(HOUR FROM t AT TIME ZONE "-1"),` + ` EXTRACT(MINUTE FROM t AT TIME ZONE "-1")` + ` FROM (SELECT TIMESTAMP '2012-01-01 12:00:00.999999999Z' t)`, expected: [][]interface{}{ []interface{}{int64(13), int64(0), int64(11), int64(0)}, }, }, { name: "Function_Extract_FixedTimeZone3", sql: `SELECT` + ` EXTRACT(HOUR FROM t AT TIME ZONE "+11"),` + ` EXTRACT(MINUTE FROM t AT TIME ZONE "+11"),` + ` EXTRACT(HOUR FROM t AT TIME ZONE "-11"),` + ` EXTRACT(MINUTE FROM t AT TIME ZONE "-11")` + ` FROM (SELECT TIMESTAMP '2012-01-01 12:00:00.999999999Z' t)`, expected: [][]interface{}{ []interface{}{int64(23), int64(0), int64(1), int64(0)}, }, }, { name: "Function_Extract_FixedTimeZone4", sql: `SELECT` + ` EXTRACT(HOUR FROM t AT TIME ZONE "+11:3"),` + ` EXTRACT(MINUTE FROM t AT TIME ZONE "+11:3"),` + ` EXTRACT(HOUR FROM t AT TIME ZONE "-11:3"),` + ` EXTRACT(MINUTE FROM t AT TIME ZONE "-11:3")` + ` FROM (SELECT TIMESTAMP '2012-01-01 12:00:00.999999999Z' t)`, expected: [][]interface{}{ []interface{}{int64(23), int64(3), int64(0), int64(57)}, }, }, { name: "Function_Extract_FixedTimeZone5", sql: `SELECT` + ` EXTRACT(HOUR FROM t AT TIME ZONE "+1:35"),` + ` EXTRACT(MINUTE FROM t AT TIME ZONE "+1:35"),` + ` EXTRACT(HOUR FROM t AT TIME ZONE "-1:35"),` + ` EXTRACT(MINUTE FROM t AT TIME ZONE "-1:35")` + ` FROM (SELECT TIMESTAMP '2012-01-01 12:00:00.999999999Z' t)`, expected: [][]interface{}{ []interface{}{int64(13), int64(35), int64(10), int64(25)}, }, }, { name: "Function_Extract_FixedTimeZone_Invalid", sql: `SELECT EXTRACT(HOUR FROM t AT TIME ZONE "11:11")` + ` FROM (SELECT TIMESTAMP '2012-01-01 12:00:00.999999999Z' t)`, code: codes.OutOfRange, msg: regexp.MustCompile(`^Invalid time zone: 11:11`), }, { name: "Function_Extract_FixedTimeZone_Invalid2", sql: `SELECT EXTRACT(HOUR FROM t AT TIME ZONE "+:11")` + ` FROM (SELECT TIMESTAMP '2012-01-01 12:00:00.999999999Z' t)`, code: codes.OutOfRange, msg: regexp.MustCompile(`^Invalid time zone: \+:11`), }, { name: "Function_Extract_FixedTimeZone_Invalid3", sql: `SELECT EXTRACT(HOUR FROM t AT TIME ZONE "+11:")` + ` FROM (SELECT TIMESTAMP '2012-01-01 12:00:00.999999999Z' t)`, code: codes.OutOfRange, msg: regexp.MustCompile(`^Invalid time zone: \+11:`), }, { name: "Function_Extract_FixedTimeZone_Invalid4", sql: `SELECT EXTRACT(HOUR FROM t AT TIME ZONE "+")` + ` FROM (SELECT TIMESTAMP '2012-01-01 12:00:00.999999999Z' t)`, code: codes.OutOfRange, msg: regexp.MustCompile(`^Invalid time zone: \+`), }, { name: "Function_Extract_FixedTimeZone_Invalid5", sql: `SELECT EXTRACT(HOUR FROM t AT TIME ZONE "+25:00")` + ` FROM (SELECT TIMESTAMP '2012-01-01 12:00:00.999999999Z' t)`, code: codes.OutOfRange, msg: regexp.MustCompile(`^Invalid time zone: \+25:00`), }, { name: "Function_Extract_FixedTimeZone_Invalid6", sql: `SELECT EXTRACT(HOUR FROM t AT TIME ZONE "+10:61")` + ` FROM (SELECT TIMESTAMP '2012-01-01 12:00:00.999999999Z' t)`, code: codes.OutOfRange, msg: regexp.MustCompile(`^Invalid time zone: \+10:61`), }, { name: "Function_Extract_Timestamp_WithoutTimezone", sql: `SELECT EXTRACT(HOUR FROM t)` + ` FROM (SELECT TIMESTAMP '2012-01-02 12:34:56.987654321Z' t)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^handy-spanner: please specify timezone explicitly.`), }, { name: "Function_Extract_Timestamp_TimeZone_ByParam", sql: `SELECT EXTRACT(HOUR FROM t AT TIME ZONE @foo)` + ` FROM (SELECT TIMESTAMP '2012-01-02 12:34:56.987654321Z' t)`, params: map[string]Value{ "foo": makeTestValue("UTC"), }, expected: [][]interface{}{ []interface{}{int64(12)}, }, }, { name: "Function_Extract_Timestamp_TimeZone_InvalidType", sql: `SELECT EXTRACT(HOUR FROM t AT TIME ZONE @foo)` + ` FROM (SELECT TIMESTAMP '2012-01-02 12:34:56.987654321Z' t)`, params: map[string]Value{ "foo": makeTestValue(int64(100)), }, code: codes.InvalidArgument, msg: regexp.MustCompile(`^No matching signature for function EXTRACT for argument types: DATE_TIME_PART FROM TIMESTAMP AT TIME ZONE INT64. Supported signatures`), }, { name: "Function_Extract_Date", sql: `SELECT` + ` EXTRACT(DAYOFWEEK FROM t),` + ` EXTRACT(DAY FROM t),` + ` EXTRACT(DAYOFYEAR FROM t),` + ` EXTRACT(ISOWEEK FROM t),` + ` EXTRACT(QUARTER FROM t),` + ` EXTRACT(YEAR FROM t),` + ` EXTRACT(ISOYEAR FROM t)` + ` FROM (SELECT DATE '2012-01-02' t)`, expected: [][]interface{}{ []interface{}{ int64(1), int64(2), int64(2), // dayofweek, day, dayofyear int64(1), // isoweek int64(1), int64(2012), int64(2012), // quarter, year, isoyear }, }, }, { name: "Function_Extract_Date_InvalidPart_Nano", sql: `SELECT EXTRACT(NANOSECOND FROM t)` + ` FROM (SELECT DATE '2012-01-02' t)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^EXTRACT from DATE does not support the NANOSECOND date part`), }, { name: "Function_Extract_Date_InvalidPart_Date", sql: `SELECT EXTRACT(DATE FROM t)` + ` FROM (SELECT DATE '2012-01-02' t)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^EXTRACT from DATE does not support the DATE date part`), }, { name: "Function_Extract_Date_WithTimeZone", sql: `SELECT EXTRACT(DAY FROM t AT TIME ZONE "UTC")` + ` FROM (SELECT DATE '2012-01-02' t)`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^EXTRACT from DATE does not support AT TIME ZONE`), }, { name: "IFNULL_Null_IntLiteral", sql: `SELECT IFNULL(NULL, 1) as result`, names: []string{"result"}, expected: [][]interface{}{ []interface{}{int64(1)}, }, }, { name: "IFNULL_IntLiteral_Null", sql: `SELECT IFNULL(2, 0) as result`, names: []string{"result"}, expected: [][]interface{}{ []interface{}{int64(2)}, }, }, { name: "IFNULL_IntLiteral_IntLiteral", sql: `SELECT IFNULL(1, 2) as result`, names: []string{"result"}, expected: [][]interface{}{ []interface{}{int64(1)}, }, }, { name: "IFNULL_Null_Null", sql: `SELECT IFNULL(NULL, NULL) as result`, names: []string{"result"}, expected: [][]interface{}{ []interface{}{nil}, }, }, { name: "IFNULL_Param_Null", sql: `SELECT IFNULL(@foo, NULL) as result`, params: map[string]Value{ "foo": makeTestValue(int64(100)), }, names: []string{"result"}, expected: [][]interface{}{ []interface{}{int64(100)}, }, }, { name: "IFNULL_SubQuery_IntLiteral", sql: `SELECT IFNULL((SELECT NULL), 3) as result`, names: []string{"result"}, expected: [][]interface{}{ []interface{}{int64(3)}, }, }, { name: "IFNULL_NotMatchSignature_Int_String", sql: `SELECT IFNULL(1, "a") as result`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^arguments does not match for IFNULL`), // TODO }, { name: "NULLIF_Equal", sql: `SELECT NULLIF(0, 0) as result`, names: []string{"result"}, expected: [][]interface{}{ []interface{}{nil}, }, }, { name: "NULLIF_NotEqual", sql: `SELECT NULLIF(1, 0) as result`, names: []string{"result"}, expected: [][]interface{}{ []interface{}{int64(1)}, }, }, { name: "NULLIF_NotEqual2", sql: `SELECT NULLIF(0, 1) as result`, names: []string{"result"}, expected: [][]interface{}{ []interface{}{int64(0)}, }, }, { name: "NULLIF_NotMatchSignature_Int_String", sql: `SELECT NULLIF(1, "a") as result`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^arguments does not match for NULLIF`), // TODO }, { name: "Function_Mod", sql: `SELECT MOD(21, 2) as result`, names: []string{"result"}, expected: [][]interface{}{ []interface{}{int64(1)}, }, }, { name: "Function_GENERATE_ARRAY", sql: `SELECT GENERATE_ARRAY(1, 3)`, expected: [][]interface{}{ {[]int64{1, 2, 3}}, }, }, { name: "Function_GENERATE_ARRAY2", sql: `SELECT GENERATE_ARRAY(1, 10, 2)`, expected: [][]interface{}{ {[]int64{1, 3, 5, 7, 9}}, }, }, }, } for name, tcs := range table { t.Run(name, func(t *testing.T) { tcs := tcs t.Parallel() for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { tc := tc ses := newSession(db, "foo") testRunInTransaction(t, ses, func(tx *transaction) { stmt, err := (&memefish.Parser{ Lexer: &memefish.Lexer{ File: &token.File{FilePath: "", Buffer: tc.sql}, }, }).ParseQuery() if err != nil { t.Fatalf("failed to parse sql: %q %v", tc.sql, err) } // The test case expects OK, it checks respons values. // otherwise it checks the error code and the error message. if tc.code == codes.OK { it, err := db.Query(ctx, tx, stmt, tc.params) if err != nil { t.Fatalf("Query failed: %v", err) } var rows [][]interface{} err = it.Do(func(row []interface{}) error { rows = append(rows, row) return nil }) if err != nil { t.Fatalf("unexpected error in iteration: %v", err) } if diff := cmp.Diff(tc.expected, rows); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } // TODO: add names to all test cases. now this is optional check if tc.names != nil { var gotnames []string for _, item := range it.ResultSet() { gotnames = append(gotnames, item.Name) } if diff := cmp.Diff(tc.names, gotnames); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } } } else { it, err := db.Query(ctx, tx, stmt, tc.params) if err == nil { err = it.Do(func([]interface{}) error { return nil }) } st := status.Convert(err) if st.Code() != tc.code { t.Errorf("expect code to be %v but got %v", tc.code, st.Code()) } if !tc.msg.MatchString(st.Message()) { t.Errorf("unexpected error message: \n %q\n expected:\n %q", st.Message(), tc.msg) } } }) }) } }) } } func TestQuery_ArbitraryAssertion(t *testing.T) { ctx := context.Background() db := newDatabase() for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } table := []struct { name string sql string params map[string]Value assert func(t *testing.T, rows [][]interface{}) }{ { name: "Sample", sql: `SELECT 1`, assert: func(t *testing.T, rows [][]interface{}) { expected := [][]interface{}{ []interface{}{int64(1)}, } if diff := cmp.Diff(expected, rows); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }, }, { name: "CURRENT_TIMESTAMP", sql: `SELECT CURRENT_TIMESTAMP()`, assert: func(t *testing.T, rows [][]interface{}) { if len(rows) != 1 { t.Fatalf("the number of rows should be 1 but got %v", len(rows)) } if len(rows[0]) != 1 { t.Fatalf("the number of columns should be 1 but got %v", len(rows[0])) } ts, ok := rows[0][0].(string) if !ok { t.Fatalf("the column should be string but %T", rows[0][0]) } ts2, ok := parseTimestampLiteral(ts) if !ok { t.Fatalf("failed to parse timestamp: %v", ts) } if time.Since(ts2) > 3*time.Second { t.Fatalf("unexpected time: %v, %v", ts2, time.Now()) } }, }, } for _, tc := range table { t.Run(tc.name, func(t *testing.T) { tc := tc ses := newSession(db, "foo") testRunInTransaction(t, ses, func(tx *transaction) { stmt, err := (&memefish.Parser{ Lexer: &memefish.Lexer{ File: &token.File{FilePath: "", Buffer: tc.sql}, }, }).ParseQuery() if err != nil { t.Fatalf("failed to parse sql: %q %v", tc.sql, err) } it, err := db.Query(ctx, tx, stmt, tc.params) if err != nil { t.Fatalf("Query failed: %v", err) } var rows [][]interface{} err = it.Do(func(row []interface{}) error { rows = append(rows, row) return nil }) if err != nil { t.Fatalf("unexpected error in iteration: %v", err) } tc.assert(t, rows) }) }) } } ================================================ FILE: server/database_test.go ================================================ // Copyright 2019 Masahiro Sano // // 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 server import ( "context" "fmt" "regexp" "strconv" "strings" "testing" "time" "github.com/cloudspannerecosystem/memefish" "github.com/cloudspannerecosystem/memefish/ast" "github.com/cloudspannerecosystem/memefish/token" cmp "github.com/google/go-cmp/cmp" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/testing/protocmp" structpb "google.golang.org/protobuf/types/known/structpb" ) var ( allSchema = []string{schemaSimple, schemaInterleaved, schemaInterleavedCascade, schemaInterleavedNoAction, schemaForeignCascade, schemaForeignNoAction, schemaCompositePrimaryKeys, schemaFullTypes, schemaArrayTypes, schemaJoinA, schemaJoinB, schemaFromTable, schemaGeneratedValues, schemaGeneratedColumn, schemaDefaultValues, schemaChangeStream} schemaSimple = `CREATE TABLE Simple ( Id INT64 NOT NULL, Value STRING(MAX) NOT NULL, ) PRIMARY KEY(Id); ` schemaInterleaved = `CREATE TABLE ParentTable ( Id INT64 NOT NULL, Value STRING(MAX) NOT NULL, ) PRIMARY KEY(Id); CREATE TABLE Interleaved ( InterleavedId INT64 NOT NULL, Id INT64 NOT NULL, Value STRING(MAX) NOT NULL, ) PRIMARY KEY(Id, InterleavedId), INTERLEAVE IN PARENT ParentTable; CREATE INDEX InterleavedKey ON Interleaved(Id, Value), INTERLEAVE IN ParentTable ` schemaInterleavedCascade = `CREATE TABLE ParentTableCascade ( Id INT64 NOT NULL, Value STRING(MAX) NOT NULL, ) PRIMARY KEY(Id); CREATE TABLE InterleavedCascade ( InterleavedId INT64 NOT NULL, Id INT64 NOT NULL, Value STRING(MAX) NOT NULL, ) PRIMARY KEY(Id, InterleavedId), INTERLEAVE IN PARENT ParentTableCascade ON DELETE CASCADE; ` schemaInterleavedNoAction = `CREATE TABLE ParentTableNoAction ( Id INT64 NOT NULL, Value STRING(MAX) NOT NULL, ) PRIMARY KEY(Id); CREATE TABLE InterleavedNoAction ( InterleavedId INT64 NOT NULL, Id INT64 NOT NULL, Value STRING(MAX) NOT NULL, ) PRIMARY KEY(Id, InterleavedId), INTERLEAVE IN PARENT ParentTableNoAction ON DELETE NO ACTION; ` schemaForeignCascade = `CREATE TABLE ForeignParentCascade ( Id INT64 NOT NULL, Value STRING(MAX) NOT NULL, ) PRIMARY KEY(Id); CREATE TABLE ForeignChildCascade ( Id INT64 NOT NULL, ForeignId INT64 NOT NULL, ForeignSecondId INT64 NOT NULL, Value STRING(MAX) NOT NULL, CONSTRAINT FK_Cascade FOREIGN KEY (ForeignId) REFERENCES ForeignParentCascade (Id) ON DELETE CASCADE, FOREIGN KEY (ForeignSecondId) REFERENCES ForeignParentCascade (Id) ON DELETE CASCADE, ) PRIMARY KEY(Id, ForeignId); ` schemaForeignNoAction = `CREATE TABLE ForeignParentNoAction ( Id INT64 NOT NULL, Value STRING(MAX) NOT NULL, ) PRIMARY KEY(Id); CREATE TABLE ForeignChildNoAction ( Id INT64 NOT NULL, ForeignId INT64 NOT NULL, ForeignSecondId INT64 NOT NULL, Value STRING(MAX) NOT NULL, CONSTRAINT FK_NoAction FOREIGN KEY (ForeignId) REFERENCES ForeignParentNoAction (Id), FOREIGN KEY (ForeignSecondId) REFERENCES ForeignParentNoAction (Id), ) PRIMARY KEY(Id, ForeignId); ` schemaCompositePrimaryKeys = `CREATE TABLE CompositePrimaryKeys ( Id INT64 NOT NULL, PKey1 STRING(32) NOT NULL, PKey2 INT64 NOT NULL, Error INT64 NOT NULL, X STRING(32) NOT NULL, Y STRING(32) NOT NULL, Z STRING(32) NOT NULL, ) PRIMARY KEY(PKey1, PKey2 DESC); CREATE INDEX CompositePrimaryKeysByXY ON CompositePrimaryKeys(X, Y DESC) STORING (Z); CREATE INDEX CompositePrimaryKeysByError ON CompositePrimaryKeys(Error); ` schemaFullTypes = `CREATE TABLE FullTypes ( PKey STRING(32) NOT NULL, FTString STRING(32) NOT NULL, FTStringNull STRING(32), FTBool BOOL NOT NULL, FTBoolNull BOOL, FTBytes BYTES(32) NOT NULL, FTBytesNull BYTES(32), FTTimestamp TIMESTAMP NOT NULL, FTTimestampNull TIMESTAMP OPTIONS (allow_commit_timestamp=true), FTInt INT64 NOT NULL, FTIntNull INT64, FTFloat FLOAT64 NOT NULL, FTFloatNull FLOAT64, FTDate DATE NOT NULL, FTDateNull DATE, ) PRIMARY KEY(PKey); CREATE UNIQUE INDEX FullTypesByFTString ON FullTypes(FTString); CREATE UNIQUE INDEX FullTypesByIntDate ON FullTypes(FTInt, FTDate); CREATE INDEX FullTypesByIntTimestamp ON FullTypes(FTInt, FTTimestamp); CREATE INDEX FullTypesByTimestamp ON FullTypes(FTTimestamp); ` schemaArrayTypes = `CREATE TABLE ArrayTypes ( Id INT64 NOT NULL, ArrayString ARRAY, ArrayBool ARRAY, ArrayBytes ARRAY, ArrayTimestamp ARRAY, ArrayInt ARRAY, ArrayFloat ARRAY, ArrayDate ARRAY, ) PRIMARY KEY(Id); ` schemaJoinA = `CREATE TABLE JoinA ( Id INT64 NOT NULL, Value STRING(MAX) NOT NULL, ) PRIMARY KEY(Id); ` schemaJoinB = `CREATE TABLE JoinB ( Id INT64 NOT NULL, Value STRING(MAX) NOT NULL, ) PRIMARY KEY(Id); ` schemaFromTable = "CREATE TABLE `From` (" + "`ALL` INT64 NOT NULL," + "`CAST` INT64 NOT NULL, " + "`JOIN` INT64 NOT NULL, " + ") PRIMARY KEY(`ALL`); \n" + "CREATE INDEX `ALL` ON `From`(`ALL`);" schemaGeneratedColumn = `CREATE TABLE GeneratedColumn ( Id INT64 NOT NULL, Value STRING(MAX) NOT NULL AS (CAST(Id AS STRING)) STORED, ) PRIMARY KEY(Id); ` schemaGeneratedValues = `CREATE TABLE GeneratedValues ( Id INT64 NOT NULL, Value STRING(32) NOT NULL, N INT64 NOT NULL AS (1) STORED, N2 INT64 NOT NULL AS (1+1) STORED, N3 INT64 NOT NULL AS (N2+1) STORED, X STRING(32) NOT NULL AS ("Value") STORED, X2 STRING(32) NOT NULL AS (CONCAT("Value", "Y")) STORED, ) PRIMARY KEY(Id); ` schemaDefaultValues = `CREATE TABLE DefaultValues ( Id INT64 NOT NULL, Value STRING(32) NOT NULL, N INT64 NOT NULL DEFAULT (1), X STRING(32) NOT NULL DEFAULT ("x"), Y ARRAY NOT NULL DEFAULT (ARRAY[10, 20]), T1 TIMESTAMP NOT NULL DEFAULT (CURRENT_TIMESTAMP()), T2 TIMESTAMP NOT NULL DEFAULT (current_timestamp()), Date TIMESTAMP NOT NULL DEFAULT (CURRENT_DATE()), ) PRIMARY KEY(Id); ` schemaChangeStream = `CREATE CHANGE STREAM EverythingStream FOR ALL; ` compositePrimaryKeysKeys = []string{ "Id", "PKey1", "PKey2", "Error", "X", "Y", "Z", } fullTypesKeys = []string{ "PKey", "FTString", "FTStringNull", "FTBool", "FTBoolNull", "FTBytes", "FTBytesNull", "FTTimestamp", "FTTimestampNull", "FTInt", "FTIntNull", "FTFloat", "FTFloatNull", "FTDate", "FTDateNull", } arrayTypesKeys = []string{ "Id", "ArrayString", "ArrayBool", "ArrayBytes", "ArrayTimestamp", "ArrayInt", "ArrayFloat", "ArrayDate", } fromTableKeys = []string{"ALL", "CAST", "JOIN"} ) var initialData = []struct { table string cols []string values [][]*structpb.Value }{ { table: "Simple", cols: []string{"Id", "Value"}, values: [][]*structpb.Value{ { makeStringValue("100"), makeStringValue("xxx"), }, }, }, { table: "CompositePrimaryKeys", cols: compositePrimaryKeysKeys, values: [][]*structpb.Value{ { makeStringValue("1"), // Id INT64 NOT NULL, makeStringValue("pkey1xxx"), // PKey1 STRING(32) NOT NULL, makeStringValue("100"), // PKey2 INT64 NOT NULL, makeStringValue("2"), // Error INT64 NOT NULL, makeStringValue("x1"), // X STRING(32) NOT NULL, makeStringValue("y1"), // Y STRING(32) NOT NULL, makeStringValue("z1"), // Z STRING(32) NOT NULL, }, }, }, { table: "FullTypes", cols: fullTypesKeys, values: [][]*structpb.Value{ { makeStringValue("xxx"), // PKey STRING(32) NOT NULL, makeStringValue("xxx"), // FTString STRING(32) NOT NULL, makeStringValue("xxx"), // FTStringNull STRING(32), makeBoolValue(true), // FTBool BOOL NOT NULL, makeBoolValue(true), // FTBoolNull BOOL, makeStringValue("eHl6"), // FTBytes BYTES(32) NOT NULL, makeStringValue("eHl6"), // FTBytesNull BYTES(32), makeStringValue("2012-03-04T12:34:56.123456789Z"), // FTTimestamp TIMESTAMP NOT NULL, makeStringValue("2012-03-04T12:34:56.123456789Z"), // FTTimestampNull TIMESTAMP, makeStringValue("100"), // FTInt INT64 NOT NULL, makeStringValue("100"), // FTIntNull INT64, makeNumberValue(0.5), // FTFloat FLOAT64 NOT NULL, makeNumberValue(0.5), // FTFloatNull FLOAT64, makeStringValue("2012-03-04"), // FTDate DATE NOT NULL, makeStringValue("2012-03-04"), // FTDateNull DATE, }, }, }, { table: "FullTypes", cols: fullTypesKeys, values: [][]*structpb.Value{ { makeStringValue("yyy"), // PKey STRING(32) NOT NULL, makeStringValue("yyy"), // FTString STRING(32) NOT NULL, makeStringValue("yyy"), // FTStringNull STRING(32), makeBoolValue(true), // FTBool BOOL NOT NULL, makeBoolValue(true), // FTBoolNull BOOL, makeStringValue("eHl6"), // FTBytes BYTES(32) NOT NULL, makeStringValue("eHl6"), // FTBytesNull BYTES(32), makeStringValue("2012-03-04T12:34:56.123456789Z"), // FTTimestamp TIMESTAMP NOT NULL, makeStringValue("2012-03-04T12:34:56.123456789Z"), // FTTimestampNull TIMESTAMP, makeStringValue("101"), // FTInt INT64 NOT NULL, makeStringValue("101"), // FTIntNull INT64, makeNumberValue(0.5), // FTFloat FLOAT64 NOT NULL, makeNumberValue(0.5), // FTFloatNull FLOAT64, makeStringValue("2012-03-04"), // FTDate DATE NOT NULL, makeStringValue("2012-03-04"), // FTDateNull DATE, }, }, }, { table: "ArrayTypes", cols: arrayTypesKeys, values: [][]*structpb.Value{ { makeStringValue("100"), makeListValueAsValue(makeListValue( makeStringValue("yyy"), makeStringValue("yyy"), )), makeListValueAsValue(makeListValue( makeBoolValue(true), makeBoolValue(true), )), makeListValueAsValue(makeListValue( makeStringValue("eHl6"), makeStringValue("eHl6"), )), makeListValueAsValue(makeListValue( makeStringValue("2012-03-04T12:34:56.123456789Z"), makeStringValue("2012-03-04T12:34:56.123456789Z"), )), makeListValueAsValue(makeListValue( makeStringValue("101"), makeStringValue("101"), )), makeListValueAsValue(makeListValue( makeNumberValue(0.5), makeNumberValue(0.5), )), makeListValueAsValue(makeListValue( makeStringValue("2012-03-04"), makeStringValue("2012-03-04"), )), }, }, }, { table: "From", cols: fromTableKeys, values: [][]*structpb.Value{ { makeStringValue("1"), makeStringValue("1"), makeStringValue("1"), }, }, }, } func testRunInTransaction(t *testing.T, ses *session, fn func(*transaction)) { t.Helper() tx, err := ses.createTransaction(txReadWrite, false) if err != nil { t.Fatalf("createTransaction failed: %v", err) } defer tx.Done(TransactionCommited) fn(tx) if err := ses.database.Commit(tx); err != nil { t.Fatalf("commit failed: %v", err) } } func createInitialData(t *testing.T, ctx context.Context, db Database, tx *transaction) { t.Helper() for _, d := range initialData { for _, values := range d.values { listValues := []*structpb.ListValue{ {Values: values}, } if err := db.Insert(ctx, tx, d.table, d.cols, listValues); err != nil { t.Fatalf("Insert failed: %v", err) } } } } func makeNullValue() *structpb.Value { return &structpb.Value{Kind: &structpb.Value_NullValue{}} } func makeNumberValue(n float64) *structpb.Value { return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: n}} } func makeStringValue(s string) *structpb.Value { return &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: s}} } func makeBoolValue(b bool) *structpb.Value { return &structpb.Value{Kind: &structpb.Value_BoolValue{BoolValue: b}} } func makeStructValue(v map[string]*structpb.Value) *structpb.Value { return &structpb.Value{Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{ Fields: v, }}} } func makeListValue(vs ...*structpb.Value) *structpb.ListValue { return &structpb.ListValue{Values: vs} } func makeListValueAsValue(v *structpb.ListValue) *structpb.Value { return &structpb.Value{Kind: &structpb.Value_ListValue{ListValue: v}} } func makeTestValue(v interface{}) Value { switch vv := v.(type) { case string: return Value{ Data: v, Type: ValueType{Code: TCString}, } case []string: return Value{ Data: v, Type: ValueType{ Code: TCArray, ArrayType: &ValueType{Code: TCString}, }, } case int64: return Value{ Data: v, Type: ValueType{Code: TCInt64}, } case int: return Value{ Data: int64(vv), Type: ValueType{Code: TCInt64}, } case []int64: return Value{ Data: v, Type: ValueType{ Code: TCArray, ArrayType: &ValueType{Code: TCInt64}, }, } case float64: return Value{ Data: v, Type: ValueType{Code: TCFloat64}, } default: panic(fmt.Sprintf("fix makeTestValue to be able to convert interface{} to Value: %T", v)) } } func makeTestWrappedArray(code TypeCode, vs ...interface{}) interface{} { switch code { case TCBool: arr := make([]*bool, len(vs)) for i := range vs { if vs[i] == nil { arr[i] = nil } else { s := new(bool) *s = vs[i].(bool) arr[i] = s } } return &ArrayValueEncoder{Values: arr} case TCString: arr := make([]*string, len(vs)) for i := range vs { if vs[i] == nil { arr[i] = nil } else { s := new(string) *s = vs[i].(string) arr[i] = s } } return &ArrayValueEncoder{Values: arr} case TCInt64: arr := make([]*int64, len(vs)) for i := range vs { if vs[i] == nil { arr[i] = nil } else { s := new(int64) vv, ok := vs[i].(int64) if !ok { vv = int64(vs[i].(int)) } *s = vv arr[i] = s } } return &ArrayValueEncoder{Values: arr} case TCFloat64: arr := make([]*float64, len(vs)) for i := range vs { if vs[i] == nil { arr[i] = nil } else { s := new(float64) *s = vs[i].(float64) arr[i] = s } } return &ArrayValueEncoder{Values: arr} case TCBytes: arr := make([][]byte, len(vs)) for i := range vs { if vs[i] == nil { arr[i] = nil } else { arr[i] = vs[i].([]byte) } } return &ArrayValueEncoder{Values: arr} default: panic(fmt.Sprintf("fix makeTestArray to be able to convert interface{}: %v", code)) } } func makeTestArray(code TypeCode, vs ...interface{}) interface{} { switch code { case TCBool: arr := make([]*bool, len(vs)) for i := range vs { if vs[i] == nil { arr[i] = nil } else { s := new(bool) *s = vs[i].(bool) arr[i] = s } } return arr case TCString: arr := make([]*string, len(vs)) for i := range vs { if vs[i] == nil { arr[i] = nil } else { s := new(string) *s = vs[i].(string) arr[i] = s } } return arr case TCInt64: arr := make([]*int64, len(vs)) for i := range vs { if vs[i] == nil { arr[i] = nil } else { s := new(int64) vv, ok := vs[i].(int64) if !ok { vv = int64(vs[i].(int)) } *s = vv arr[i] = s } } return arr case TCFloat64: arr := make([]*float64, len(vs)) for i := range vs { if vs[i] == nil { arr[i] = nil } else { s := new(float64) *s = vs[i].(float64) arr[i] = s } } return arr case TCBytes: arr := make([][]byte, len(vs)) for i := range vs { if vs[i] == nil { arr[i] = nil } else { arr[i] = vs[i].([]byte) } } return arr default: panic(fmt.Sprintf("fix makeTestArray to be able to convert interface{}: %v", code)) } } func parseDDL(t *testing.T, s string) []ast.DDL { var ddls []ast.DDL stmts := strings.Split(s, ";") for _, stmt := range stmts { stmt := strings.TrimSpace(stmt) if stmt == "" { continue } ddl, err := (&memefish.Parser{ Lexer: &memefish.Lexer{ File: &token.File{FilePath: "", Buffer: stmt}, }, }).ParseDDL() if err != nil { t.Fatalf("parse error: %v", err) } ddls = append(ddls, ddl) } return ddls } func TestApplyDDL(t *testing.T) { table := []struct { ddl string }{ { ddl: schemaSimple, }, { ddl: schemaCompositePrimaryKeys, }, { ddl: schemaFullTypes, }, { ddl: schemaGeneratedValues, }, { ddl: schemaDefaultValues, }, { ddl: schemaChangeStream, }, } for _, tt := range table { ctx := context.Background() db := newDatabase() ddls := parseDDL(t, tt.ddl) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } } func TestClose(t *testing.T) { db := newDatabase() err := db.Close() if err != nil { t.Fatalf("failed to close: %v", err) } err = db.Close() if err != nil { t.Fatalf("failed to close: %v", err) } } func TestRead(t *testing.T) { ctx := context.Background() db := newDatabase() for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } for _, query := range []string{ `INSERT INTO Simple VALUES(100, "xxx")`, `INSERT INTO Simple VALUES(200, "yyy")`, `INSERT INTO Simple VALUES(300, "zzz")`, `INSERT INTO CompositePrimaryKeys VALUES(1, "aaa", 1, 0, "x1", "y1", "z")`, `INSERT INTO CompositePrimaryKeys VALUES(2, "bbb", 2, 0, "x1", "y2", "z")`, `INSERT INTO CompositePrimaryKeys VALUES(3, "bbb", 3, 0, "x1", "y3", "z")`, `INSERT INTO CompositePrimaryKeys VALUES(4, "ccc", 3, 0, "x2", "y4", "z")`, `INSERT INTO CompositePrimaryKeys VALUES(5, "ccc", 4, 0, "x2", "y5", "z")`, "INSERT INTO `From` VALUES(1, 1, 1)", `INSERT INTO GeneratedValues (Id, Value) VALUES(1, "xxx")`, `INSERT INTO DefaultValues (Id, Value) VALUES(1, "xxx")`, } { if _, err := db.db.ExecContext(ctx, query); err != nil { t.Fatalf("Insert failed: %v", err) } } table := map[string]struct { tbl string idx string cols []string ks *KeySet limit int64 expected [][]interface{} }{ "SimpleFull": { tbl: "Simple", cols: []string{"Id", "Value"}, ks: &KeySet{All: true}, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "yyy"}, []interface{}{int64(300), "zzz"}, }, }, "SimplePartialColumn": { tbl: "Simple", cols: []string{"Value"}, ks: &KeySet{All: true}, limit: 100, expected: [][]interface{}{ []interface{}{"xxx"}, []interface{}{"yyy"}, []interface{}{"zzz"}, }, }, "SimpleColumnOrder": { tbl: "Simple", cols: []string{"Value", "Id"}, ks: &KeySet{All: true}, limit: 100, expected: [][]interface{}{ []interface{}{"xxx", int64(100)}, []interface{}{"yyy", int64(200)}, []interface{}{"zzz", int64(300)}, }, }, "SimpleLimit": { tbl: "Simple", cols: []string{"Id", "Value"}, ks: &KeySet{All: true}, limit: 2, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "yyy"}, }, }, "SimpleDuplicateColumns": { tbl: "Simple", cols: []string{"Id", "Value", "Id", "Value"}, ks: &KeySet{All: true}, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "xxx", int64(100), "xxx"}, []interface{}{int64(200), "yyy", int64(200), "yyy"}, []interface{}{int64(300), "zzz", int64(300), "zzz"}, }, }, // Simple KeySet "Simple_SingleKey": { tbl: "Simple", cols: []string{"Id", "Value"}, ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue(makeStringValue("100")), }, }, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, "Simple_SingleKey_NotFound": { tbl: "Simple", cols: []string{"Id", "Value"}, ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue(makeStringValue("1000")), }, }, limit: 100, expected: nil, }, "Simple_MultiKeys": { tbl: "Simple", cols: []string{"Id", "Value"}, ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue(makeStringValue("100")), makeListValue(makeStringValue("300")), }, }, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(300), "zzz"}, }, }, "Simple_MultiKeys_PartialNotFound": { tbl: "Simple", cols: []string{"Id", "Value"}, ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue(makeStringValue("100")), makeListValue(makeStringValue("300")), makeListValue(makeStringValue("1000")), }, }, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(300), "zzz"}, }, }, "Simple_KeyRange": { tbl: "Simple", cols: []string{"Id", "Value"}, ks: &KeySet{ Ranges: []*KeyRange{ { start: makeListValue(makeStringValue("100")), end: makeListValue(makeStringValue("300")), startClosed: true, endClosed: true, }, }, }, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "yyy"}, []interface{}{int64(300), "zzz"}, }, }, "Simple_KeyRange2": { tbl: "Simple", cols: []string{"Id", "Value"}, ks: &KeySet{ Ranges: []*KeyRange{ { start: makeListValue(makeStringValue("150")), end: makeListValue(makeStringValue("250")), startClosed: true, endClosed: true, }, }, }, limit: 100, expected: [][]interface{}{ []interface{}{int64(200), "yyy"}, }, }, "Simple_KeyRange_OpenClose": { tbl: "Simple", cols: []string{"Id", "Value"}, ks: &KeySet{ Ranges: []*KeyRange{ { start: makeListValue(makeStringValue("100")), end: makeListValue(makeStringValue("300")), startClosed: false, endClosed: true, }, }, }, limit: 100, expected: [][]interface{}{ []interface{}{int64(200), "yyy"}, []interface{}{int64(300), "zzz"}, }, }, "Simple_KeyRange_OpenClose2": { tbl: "CompositePrimaryKeys", cols: []string{"Id", "PKey1", "PKey2"}, ks: &KeySet{ Ranges: []*KeyRange{ { start: makeListValue(makeStringValue("bbb"), makeStringValue("3")), end: makeListValue(makeStringValue("ccc"), makeStringValue("3")), startClosed: false, endClosed: true, }, }, }, limit: 100, expected: [][]interface{}{ []interface{}{int64(4), "ccc", int64(3)}, }, }, "Simple_KeyRange_CloseOpen": { tbl: "Simple", cols: []string{"Id", "Value"}, ks: &KeySet{ Ranges: []*KeyRange{ { start: makeListValue(makeStringValue("100")), end: makeListValue(makeStringValue("300")), startClosed: true, endClosed: false, }, }, }, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "yyy"}, }, }, "Simple_KeyRange_OpenOpen": { tbl: "Simple", cols: []string{"Id", "Value"}, ks: &KeySet{ Ranges: []*KeyRange{ { start: makeListValue(makeStringValue("100")), end: makeListValue(makeStringValue("300")), startClosed: false, endClosed: false, }, }, }, limit: 100, expected: [][]interface{}{ []interface{}{int64(200), "yyy"}, }, }, "Simple_KeyRange_LessPrimaryKey": { tbl: "CompositePrimaryKeys", cols: []string{"Id", "PKey1", "PKey2"}, ks: &KeySet{ Ranges: []*KeyRange{ { start: makeListValue(makeStringValue("bbb"), makeStringValue("2")), end: makeListValue(makeStringValue("bbb")), startClosed: true, endClosed: true, }, }, }, limit: 100, expected: [][]interface{}{ []interface{}{int64(2), "bbb", int64(2)}, }, }, "Simple_KeyRange_LessPrimaryKey2": { tbl: "CompositePrimaryKeys", cols: []string{"Id", "PKey1", "PKey2"}, ks: &KeySet{ Ranges: []*KeyRange{ { start: makeListValue(makeStringValue("bbb")), end: makeListValue(makeStringValue("bbb"), makeStringValue("3")), startClosed: true, endClosed: true, }, }, }, limit: 100, expected: [][]interface{}{ []interface{}{int64(3), "bbb", int64(3)}, }, }, "Simple_KeyRange_LessPrimaryKey3": { tbl: "CompositePrimaryKeys", cols: []string{"Id", "PKey1", "PKey2"}, ks: &KeySet{ Ranges: []*KeyRange{ { start: makeListValue(makeStringValue("bbb")), end: makeListValue(makeStringValue("bbb")), startClosed: true, endClosed: true, }, }, }, expected: [][]interface{}{ []interface{}{int64(3), "bbb", int64(3)}, []interface{}{int64(2), "bbb", int64(2)}, }, }, "Simple_KeyRange_LessPrimaryKeyOpenClosed": { tbl: "CompositePrimaryKeys", cols: []string{"Id", "PKey1", "PKey2"}, ks: &KeySet{ Ranges: []*KeyRange{ { start: makeListValue(makeStringValue("bbb"), makeStringValue("2")), end: makeListValue(makeStringValue("bbb")), startClosed: false, endClosed: true, }, }, }, limit: 100, expected: nil, }, "Simple_KeyRange_LessPrimaryKeyClosedOpen": { tbl: "CompositePrimaryKeys", cols: []string{"Id", "PKey1", "PKey2"}, ks: &KeySet{ Ranges: []*KeyRange{ { start: makeListValue(makeStringValue("bbb")), end: makeListValue(makeStringValue("bbb"), makeStringValue("3")), startClosed: true, endClosed: false, }, }, }, limit: 100, expected: nil, }, // Composite Keys "CompositePrimaryKeys_Keys": { tbl: "CompositePrimaryKeys", cols: []string{"Id", "PKey1", "PKey2"}, ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue(makeStringValue("aaa"), makeStringValue("1")), }, }, limit: 100, expected: [][]interface{}{ []interface{}{int64(1), "aaa", int64(1)}, }, }, "CompositePrimaryKeys_Keys_Multi": { tbl: "CompositePrimaryKeys", cols: []string{"Id", "PKey1", "PKey2"}, ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue(makeStringValue("aaa"), makeStringValue("100")), makeListValue(makeStringValue("xxx"), makeStringValue("2")), makeListValue(makeStringValue("ccc"), makeStringValue("3")), }, }, limit: 100, expected: [][]interface{}{ []interface{}{int64(4), "ccc", int64(3)}, }, }, // Composite Ranges "CompositePrimaryKeys_Ranges": { tbl: "CompositePrimaryKeys", cols: []string{"Id", "PKey1", "PKey2"}, ks: &KeySet{ Ranges: []*KeyRange{ { start: makeListValue(makeStringValue("bbb"), makeStringValue("3")), end: makeListValue(makeStringValue("ccc"), makeStringValue("3")), startClosed: true, endClosed: true, }, }, }, limit: 100, expected: [][]interface{}{ []interface{}{int64(3), "bbb", int64(3)}, []interface{}{int64(4), "ccc", int64(3)}, }, }, "CompositePrimaryKeys_Ranges_Multi": { tbl: "CompositePrimaryKeys", cols: []string{"Id", "PKey1", "PKey2"}, ks: &KeySet{ Ranges: []*KeyRange{ { start: makeListValue(makeStringValue("bbb"), makeStringValue("3")), end: makeListValue(makeStringValue("bbb"), makeStringValue("2")), startClosed: true, endClosed: true, }, { start: makeListValue(makeStringValue("ccc"), makeStringValue("4")), end: makeListValue(makeStringValue("ccc"), makeStringValue("3")), startClosed: true, endClosed: true, }, }, }, limit: 100, expected: [][]interface{}{ []interface{}{int64(3), "bbb", int64(3)}, []interface{}{int64(2), "bbb", int64(2)}, []interface{}{int64(5), "ccc", int64(4)}, []interface{}{int64(4), "ccc", int64(3)}, }, }, // Composite Keys and Ranges "CompositePrimaryKeys_KeysRanges": { tbl: "CompositePrimaryKeys", cols: []string{"Id", "PKey1", "PKey2"}, ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue(makeStringValue("aaa"), makeStringValue("1")), }, Ranges: []*KeyRange{ { start: makeListValue(makeStringValue("bbb"), makeStringValue("3")), end: makeListValue(makeStringValue("bbb"), makeStringValue("2")), startClosed: true, endClosed: true, }, }, }, limit: 100, expected: [][]interface{}{ []interface{}{int64(1), "aaa", int64(1)}, []interface{}{int64(3), "bbb", int64(3)}, []interface{}{int64(2), "bbb", int64(2)}, }, }, // Composite SecondaryIndex "CompositePrimaryKeys_Index": { tbl: "CompositePrimaryKeys", idx: "CompositePrimaryKeysByXY", cols: []string{"PKey1", "PKey2", "X", "Y", "Z"}, ks: &KeySet{All: true}, limit: 100, expected: [][]interface{}{ []interface{}{"bbb", int64(3), "x1", "y3", "z"}, []interface{}{"bbb", int64(2), "x1", "y2", "z"}, []interface{}{"aaa", int64(1), "x1", "y1", "z"}, []interface{}{"ccc", int64(4), "x2", "y5", "z"}, []interface{}{"ccc", int64(3), "x2", "y4", "z"}, }, }, // Escape Keywork "Keyword_All": { tbl: "From", idx: "", cols: fromTableKeys, ks: &KeySet{All: true}, limit: 100, expected: [][]interface{}{ []interface{}{int64(1), int64(1), int64(1)}, }, }, "Keyword_Keys": { tbl: "From", idx: "", cols: fromTableKeys, ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue(makeStringValue("1")), }, }, limit: 100, expected: [][]interface{}{ []interface{}{int64(1), int64(1), int64(1)}, }, }, "Keyword_Ranges": { tbl: "From", idx: "", cols: fromTableKeys, ks: &KeySet{ Ranges: []*KeyRange{ { start: makeListValue(makeStringValue("1")), end: makeListValue(makeStringValue("1")), startClosed: true, endClosed: true, }, }, }, limit: 100, expected: [][]interface{}{ []interface{}{int64(1), int64(1), int64(1)}, }, }, "Keyword_Index": { tbl: "From", idx: "ALL", cols: []string{"ALL"}, ks: &KeySet{All: true}, limit: 100, expected: [][]interface{}{ []interface{}{int64(1)}, }, }, "GeneratedValues_Full": { tbl: "GeneratedValues", cols: []string{"Id", "Value", "N", "N2", "N3", "X", "X2"}, ks: &KeySet{All: true}, limit: 100, expected: [][]interface{}{ []interface{}{int64(1), "xxx", int64(1), int64(2), int64(3), "xxx", "xxxY"}, }, }, "DefaultValues_Full": { tbl: "DefaultValues", cols: []string{"Id", "Value", "N", "X", "Y"}, ks: &KeySet{All: true}, limit: 100, expected: [][]interface{}{ []interface{}{int64(1), "xxx", int64(1), "x", []*int64{}}, }, }, } for name, tt := range table { t.Run(name, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() ses := newSession(db, "foo") testRunInTransaction(t, ses, func(tx *transaction) { it, err := db.Read(ctx, tx, tt.tbl, tt.idx, tt.cols, tt.ks, tt.limit) if err != nil { t.Fatalf("Read failed: %v", err) } var rows [][]interface{} err = it.Do(func(row []interface{}) error { rows = append(rows, row) return nil }) if err != nil { t.Fatalf("unexpected error in iteration: %v", err) } if diff := cmp.Diff(tt.expected, rows); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) }) } } func TestReadError(t *testing.T) { ctx := context.Background() db := newDatabase() for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } table := map[string]struct { tbl string idx string cols []string ks *KeySet limit int64 code codes.Code msg *regexp.Regexp details []interface{} }{ "EmptyColumns": { tbl: "Simple", idx: "", cols: []string{}, ks: &KeySet{All: true}, limit: 100, code: codes.InvalidArgument, msg: regexp.MustCompile(`Invalid StreamingRead request`), details: []interface{}{}, }, "EmptyTableName": { tbl: "", idx: "", cols: []string{"Id"}, ks: &KeySet{All: true}, limit: 100, code: codes.InvalidArgument, msg: regexp.MustCompile(`Invalid StreamingRead request`), details: []interface{}{}, }, "EmptyKeySet": { tbl: "Simple", idx: "", cols: []string{"Id"}, ks: nil, limit: 100, code: codes.InvalidArgument, msg: regexp.MustCompile(`Invalid StreamingRead request`), details: []interface{}{}, }, "TableNotFound": { tbl: "NotExistTable", idx: "", cols: []string{"Id"}, ks: &KeySet{All: true}, limit: 100, code: codes.NotFound, msg: regexp.MustCompile(`Table not found`), details: []interface{}{ &errdetails.ResourceInfo{ ResourceType: "spanner.googleapis.com/Table", ResourceName: "NotExistTable", Description: "Table not found", }, }, }, "IndexNotFound": { tbl: "Simple", idx: "NotExistIndex", cols: []string{"Id", "Value"}, ks: &KeySet{All: true}, limit: 100, code: codes.NotFound, msg: regexp.MustCompile(`Index not found on table`), details: []interface{}{ &errdetails.ResourceInfo{ ResourceType: "spanner.googleapis.com/Index", ResourceName: "NotExistIndex", Description: "Index not found on table Simple", }, }, }, "ColumnNotFound": { tbl: "Simple", idx: "", cols: []string{"Id", "Xyz"}, ks: &KeySet{All: true}, limit: 100, code: codes.NotFound, msg: regexp.MustCompile(`Column not found`), details: []interface{}{ &errdetails.ResourceInfo{ ResourceType: "spanner.googleapis.com/Column", ResourceName: "Xyz", }, }, }, "ColumnNotFoundOnSecondaryIndex": { tbl: "CompositePrimaryKeys", idx: "CompositePrimaryKeysByXY", cols: []string{"Id", "PKey1", "PKey2", "X", "Y", "Z"}, ks: &KeySet{All: true}, limit: 100, code: codes.Unimplemented, // real spanner returns Unimplemented msg: regexp.MustCompile(`Reading non-indexed columns using an index is not supported. Consider adding Id to the index using a STORING clause`), details: []interface{}{}, }, } for name, tt := range table { t.Run(name, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() ses := newSession(db, "foo") testRunInTransaction(t, ses, func(tx *transaction) { _, err := db.Read(ctx, tx, tt.tbl, tt.idx, tt.cols, tt.ks, tt.limit) st := status.Convert(err) if st.Code() != tt.code { t.Errorf("expect code to be %v but got %v", tt.code, st.Code()) } if !tt.msg.MatchString(st.Message()) { t.Errorf("unexpected error message: %v", st.Message()) } if diff := cmp.Diff(tt.details, st.Details(), protocmp.Transform()); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) }) } } func TestRead_FullType_Range(t *testing.T) { ctx := context.Background() db := newDatabase() schema := []string{ `CREATE TABLE FTString ( Id STRING(MAX) NOT NULL ) PRIMARY KEY (Id)`, `CREATE TABLE FTTimestamp ( Id TIMESTAMP NOT NULL ) PRIMARY KEY (Id)`, } for _, s := range schema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } for _, query := range []string{ `INSERT INTO FTString VALUES("aaa")`, `INSERT INTO FTString VALUES("aaaa")`, `INSERT INTO FTString VALUES("aaab")`, `INSERT INTO FTString VALUES("11")`, `INSERT INTO FTString VALUES("100")`, `INSERT INTO FTTimestamp VALUES("2012-03-04T12:34:56.123456789Z")`, `INSERT INTO FTTimestamp VALUES("2012-03-04T00:00:00.123456789Z")`, `INSERT INTO FTTimestamp VALUES("2012-03-04T00:00:00.999999999Z")`, `INSERT INTO FTTimestamp VALUES("2012-03-05T00:00:00.000000000Z")`, } { if _, err := db.db.ExecContext(ctx, query); err != nil { t.Fatalf("Insert failed: %v", err) } } table := map[string]struct { tbl string cols []string start *structpb.ListValue end *structpb.ListValue startClosed bool endClosed bool expected [][]interface{} }{ "FTString_1": { tbl: "FTString", cols: []string{"Id"}, start: makeListValue(makeStringValue("aaa")), end: makeListValue(makeStringValue("aaz")), startClosed: true, endClosed: true, expected: [][]interface{}{ []interface{}{"aaa"}, []interface{}{"aaaa"}, []interface{}{"aaab"}, }, }, "FTString_2": { tbl: "FTString", cols: []string{"Id"}, start: makeListValue(makeStringValue("10")), end: makeListValue(makeStringValue("11")), startClosed: true, endClosed: true, expected: [][]interface{}{ []interface{}{"100"}, []interface{}{"11"}, }, }, "FTTimestamp_1": { tbl: "FTTimestamp", cols: []string{"Id"}, start: makeListValue(makeStringValue("2012-03-04T00:00:00.000000000Z")), end: makeListValue(makeStringValue("2012-03-05T00:00:00.000000000Z")), startClosed: true, endClosed: false, expected: [][]interface{}{ []interface{}{"2012-03-04T00:00:00.123456789Z"}, []interface{}{"2012-03-04T00:00:00.999999999Z"}, []interface{}{"2012-03-04T12:34:56.123456789Z"}, }, }, "FTTimestamp_2": { tbl: "FTTimestamp", cols: []string{"Id"}, start: makeListValue(makeStringValue("2012-03-04T00:00:00.000000000Z")), end: makeListValue(makeStringValue("2012-03-04T00:00:01.000000000Z")), startClosed: true, endClosed: false, expected: [][]interface{}{ []interface{}{"2012-03-04T00:00:00.123456789Z"}, []interface{}{"2012-03-04T00:00:00.999999999Z"}, }, }, } for name, tt := range table { t.Run(name, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() ses := newSession(db, "foo") testRunInTransaction(t, ses, func(tx *transaction) { ks := &KeySet{ Ranges: []*KeyRange{ { start: tt.start, end: tt.end, startClosed: tt.startClosed, endClosed: tt.endClosed, }, }, } it, err := db.Read(ctx, tx, tt.tbl, "", tt.cols, ks, 100) if err != nil { t.Fatalf("Read failed: %v", err) } var rows [][]interface{} err = it.Do(func(row []interface{}) error { rows = append(rows, row) return nil }) if err != nil { t.Fatalf("unexpected error in iteration: %v", err) } if diff := cmp.Diff(tt.expected, rows); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) }) } } func TestInsertAndReplace(t *testing.T) { table := map[string]struct { tbl string wcols []string values []*structpb.Value cols []string limit int64 expected [][]interface{} }{ "Simple": { tbl: "Simple", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("xxx"), }, cols: []string{"Id", "Value"}, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, "SimpleMaxInt": { tbl: "Simple", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue(strconv.FormatInt(0x7FFFFFFFFFFFFFFF, 10)), makeStringValue("xxx"), }, cols: []string{"Id"}, limit: 100, expected: [][]interface{}{ []interface{}{int64(0x7FFFFFFFFFFFFFFF)}, }, }, "SimpleMinInt": { tbl: "Simple", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue(strconv.FormatInt(-0x7FFFFFFFFFFFFFFF, 10)), makeStringValue("xxx"), }, cols: []string{"Id"}, limit: 100, expected: [][]interface{}{ []interface{}{int64(-0x7FFFFFFFFFFFFFFF)}, }, }, "FullType": { tbl: "FullTypes", wcols: fullTypesKeys, values: []*structpb.Value{ makeStringValue("xxx"), // PKey STRING(32) NOT NULL, makeStringValue("xxx"), // FTString STRING(32) NOT NULL, makeStringValue("xxx"), // FTStringNull STRING(32), makeBoolValue(true), // FTBool BOOL NOT NULL, makeBoolValue(true), // FTBoolNull BOOL, makeStringValue("eHl6"), // FTBytes BYTES(32) NOT NULL, makeStringValue("eHl6"), // FTBytesNull BYTES(32), makeStringValue("2012-03-04T12:34:56.123456789Z"), // FTTimestamp TIMESTAMP NOT NULL, makeStringValue("2012-03-04T12:34:56.123456789Z"), // FTTimestampNull TIMESTAMP, makeStringValue("100"), // FTInt INT64 NOT NULL, makeStringValue("100"), // FTIntNull INT64, makeNumberValue(0.5), // FTFloat FLOAT64 NOT NULL, makeNumberValue(0.5), // FTFloatNull FLOAT64, makeStringValue("2012-03-04"), // FTDate DATE NOT NULL, makeStringValue("2012-03-04"), // FTDateNull DATE, }, cols: []string{ "PKey", "FTString", "FTStringNull", "FTBool", "FTBoolNull", "FTBytes", "FTBytesNull", "FTTimestamp", "FTTimestampNull", "FTInt", "FTIntNull", "FTFloat", "FTFloatNull", "FTDate", "FTDateNull", }, limit: 100, expected: [][]interface{}{ []interface{}{"xxx", "xxx", "xxx", true, true, []byte("xyz"), []byte("xyz"), "2012-03-04T12:34:56.123456789Z", "2012-03-04T12:34:56.123456789Z", int64(100), int64(100), float64(0.5), float64(0.5), "2012-03-04", "2012-03-04", }, }, }, "FullType_WithNull": { tbl: "FullTypes", wcols: fullTypesKeys, values: []*structpb.Value{ makeStringValue("xxx"), // PKey STRING(32) NOT NULL, makeStringValue("xxx"), // FTString STRING(32) NOT NULL, makeNullValue(), // FTStringNull STRING(32), makeBoolValue(true), // FTBool BOOL NOT NULL, makeNullValue(), // FTBoolNull BOOL, makeStringValue("eHl6"), // FTBytes BYTES(32) NOT NULL, makeNullValue(), // FTBytesNull BYTES(32), makeStringValue("2012-03-04T12:34:56.123456789Z"), // FTTimestamp TIMESTAMP NOT NULL, makeNullValue(), // FTTimestampNull TIMESTAMP, makeStringValue("100"), // FTInt INT64 NOT NULL, makeNullValue(), // FTIntNull INT64, makeNumberValue(0.5), // FTFloat FLOAT64 NOT NULL, makeNullValue(), // FTFloatNull FLOAT64, makeStringValue("2012-03-04"), // FTDate DATE NOT NULL, makeNullValue(), // FTDateNull DATE, }, cols: []string{ "PKey", "FTString", "FTStringNull", "FTBool", "FTBoolNull", "FTBytes", "FTBytesNull", "FTTimestamp", "FTTimestampNull", "FTInt", "FTIntNull", "FTFloat", "FTFloatNull", "FTDate", "FTDateNull", }, limit: 100, expected: [][]interface{}{ []interface{}{"xxx", "xxx", nil, true, nil, []byte("xyz"), nil, "2012-03-04T12:34:56.123456789Z", nil, int64(100), nil, float64(0.5), nil, "2012-03-04", nil, }, }, }, "FullType_NoValuesForNull": { tbl: "FullTypes", wcols: []string{ "PKey", "FTString", "FTBool", "FTBytes", "FTTimestamp", "FTInt", "FTFloat", "FTDate", }, values: []*structpb.Value{ makeStringValue("xxx"), // PKey STRING(32) NOT NULL, makeStringValue("xxx"), // FTString STRING(32) NOT NULL, makeBoolValue(true), // FTBool BOOL NOT NULL, makeStringValue("eHl6"), // FTBytes BYTES(32) NOT NULL, makeStringValue("2012-03-04T12:34:56.123456789Z"), // FTTimestamp TIMESTAMP NOT NULL, makeStringValue("100"), // FTInt INT64 NOT NULL, makeNumberValue(0.5), // FTFloat FLOAT64 NOT NULL, makeStringValue("2012-03-04"), // FTDate DATE NOT NULL, }, cols: fullTypesKeys, limit: 100, expected: [][]interface{}{ []interface{}{"xxx", "xxx", nil, true, nil, []byte("xyz"), nil, "2012-03-04T12:34:56.123456789Z", nil, int64(100), nil, float64(0.5), nil, "2012-03-04", nil, }, }, }, "ArrayTypes": { tbl: "ArrayTypes", wcols: arrayTypesKeys, values: []*structpb.Value{ makeStringValue("100"), makeListValueAsValue(makeListValue( makeStringValue("xxx"), makeStringValue("yyy"), )), makeListValueAsValue(makeListValue( makeBoolValue(true), makeBoolValue(false), )), makeListValueAsValue(makeListValue( makeStringValue("eHl6"), makeStringValue("enp6"), )), makeListValueAsValue(makeListValue( makeStringValue("2012-03-04T12:34:56.123456789Z"), makeStringValue("2012-03-04T12:34:56.000000000Z"), )), makeListValueAsValue(makeListValue( makeStringValue("100"), makeStringValue("101"), )), makeListValueAsValue(makeListValue( makeNumberValue(0.2), makeNumberValue(0.5), )), makeListValueAsValue(makeListValue( makeStringValue("2012-03-04"), makeStringValue("2012-03-05"), )), }, cols: arrayTypesKeys, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), makeTestArray(TCString, "xxx", "yyy"), makeTestArray(TCBool, true, false), makeTestArray(TCBytes, []byte("xyz"), []byte("zzz")), makeTestArray(TCString, "2012-03-04T12:34:56.123456789Z", "2012-03-04T12:34:56.000000000Z"), makeTestArray(TCInt64, int64(100), int64(101)), makeTestArray(TCFloat64, float64(0.2), float64(0.5)), makeTestArray(TCString, "2012-03-04", "2012-03-05"), }, }, }, "Keyword": { tbl: "From", wcols: fromTableKeys, values: []*structpb.Value{ makeStringValue("2"), makeStringValue("2"), makeStringValue("2"), }, cols: fromTableKeys, limit: 100, expected: [][]interface{}{ []interface{}{int64(2), int64(2), int64(2)}, }, }, "GeneratedColumn": { tbl: "GeneratedColumn", wcols: []string{"Id"}, values: []*structpb.Value{ makeStringValue("100"), }, cols: []string{"Id", "Value"}, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "100"}, }, }, } for _, op := range []string{"INSERT", "REPLACE"} { t.Run(op, func(t *testing.T) { for name, tt := range table { t.Run(name, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() db := newDatabase() ses := newSession(db, "foo") for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } testRunInTransaction(t, ses, func(tx *transaction) { listValues := []*structpb.ListValue{ {Values: tt.values}, } if op == "INSERT" { if err := db.Insert(ctx, tx, tt.tbl, tt.wcols, listValues); err != nil { t.Fatalf("Insert failed: %v", err) } } else if op == "REPLACE" { if err := db.Replace(ctx, tx, tt.tbl, tt.wcols, listValues); err != nil { t.Fatalf("Replace failed: %v", err) } } }) testRunInTransaction(t, ses, func(tx *transaction) { it, err := db.Read(ctx, tx, tt.tbl, "", tt.cols, &KeySet{All: true}, tt.limit) if err != nil { t.Fatalf("Read failed: %v", err) } var rows [][]interface{} err = it.Do(func(row []interface{}) error { rows = append(rows, row) return nil }) if err != nil { t.Fatalf("unexpected error in iteration: %v", err) } if diff := cmp.Diff(tt.expected, rows); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) }) } }) } } func TestInsertOrRepace_CommitTimestamp(t *testing.T) { tbl := "FullTypes" wcols := []string{ "PKey", "FTString", "FTBool", "FTBytes", "FTTimestamp", "FTTimestampNull", "FTInt", "FTFloat", "FTDate", } values := []*structpb.Value{ makeStringValue("xxx"), // PKey STRING(32) NOT NULL, makeStringValue("xxx"), // FTString STRING(32) NOT NULL, makeBoolValue(true), // FTBool BOOL NOT NULL, makeStringValue("eHl6"), // FTBytes BYTES(32) NOT NULL, makeStringValue("2012-03-04T12:34:56.123456789Z"), // FTTimestamp TIMESTAMP NOT NULL, makeStringValue("spanner.commit_timestamp()"), makeStringValue("100"), // FTInt INT64 NOT NULL, makeNumberValue(0.5), // FTFloat FLOAT64 NOT NULL, makeStringValue("2012-03-04"), // FTDate DATE NOT NULL, } cols := []string{"FTTimestamp", "FTTimestampNull"} limit := int64(100) for _, op := range []string{"INSERT", "REPLACE"} { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() db := newDatabase() ses := newSession(db, "foo") for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } testRunInTransaction(t, ses, func(tx *transaction) { listValues := []*structpb.ListValue{ {Values: values}, } if op == "INSERT" { if err := db.Insert(ctx, tx, tbl, wcols, listValues); err != nil { t.Fatalf("Insert failed: %v", err) } } else if op == "REPLACE" { if err := db.Replace(ctx, tx, tbl, wcols, listValues); err != nil { t.Fatalf("Replace failed: %v", err) } } }) testRunInTransaction(t, ses, func(tx *transaction) { it, err := db.Read(ctx, tx, tbl, "", cols, &KeySet{All: true}, limit) if err != nil { t.Fatalf("Read failed: %v", err) } var rows [][]interface{} err = it.Do(func(row []interface{}) error { rows = append(rows, row) return nil }) if err != nil { t.Fatalf("unexpected error in iteration: %v", err) } if len(rows) != 1 { t.Fatalf("unexpected numbers of rows: %v", len(rows)) } row := rows[0] var a, b string a = row[0].(string) b = row[1].(string) if got := "2012-03-04T12:34:56.123456789Z"; a != got { t.Errorf("expect %v, but got %v", got, a) } timestamp, err := time.Parse(time.RFC3339Nano, b) if err != nil { t.Fatalf("unexpected format timestamp: %v", err) } d := time.Since(timestamp) if d >= 100*time.Millisecond { t.Fatalf("unexpected time: %v", d) } }) } } func TestReplace(t *testing.T) { table := map[string]struct { tbl string wcols []string values []*structpb.Value cols []string limit int64 expected [][]interface{} }{ "Simple_NothingChanged": { tbl: "Simple", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("yyy"), }, cols: []string{"Id", "Value"}, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "yyy"}, }, }, "Simple_ConflictUpdate": { tbl: "Simple", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("zzz"), }, cols: []string{"Id", "Value"}, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "zzz"}, }, }, "Simple_Insert": { tbl: "Simple", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("101"), makeStringValue("xxx"), }, cols: []string{"Id", "Value"}, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(101), "xxx"}, }, }, "Composite_NothingChanged": { tbl: "CompositePrimaryKeys", wcols: compositePrimaryKeysKeys, values: []*structpb.Value{ makeStringValue("1"), // Id INT64 NOT NULL, makeStringValue("pkey1xxx"), // PKey1 STRING(32) NOT NULL, makeStringValue("100"), // PKey2 INT64 NOT NULL, makeStringValue("2"), // Error INT64 NOT NULL, makeStringValue("x1"), // X STRING(32) NOT NULL, makeStringValue("y1"), // Y STRING(32) NOT NULL, makeStringValue("z1"), // Z STRING(32) NOT NULL, }, cols: compositePrimaryKeysKeys, limit: 100, expected: [][]interface{}{ []interface{}{ int64(1), "pkey1xxx", int64(100), int64(2), "x1", "y1", "z1", }, }, }, "Composite_NothingChanged_RandomOrder": { tbl: "CompositePrimaryKeys", wcols: []string{"PKey2", "X", "PKey1", "Id", "Error", "Y", "Z"}, values: []*structpb.Value{ makeStringValue("100"), // PKey2 INT64 NOT NULL, makeStringValue("x1"), // X STRING(32) NOT NULL, makeStringValue("pkey1xxx"), // PKey1 STRING(32) NOT NULL, makeStringValue("1"), // Id INT64 NOT NULL, makeStringValue("2"), // Error INT64 NOT NULL, makeStringValue("y1"), // Y STRING(32) NOT NULL, makeStringValue("z1"), // Z STRING(32) NOT NULL, }, cols: compositePrimaryKeysKeys, limit: 100, expected: [][]interface{}{ []interface{}{ int64(1), "pkey1xxx", int64(100), int64(2), "x1", "y1", "z1", }, }, }, "Composite_ConflictUpdate_RandomOrder": { tbl: "CompositePrimaryKeys", wcols: []string{"PKey2", "X", "PKey1", "Id", "Error", "Y", "Z"}, values: []*structpb.Value{ makeStringValue("100"), // PKey2 INT64 NOT NULL, makeStringValue("x4"), // X STRING(32) NOT NULL, makeStringValue("pkey1xxx"), // PKey1 STRING(32) NOT NULL, makeStringValue("1"), // Id INT64 NOT NULL, makeStringValue("10000"), // Error INT64 NOT NULL, makeStringValue("y4"), // Y STRING(32) NOT NULL, makeStringValue("z4"), // Z STRING(32) NOT NULL, }, cols: compositePrimaryKeysKeys, limit: 100, expected: [][]interface{}{ []interface{}{ int64(1), "pkey1xxx", int64(100), int64(10000), "x4", "y4", "z4", }, }, }, "Keyword": { tbl: "From", wcols: fromTableKeys, values: []*structpb.Value{ makeStringValue("1"), makeStringValue("2"), makeStringValue("3"), }, cols: fromTableKeys, limit: 100, expected: [][]interface{}{ []interface{}{int64(1), int64(2), int64(3)}, }, }, } for name, tt := range table { t.Run(name, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() db := newDatabase() ses := newSession(db, "foo") for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } testRunInTransaction(t, ses, func(tx *transaction) { createInitialData(t, ctx, db, tx) }) testRunInTransaction(t, ses, func(tx *transaction) { listValues := []*structpb.ListValue{ {Values: tt.values}, } if err := db.Replace(ctx, tx, tt.tbl, tt.wcols, listValues); err != nil { t.Fatalf("Replace failed: %v", err) } }) testRunInTransaction(t, ses, func(tx *transaction) { it, err := db.Read(ctx, tx, tt.tbl, "", tt.cols, &KeySet{All: true}, tt.limit) if err != nil { t.Fatalf("Read failed: %v", err) } var rows [][]interface{} err = it.Do(func(row []interface{}) error { rows = append(rows, row) return nil }) if err != nil { t.Fatalf("unexpected error in iteration: %v", err) } if diff := cmp.Diff(tt.expected, rows); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) }) } } func TestUpdate(t *testing.T) { table := map[string]struct { tbl string wcols []string values []*structpb.Value cols []string limit int64 expected [][]interface{} }{ "Simple": { tbl: "Simple", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("yyy"), }, cols: []string{"Id", "Value"}, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "yyy"}, }, }, "Simple_UpdateWithSameValues": { tbl: "Simple", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("xxx"), }, cols: []string{"Id", "Value"}, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, "Simple_PKeyOnly": { tbl: "Simple", wcols: []string{"Id"}, values: []*structpb.Value{ makeStringValue("100"), }, cols: []string{"Id", "Value"}, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, "Composite_InOrder": { tbl: "CompositePrimaryKeys", wcols: []string{"PKey1", "PKey2", "X", "Y"}, values: []*structpb.Value{ makeStringValue("pkey1xxx"), makeStringValue("100"), makeStringValue("xxxxxxxxx"), makeStringValue("yyyyyyyyy"), }, cols: compositePrimaryKeysKeys, limit: 100, expected: [][]interface{}{ []interface{}{ int64(1), "pkey1xxx", int64(100), int64(2), "xxxxxxxxx", "yyyyyyyyy", "z1", }, }, }, "Composite_PKeyReverse": { tbl: "CompositePrimaryKeys", wcols: []string{"PKey2", "PKey1", "X", "Y"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("pkey1xxx"), makeStringValue("xxxxxxxxx"), makeStringValue("yyyyyyyyy"), }, cols: compositePrimaryKeysKeys, limit: 100, expected: [][]interface{}{ []interface{}{ int64(1), "pkey1xxx", int64(100), int64(2), "xxxxxxxxx", "yyyyyyyyy", "z1", }, }, }, "Composite_PKeyRandom": { tbl: "CompositePrimaryKeys", wcols: []string{"PKey2", "X", "PKey1", "Y"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("xxxxxxxxx"), makeStringValue("pkey1xxx"), makeStringValue("yyyyyyyyy"), }, cols: compositePrimaryKeysKeys, limit: 100, expected: [][]interface{}{ []interface{}{ int64(1), "pkey1xxx", int64(100), int64(2), "xxxxxxxxx", "yyyyyyyyy", "z1", }, }, }, "FullType_String": { tbl: "FullTypes", wcols: []string{"PKey", "FTString"}, values: []*structpb.Value{ makeStringValue("xxx"), makeStringValue("pppp"), }, cols: fullTypesKeys, limit: 100, expected: [][]interface{}{ []interface{}{ "xxx", "pppp", "xxx", true, true, []byte("xyz"), []byte("xyz"), "2012-03-04T12:34:56.123456789Z", "2012-03-04T12:34:56.123456789Z", int64(100), int64(100), float64(0.5), float64(0.5), "2012-03-04", "2012-03-04", }, []interface{}{ "yyy", "yyy", "yyy", true, true, []byte("xyz"), []byte("xyz"), "2012-03-04T12:34:56.123456789Z", "2012-03-04T12:34:56.123456789Z", int64(101), int64(101), float64(0.5), float64(0.5), "2012-03-04", "2012-03-04", }, }, }, "Keyword": { tbl: "From", wcols: fromTableKeys, values: []*structpb.Value{ makeStringValue("1"), makeStringValue("2"), makeStringValue("3"), }, cols: fromTableKeys, limit: 100, expected: [][]interface{}{ []interface{}{int64(1), int64(2), int64(3)}, }, }, } for name, tt := range table { t.Run(name, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() db := newDatabase() ses := newSession(db, "foo") for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } testRunInTransaction(t, ses, func(tx *transaction) { createInitialData(t, ctx, db, tx) }) testRunInTransaction(t, ses, func(tx *transaction) { listValues := []*structpb.ListValue{ {Values: tt.values}, } if err := db.Update(ctx, tx, tt.tbl, tt.wcols, listValues); err != nil { t.Fatalf("Update failed: %v", err) } }) testRunInTransaction(t, ses, func(tx *transaction) { it, err := db.Read(ctx, tx, tt.tbl, "", tt.cols, &KeySet{All: true}, tt.limit) if err != nil { t.Fatalf("Read failed: %v", err) } var rows [][]interface{} err = it.Do(func(row []interface{}) error { rows = append(rows, row) return nil }) if err != nil { t.Fatalf("unexpected error in iteration: %v", err) } if diff := cmp.Diff(tt.expected, rows); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) }) } } func TestInsertOrUpdate(t *testing.T) { table := map[string]struct { tbl string wcols []string values []*structpb.Value cols []string limit int64 expected [][]interface{} }{ "Simple_NothingChanged": { tbl: "Simple", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("yyy"), }, cols: []string{"Id", "Value"}, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "yyy"}, }, }, "Simple_ConflictUpdate": { tbl: "Simple", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("zzz"), }, cols: []string{"Id", "Value"}, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "zzz"}, }, }, "Simple_Insert": { tbl: "Simple", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("101"), makeStringValue("xxx"), }, cols: []string{"Id", "Value"}, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(101), "xxx"}, }, }, "Composite_NothingChanged": { tbl: "CompositePrimaryKeys", wcols: compositePrimaryKeysKeys, values: []*structpb.Value{ makeStringValue("1"), // Id INT64 NOT NULL, makeStringValue("pkey1xxx"), // PKey1 STRING(32) NOT NULL, makeStringValue("100"), // PKey2 INT64 NOT NULL, makeStringValue("2"), // Error INT64 NOT NULL, makeStringValue("x1"), // X STRING(32) NOT NULL, makeStringValue("y1"), // Y STRING(32) NOT NULL, makeStringValue("z1"), // Z STRING(32) NOT NULL, }, cols: compositePrimaryKeysKeys, limit: 100, expected: [][]interface{}{ []interface{}{ int64(1), "pkey1xxx", int64(100), int64(2), "x1", "y1", "z1", }, }, }, "Composite_NothingChanged_RandomOrder": { tbl: "CompositePrimaryKeys", wcols: []string{"PKey2", "X", "PKey1", "Id", "Error", "Y", "Z"}, values: []*structpb.Value{ makeStringValue("100"), // PKey2 INT64 NOT NULL, makeStringValue("x1"), // X STRING(32) NOT NULL, makeStringValue("pkey1xxx"), // PKey1 STRING(32) NOT NULL, makeStringValue("1"), // Id INT64 NOT NULL, makeStringValue("2"), // Error INT64 NOT NULL, makeStringValue("y1"), // Y STRING(32) NOT NULL, makeStringValue("z1"), // Z STRING(32) NOT NULL, }, cols: compositePrimaryKeysKeys, limit: 100, expected: [][]interface{}{ []interface{}{ int64(1), "pkey1xxx", int64(100), int64(2), "x1", "y1", "z1", }, }, }, "Composite_ConflictUpdate_RandomOrder": { tbl: "CompositePrimaryKeys", wcols: []string{"PKey2", "X", "PKey1", "Id", "Error", "Y", "Z"}, values: []*structpb.Value{ makeStringValue("100"), // PKey2 INT64 NOT NULL, makeStringValue("x4"), // X STRING(32) NOT NULL, makeStringValue("pkey1xxx"), // PKey1 STRING(32) NOT NULL, makeStringValue("1"), // Id INT64 NOT NULL, makeStringValue("10000"), // Error INT64 NOT NULL, makeStringValue("y4"), // Y STRING(32) NOT NULL, makeStringValue("z4"), // Z STRING(32) NOT NULL, }, cols: compositePrimaryKeysKeys, limit: 100, expected: [][]interface{}{ []interface{}{ int64(1), "pkey1xxx", int64(100), int64(10000), "x4", "y4", "z4", }, }, }, } for name, tt := range table { t.Run(name, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() db := newDatabase() ses := newSession(db, name) for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } testRunInTransaction(t, ses, func(tx *transaction) { createInitialData(t, ctx, db, tx) }) testRunInTransaction(t, ses, func(tx *transaction) { listValues := []*structpb.ListValue{ {Values: tt.values}, } if err := db.InsertOrUpdate(ctx, tx, tt.tbl, tt.wcols, listValues); err != nil { t.Fatalf("InsertOrUpdate failed: %v", err) } }) testRunInTransaction(t, ses, func(tx *transaction) { it, err := db.Read(ctx, tx, tt.tbl, "", tt.cols, &KeySet{All: true}, tt.limit) if err != nil { t.Fatalf("Read failed: %v", err) } var rows [][]interface{} err = it.Do(func(row []interface{}) error { rows = append(rows, row) return nil }) if err != nil { t.Fatalf("unexpected error in iteration: %v", err) } if diff := cmp.Diff(tt.expected, rows); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) }) } } func TestDelete(t *testing.T) { table := map[string]struct { tbl string ks *KeySet cols []string expected [][]interface{} }{ "Simple_All": { tbl: "Simple", ks: &KeySet{All: true}, cols: []string{"Id"}, expected: nil, }, "Simple_Keys_Single": { tbl: "Simple", ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue(makeStringValue("100")), }, }, cols: []string{"Id"}, expected: [][]interface{}{ []interface{}{int64(200)}, []interface{}{int64(300)}, }, }, "Simple_Keys_MultiKeys": { tbl: "Simple", ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue(makeStringValue("100")), makeListValue(makeStringValue("300")), }, }, cols: []string{"Id"}, expected: [][]interface{}{ []interface{}{int64(200)}, }, }, "Simple_Keys_NotExist": { tbl: "Simple", ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue(makeStringValue("1000")), }, }, cols: []string{"Id"}, expected: [][]interface{}{ []interface{}{int64(100)}, []interface{}{int64(200)}, []interface{}{int64(300)}, }, }, "Simple_KeyRange": { tbl: "Simple", ks: &KeySet{ Ranges: []*KeyRange{ { start: makeListValue(makeStringValue("100")), end: makeListValue(makeStringValue("200")), startClosed: true, endClosed: true, }, }, }, cols: []string{"Id"}, expected: [][]interface{}{ []interface{}{int64(300)}, }, }, "CompositePrimaryKeys_Keys": { tbl: "CompositePrimaryKeys", ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue(makeStringValue("bbb"), makeStringValue("1")), makeListValue(makeStringValue("ccc"), makeStringValue("4")), }, }, cols: []string{"PKey1", "PKey2"}, expected: [][]interface{}{ []interface{}{"aaa", int64(1)}, []interface{}{"bbb", int64(3)}, []interface{}{"bbb", int64(2)}, []interface{}{"ccc", int64(3)}, }, }, "CompositePrimaryKeys_KeyRange": { tbl: "CompositePrimaryKeys", ks: &KeySet{ Ranges: []*KeyRange{ { start: makeListValue(makeStringValue("bbb"), makeStringValue("4")), end: makeListValue(makeStringValue("bbb"), makeStringValue("1")), startClosed: true, endClosed: true, }, }, }, cols: []string{"PKey1", "PKey2"}, expected: [][]interface{}{ []interface{}{"aaa", int64(1)}, []interface{}{"ccc", int64(4)}, []interface{}{"ccc", int64(3)}, }, }, // Escape keywords "Keyword_All": { tbl: "From", ks: &KeySet{All: true}, cols: []string{"ALL"}, expected: nil, }, "Keyword_Keys": { tbl: "From", ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue(makeStringValue("1")), }, }, cols: []string{"ALL"}, expected: nil, }, "Keyword_Ranges": { tbl: "From", ks: &KeySet{ Ranges: []*KeyRange{ { start: makeListValue(makeStringValue("1")), end: makeListValue(makeStringValue("1")), startClosed: true, endClosed: true, }, }, }, cols: []string{"ALL"}, expected: nil, }, } for name, tt := range table { t.Run(name, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() db := newDatabase() ses := newSession(db, name) for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { if err := db.ApplyDDL(ctx, ddl); err != nil { t.Fatal(err) } } } for _, query := range []string{ `INSERT INTO Simple VALUES(100, "xxx")`, `INSERT INTO Simple VALUES(200, "yyy")`, `INSERT INTO Simple VALUES(300, "zzz")`, `INSERT INTO CompositePrimaryKeys VALUES(1, "aaa", 1, 0, "x1", "y1", "z")`, `INSERT INTO CompositePrimaryKeys VALUES(2, "bbb", 2, 0, "x1", "y2", "z")`, `INSERT INTO CompositePrimaryKeys VALUES(3, "bbb", 3, 0, "x1", "y3", "z")`, `INSERT INTO CompositePrimaryKeys VALUES(4, "ccc", 3, 0, "x2", "y4", "z")`, `INSERT INTO CompositePrimaryKeys VALUES(5, "ccc", 4, 0, "x2", "y5", "z")`, "INSERT INTO `From` VALUES(1, 1, 1)", } { if _, err := db.db.ExecContext(ctx, query); err != nil { t.Fatalf("Insert failed: %v", err) } } testRunInTransaction(t, ses, func(tx *transaction) { if err := db.Delete(ctx, tx, tt.tbl, tt.ks); err != nil { t.Fatalf("Delete failed: %v", err) } }) testRunInTransaction(t, ses, func(tx *transaction) { it, err := db.Read(ctx, tx, tt.tbl, "", tt.cols, &KeySet{All: true}, 100) if err != nil { t.Fatalf("Read failed: %v", err) } var rows [][]interface{} err = it.Do(func(row []interface{}) error { rows = append(rows, row) return nil }) if err != nil { t.Fatalf("unexpected error in iteration: %v", err) } if diff := cmp.Diff(tt.expected, rows); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) }) } } func TestMutationError(t *testing.T) { table := map[string]struct { op []string tbl string wcols []string values []*structpb.Value code codes.Code msg *regexp.Regexp details []interface{} }{ "TableNotfound": { op: []string{"UPDATE", "INSERT", "UPSERT", "REPLACE"}, tbl: "XXX", wcols: []string{}, values: []*structpb.Value{}, code: codes.NotFound, msg: regexp.MustCompile(`Table not found`), details: []interface{}{ &errdetails.ResourceInfo{ ResourceType: "spanner.googleapis.com/Table", ResourceName: "XXX", Description: "Table not found", }, }, }, "ColumnNotFound": { op: []string{"UPDATE", "INSERT", "UPSERT", "REPLACE"}, tbl: "Simple", wcols: []string{"Id", "Value", "XXX"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("yyy"), makeStringValue("xxx"), }, code: codes.NotFound, msg: regexp.MustCompile(`Column not found in table Simple: XXX`), details: []interface{}{ &errdetails.ResourceInfo{ ResourceType: "spanner.googleapis.com/Column", ResourceName: "XXX", }, }, }, "MultipleValues": { op: []string{"UPDATE", "INSERT", "UPSERT", "REPLACE"}, tbl: "Simple", wcols: []string{"Id", "Value", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("yyy"), makeStringValue("yyy"), }, code: codes.InvalidArgument, msg: regexp.MustCompile(`Multiple values for column Value`), details: []interface{}{}, }, "NoPrimaryKeys": { op: []string{"UPDATE", "INSERT", "UPSERT", "REPLACE"}, tbl: "CompositePrimaryKeys", wcols: []string{"PKey1", "Id", "Error", "X", "Y", "Z"}, values: []*structpb.Value{ makeStringValue("yyy"), makeStringValue("100"), makeStringValue("1"), makeStringValue("x"), makeStringValue("y"), makeStringValue("z"), }, code: codes.FailedPrecondition, msg: regexp.MustCompile(`PKey2 must not be NULL in table CompositePrimaryKeys`), details: []interface{}{}, }, "ValuesMisMatch": { op: []string{"UPDATE", "INSERT", "UPSERT", "REPLACE"}, tbl: "Simple", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("yyy"), makeStringValue("yyy"), }, code: codes.InvalidArgument, msg: regexp.MustCompile(`Mutation has mismatched number of columns and values.`), details: []interface{}{}, }, "RowNotFound": { op: []string{"UPDATE"}, tbl: "Simple", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("100000"), makeStringValue("yyy"), }, code: codes.NotFound, msg: regexp.MustCompile(`Row \[.*\] in table Simple is missing. Row cannot be updated.`), details: []interface{}{}, // no details }, "ValuesNotSpecifiedForPKey": { op: []string{"INSERT", "REPLACE"}, tbl: "Simple", wcols: []string{"Value"}, values: []*structpb.Value{ makeStringValue("yyy"), }, code: codes.FailedPrecondition, msg: regexp.MustCompile(`Id must not be NULL in table Simple.`), details: []interface{}{}, }, "NullSpecifiedForPKey": { op: []string{"INSERT", "REPLACE"}, tbl: "Simple", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeNullValue(), makeStringValue("yyy"), }, code: codes.FailedPrecondition, msg: regexp.MustCompile(`Id must not be NULL in table Simple.`), details: []interface{}{}, }, "ValuesNotSpecifiedForNonNullable": { op: []string{"INSERT", "REPLACE"}, tbl: "Simple", wcols: []string{"Id"}, values: []*structpb.Value{ makeStringValue("100"), }, code: codes.FailedPrecondition, msg: regexp.MustCompile(`A new row in table Simple does not specify a non-null value for these NOT NULL columns: Value`), details: []interface{}{}, }, "NullSpecifiedForNonNullable": { op: []string{"INSERT", "REPLACE"}, tbl: "Simple", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeNullValue(), }, code: codes.FailedPrecondition, msg: regexp.MustCompile(`Value must not be NULL in table Simple.`), details: []interface{}{}, }, "PrimaryKeyViolation": { op: []string{"INSERT"}, tbl: "Simple", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("yyyy"), }, code: codes.AlreadyExists, msg: regexp.MustCompile(`Row \[.*\] in table Simple already exists`), details: []interface{}{}, }, "FullType": { op: []string{"INSERT"}, tbl: "FullTypes", wcols: fullTypesKeys, values: []*structpb.Value{ makeStringValue("zzz"), // PKey STRING(32) NOT NULL, makeStringValue("xxx"), // FTString STRING(32) NOT NULL, makeStringValue("xxx"), // FTStringNull STRING(32), makeBoolValue(true), // FTBool BOOL NOT NULL, makeBoolValue(true), // FTBoolNull BOOL, makeStringValue("eHl6"), // FTBytes BYTES(32) NOT NULL, makeStringValue("eHl6"), // FTBytesNull BYTES(32), makeStringValue("2012-03-04T12:34:56.123456789Z"), // FTTimestamp TIMESTAMP NOT NULL, makeStringValue("2012-03-04T12:34:56.123456789Z"), // FTTimestampNull TIMESTAMP, makeStringValue("100"), // FTInt INT64 NOT NULL, makeStringValue("100"), // FTIntNull INT64, makeNumberValue(0.5), // FTFloat FLOAT64 NOT NULL, makeNumberValue(0.5), // FTFloatNull FLOAT64, makeStringValue("2012-03-04"), // FTDate DATE NOT NULL, makeStringValue("2012-03-04"), // FTDateNull DATE, }, code: codes.AlreadyExists, msg: regexp.MustCompile(`Unique index violation at index key \[.*\]. It conflicts with row \[.*\] in table FullTypes`), details: []interface{}{}, }, // A new record coflicts only secondary index but it does not replace // "FullTypeConflictsSecondaryIndex": { // op: []string{"REPLACE", "UPSERT"}, // tbl: "FullTypes", // wcols: fullTypesKeys, // values: []*structpb.Value{ // makeStringValue("zzzzzzzz"), // PKey STRING(32) NOT NULL, // makeStringValue("xxx"), // FTString STRING(32) NOT NULL, // makeStringValue("xxx"), // FTStringNull STRING(32), // makeBoolValue(true), // FTBool BOOL NOT NULL, // makeBoolValue(true), // FTBoolNull BOOL, // makeStringValue("xyz"), // FTBytes BYTES(32) NOT NULL, // makeStringValue("xyz"), // FTBytesNull BYTES(32), // makeStringValue("2012-03-04T12:34:56.123456789Z"), // FTTimestamp TIMESTAMP NOT NULL, // makeStringValue("2012-03-04T12:34:56.123456789Z"), // FTTimestampNull TIMESTAMP, // makeStringValue("300"), // FTInt INT64 NOT NULL, // makeStringValue("300"), // FTIntNull INT64, // makeNumberValue(0.5), // FTFloat FLOAT64 NOT NULL, // makeNumberValue(0.5), // FTFloatNull FLOAT64, // makeStringValue("2012-03-04"), // FTDate DATE NOT NULL, // makeStringValue("2012-03-04"), // FTDateNull DATE, // }, // code: codes.AlreadyExists, // msg: regexp.MustCompile(`Unique index violation at index key \[.*\]. It conflicts with row \[.*\] in table FullTypes`), // }, "FullType_CommitTimestamp": { op: []string{"INSERT"}, tbl: "FullTypes", wcols: fullTypesKeys, values: []*structpb.Value{ makeStringValue("zzz"), // PKey STRING(32) NOT NULL, makeStringValue("zzz"), // FTString STRING(32) NOT NULL, makeStringValue("zzz"), // FTStringNull STRING(32), makeBoolValue(true), // FTBool BOOL NOT NULL, makeBoolValue(true), // FTBoolNull BOOL, makeStringValue("eHl6"), // FTBytes BYTES(32) NOT NULL, makeStringValue("eHl6"), // FTBytesNull BYTES(32), makeStringValue("spanner.commit_timestamp()"), // FTTimestamp TIMESTAMP NOT NULL, makeStringValue("2012-03-04T12:34:56.123456789Z"), // FTTimestampNull TIMESTAMP, makeStringValue("999"), // FTInt INT64 NOT NULL, makeStringValue("100"), // FTIntNull INT64, makeNumberValue(0.5), // FTFloat FLOAT64 NOT NULL, makeNumberValue(0.5), // FTFloatNull FLOAT64, makeStringValue("2012-03-04"), // FTDate DATE NOT NULL, makeStringValue("2012-03-04"), // FTDateNull DATE, }, code: codes.InvalidArgument, // TODO: FailedPrecondition msg: regexp.MustCompile(`Cannot write commit timestamp because the allow_commit_timestamp column option is not set to true for column`), details: []interface{}{}, }, "FullType_ExceedsMaximumSize": { op: []string{"INSERT"}, tbl: "FullTypes", wcols: fullTypesKeys, values: []*structpb.Value{ makeStringValue(strings.Repeat("z", 1000)), // PKey STRING(32) NOT NULL, makeStringValue("zzz"), // FTString STRING(32) NOT NULL, makeStringValue("zzz"), // FTStringNull STRING(32), makeBoolValue(true), // FTBool BOOL NOT NULL, makeBoolValue(true), // FTBoolNull BOOL, makeStringValue("eHl6"), // FTBytes BYTES(32) NOT NULL, makeStringValue("eHl6"), // FTBytesNull BYTES(32), makeStringValue("2012-03-04T12:34:56.123456789Z"), // FTTimestamp TIMESTAMP NOT NULL, makeStringValue("2012-03-04T12:34:56.123456789Z"), // FTTimestampNull TIMESTAMP, makeStringValue("999"), // FTInt INT64 NOT NULL, makeStringValue("100"), // FTIntNull INT64, makeNumberValue(0.5), // FTFloat FLOAT64 NOT NULL, makeNumberValue(0.5), // FTFloatNull FLOAT64, makeStringValue("2012-03-04"), // FTDate DATE NOT NULL, makeStringValue("2012-03-04"), // FTDateNull DATE, }, code: codes.FailedPrecondition, // msg: regexp.MustCompile(`New value exceeds the maximum size limit for this column in this database: FullTypes.PKey, size: 1000, limit: 32.`), msg: regexp.MustCompile("CHECK constraint failed: LENGTH\\(`PKey`\\) <= 32"), details: []interface{}{}, }, } for name, tt := range table { t.Run(name, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() db := newDatabase() ses := newSession(db, "foo") for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } testRunInTransaction(t, ses, func(tx *transaction) { createInitialData(t, ctx, db, tx) }) for _, op := range tt.op { t.Run(op, func(t *testing.T) { listValues := []*structpb.ListValue{ {Values: tt.values}, } testRunInTransaction(t, ses, func(tx *transaction) { var err error switch op { case "INSERT": err = db.Insert(ctx, tx, tt.tbl, tt.wcols, listValues) case "UPDATE": err = db.Update(ctx, tx, tt.tbl, tt.wcols, listValues) case "UPSERT": err = db.InsertOrUpdate(ctx, tx, tt.tbl, tt.wcols, listValues) case "REPLACE": err = db.Replace(ctx, tx, tt.tbl, tt.wcols, listValues) default: t.Fatalf("unexpected op: %v", op) } st := status.Convert(err) if st.Code() != tt.code { t.Errorf("expect code to be %v but got %v", tt.code, st.Code()) } if !tt.msg.MatchString(st.Message()) { t.Errorf("unexpected error message: %v", st.Message()) } if diff := cmp.Diff(tt.details, st.Details(), protocmp.Transform()); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) }) } }) } } func TestExecute(t *testing.T) { table := map[string]struct { sql string params map[string]Value code codes.Code msg *regexp.Regexp expectedCount int64 table string cols []string expected [][]interface{} }{ "Simple_Update": { sql: `UPDATE Simple SET Value = "zzz" WHERE Id = 100`, expectedCount: 1, table: "Simple", cols: []string{"Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "zzz"}, }, }, // TODO // "Simple_Update_Alias": { // sql: `UPDATE Simple AS s SET s.Value = "zzz" WHERE s.Id = 100`, // expectedCount: 1, // table: "Simple", // cols: []string{"Id", "Value"}, // expected: [][]interface{}{ // []interface{}{int64(100), "zzz"}, // }, // }, "Simple_Update_ParamInWhere": { sql: `UPDATE Simple SET Value = "zzz" WHERE Id = @id`, params: map[string]Value{ "id": makeTestValue(100), }, expectedCount: 1, table: "Simple", cols: []string{"Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "zzz"}, }, }, "Simple_Update_ParamInValue": { sql: `UPDATE Simple SET Value = @value WHERE Id = 100`, params: map[string]Value{ "value": makeTestValue("zzz"), }, expectedCount: 1, table: "Simple", cols: []string{"Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "zzz"}, }, }, "Simple_Update_MultipleItems": { sql: `UPDATE Simple SET Value = "zzz", Value = "zzz" WHERE Id = 100`, code: codes.InvalidArgument, msg: regexp.MustCompile(`Update item Value assigned more than once`), }, "Simple_Update_MultipleItems2": { sql: `UPDATE Simple AS s SET s.Value = "zzz", Value = "zzz" WHERE Id = 100`, code: codes.InvalidArgument, msg: regexp.MustCompile(`Update item Value assigned more than once`), }, "Simple_Update_MultipleItems3": { sql: `UPDATE Simple AS s SET Value = "zzz", s.Value = "zzz" WHERE Id = 100`, code: codes.InvalidArgument, msg: regexp.MustCompile(`Update item s.Value assigned more than once`), }, "Simple_Update_MultipleItems4": { sql: `UPDATE Simple AS s SET s.Value = "zzz", s.Value = "zzz" WHERE Id = 100`, code: codes.InvalidArgument, msg: regexp.MustCompile(`Update item s.Value assigned more than once`), }, "Simple_Update_MultipleItems5": { sql: `UPDATE Simple SET Simple.Value = "zzz", Value = "zzz" WHERE Id = 100`, code: codes.InvalidArgument, msg: regexp.MustCompile(`Update item Value assigned more than once`), }, "Simple_Update_ColumnNotFound": { sql: `UPDATE Simple SET Valu = "zzz" WHERE Id = 100`, code: codes.InvalidArgument, msg: regexp.MustCompile(`Unrecognized name: Valu`), }, "Simple_Update_PathNotFound": { sql: `UPDATE Simple AS s SET s.Valu = "zzz" WHERE Id = 100`, code: codes.InvalidArgument, msg: regexp.MustCompile(`Name Valu not found inside s`), }, "EscapeKeyword_Update": { sql: "UPDATE `From` SET `CAST` = 2 WHERE `ALL` = 1", expectedCount: 1, table: "From", cols: fromTableKeys, expected: [][]interface{}{ []interface{}{int64(1), int64(2), int64(1)}, }, }, // TODO // "EscapeKeyword_Update_Alias": { // sql: "UPDATE `From` AS `AND` SET `AND`.`CAST` = 2 WHERE `ALL` = 1", // expectedCount: 1, // table: "From", // cols: fromTableKeys, // expected: [][]interface{}{ // []interface{}{int64(1), int64(2), int64(1)}, // }, // }, "Simple_Insert": { sql: `INSERT INTO Simple (Id, Value) VALUES(101, "yyy")`, expectedCount: 1, table: "Simple", cols: []string{"Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(101), "yyy"}, }, }, "Simple_Insert_Param": { sql: `INSERT INTO Simple (Id, Value) VALUES(@a, @b)`, params: map[string]Value{ "a": makeTestValue("1000"), "b": makeTestValue("bbbb"), }, expectedCount: 1, table: "Simple", cols: []string{"Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(1000), "bbbb"}, }, }, "Simple_InsertMulti": { sql: `INSERT INTO Simple (Id, Value) VALUES(101, "yyy"), (102, "zzz")`, expectedCount: 2, table: "Simple", cols: []string{"Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(101), "yyy"}, []interface{}{int64(102), "zzz"}, }, }, "Simple_Insert_Query": { sql: `INSERT INTO Simple (Id, Value) SELECT FTInt+1, PKey FROM FullTypes`, expectedCount: 2, table: "Simple", cols: []string{"Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(101), "xxx"}, []interface{}{int64(102), "yyy"}, }, }, "Simple_Insert_Subquery": { sql: `INSERT INTO Simple (Id, Value) VALUES(200, (SELECT PKey FROM FullTypes WHERE PKey = "xxx"))`, expectedCount: 1, table: "Simple", cols: []string{"Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, []interface{}{int64(200), "xxx"}, }, }, // "Simple_Insert_Unnest": { // sql: `INSERT INTO Simple (Id, Value) SELECT * FROM UNNEST ([(200, "y"), (300, "z")])`, // expectedCount: 2, // table: "Simple", // cols: []string{"Id", "Value"}, // expected: [][]interface{}{ // []interface{}{int64(100), "xxx"}, // []interface{}{int64(200), "y"}, // []interface{}{int64(300), "z"}, // }, // }, "Simple_Insert_NonNullValue": { sql: `INSERT INTO Simple (Id) VALUES(101)`, code: codes.FailedPrecondition, msg: regexp.MustCompile(`A new row in table Simple does not specify a non-null value for these NOT NULL columns: Value`), }, "Simple_Insert_MultipleKeys": { sql: `INSERT INTO Simple (Id, Id, Value) VALUES(101, 102, "x")`, code: codes.InvalidArgument, msg: regexp.MustCompile(`INSERT has columns with duplicate name: Id`), }, "Simple_Insert_WrongColumnCount": { sql: `INSERT INTO Simple (Id, Value) VALUES(101, 102, "x")`, code: codes.InvalidArgument, msg: regexp.MustCompile(`Inserted row has wrong column count; Has 3, expected 2`), }, "Simple_Insert_ColumnNotFound": { sql: `INSERT INTO Simple (Id, Valueee) VALUES(101, "x")`, code: codes.InvalidArgument, msg: regexp.MustCompile(`Column Valueee is not present in table Simple`), }, "EscapeKeyword_Insert": { sql: "INSERT INTO `From` (`ALL`, `CAST`, `JOIN`) VALUES(2, 2, 2)", expectedCount: 1, table: "From", cols: fromTableKeys, expected: [][]interface{}{ []interface{}{int64(1), int64(1), int64(1)}, []interface{}{int64(2), int64(2), int64(2)}, }, }, "Simple_Delete": { sql: `DELETE FROM Simple WHERE Id = 100`, expectedCount: 1, table: "Simple", cols: []string{"Id", "Value"}, expected: nil, }, "Simple_Delete_SubQuery": { sql: `DELETE FROM Simple WHERE Id IN (SELECT Id FROM Simple)`, expectedCount: 1, table: "Simple", cols: []string{"Id", "Value"}, expected: nil, }, "Simple_Delete_Param": { sql: `DELETE FROM Simple WHERE Id = @id`, params: map[string]Value{ "id": makeTestValue(100), }, expectedCount: 1, table: "Simple", cols: []string{"Id", "Value"}, expected: nil, }, "Simple_Delete_NotExist": { sql: `DELETE FROM Simple WHERE Id = 10000`, expectedCount: 0, table: "Simple", cols: []string{"Id", "Value"}, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, "EscapeKeyword_Delete": { sql: "DELETE FROM `From` WHERE `ALL` = 1", expectedCount: 1, table: "From", cols: fromTableKeys, expected: nil, }, } for name, tt := range table { t.Run(name, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() stmt, err := (&memefish.Parser{ Lexer: &memefish.Lexer{ File: &token.File{FilePath: "", Buffer: tt.sql}, }, }).ParseDML() if err != nil { t.Fatalf("failed to parse DML: %q %v", tt.sql, err) } db := newDatabase() ses := newSession(db, "foo") for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } testRunInTransaction(t, ses, func(tx *transaction) { createInitialData(t, ctx, db, tx) }) testRunInTransaction(t, ses, func(tx *transaction) { r, err := db.Execute(ctx, tx, stmt, tt.params) if tt.code == codes.OK { if err != nil { t.Fatalf("Execute failed: %v", err) } if r != tt.expectedCount { t.Fatalf("expect count to be %v, but got %v", tt.expectedCount, r) } } else { st := status.Convert(err) if st.Code() != tt.code { t.Errorf("expect code to be %v but got %v", tt.code, st.Code()) } if !tt.msg.MatchString(st.Message()) { t.Errorf("unexpected error message: \n %q\n expected:\n %q", st.Message(), tt.msg) } } }) if tt.code != codes.OK { return } // check database values testRunInTransaction(t, ses, func(tx *transaction) { it, err := db.Read(ctx, tx, tt.table, "", tt.cols, &KeySet{All: true}, 100) if err != nil { t.Fatalf("Read failed: %v", err) } var rows [][]interface{} err = it.Do(func(row []interface{}) error { rows = append(rows, row) return nil }) if err != nil { t.Fatalf("unexpected error in iteration: %v", err) } if diff := cmp.Diff(tt.expected, rows); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) }) } } func TestInterleaveInsert(t *testing.T) { type tableConfig struct { tbl string wcols []string values []*structpb.Value cols []string limit int64 expected [][]interface{} } table := map[string]struct { child *tableConfig parent *tableConfig expectsError bool }{ "InsertWithoutParent": { child: &tableConfig{ tbl: "Interleaved", wcols: []string{"InterleavedId", "Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("100"), makeStringValue("xxx"), }, cols: []string{"InterleavedId", "Id", "Value"}, limit: 100, expected: nil, }, expectsError: true, }, "InsertWithParent": { parent: &tableConfig{ tbl: "ParentTable", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("xxx"), }, cols: []string{"Id", "Value"}, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, child: &tableConfig{ tbl: "Interleaved", wcols: []string{"InterleavedId", "Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("100"), makeStringValue("xxx"), }, cols: []string{"InterleavedId", "Id", "Value"}, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), int64(100), "xxx"}, }, }, expectsError: false, }, "InsertWithoutParent(Cascade)": { child: &tableConfig{ tbl: "Interleaved", wcols: []string{"InterleavedId", "Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("100"), makeStringValue("xxx"), }, cols: []string{"InterleavedId", "Id", "Value"}, limit: 100, expected: nil, }, expectsError: true, }, "InsertWithParent(Cascade)": { parent: &tableConfig{ tbl: "ParentTable", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("xxx"), }, cols: []string{"Id", "Value"}, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, child: &tableConfig{ tbl: "Interleaved", wcols: []string{"InterleavedId", "Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("100"), makeStringValue("xxx"), }, cols: []string{"InterleavedId", "Id", "Value"}, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), int64(100), "xxx"}, }, }, expectsError: false, }, "InsertWithoutParent(NoAction)": { child: &tableConfig{ tbl: "Interleaved", wcols: []string{"InterleavedId", "Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("100"), makeStringValue("xxx"), }, cols: []string{"InterleavedId", "Id", "Value"}, limit: 100, expected: nil, }, expectsError: true, }, "InsertWithParent(NoAction)": { parent: &tableConfig{ tbl: "ParentTable", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("xxx"), }, cols: []string{"Id", "Value"}, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), "xxx"}, }, }, child: &tableConfig{ tbl: "Interleaved", wcols: []string{"InterleavedId", "Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("100"), makeStringValue("xxx"), }, cols: []string{"InterleavedId", "Id", "Value"}, limit: 100, expected: [][]interface{}{ []interface{}{int64(100), int64(100), "xxx"}, }, }, expectsError: false, }, } for name, tt := range table { t.Run(name, func(t *testing.T) { ctx := context.Background() db := newDatabase() ses := newSession(db, "foo") for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } if tt.parent != nil { testRunInTransaction(t, ses, func(tx *transaction) { testInsertHelper(t, ctx, db, tx, tt.parent.tbl, tt.parent.cols, tt.parent.wcols, tt.parent.values, tt.parent.limit, tt.parent.expected) }) } if tt.expectsError { listValues := []*structpb.ListValue{ {Values: tt.child.values}, } testRunInTransaction(t, ses, func(tx *transaction) { if err := db.Insert(ctx, tx, tt.child.tbl, tt.child.wcols, listValues); err == nil { t.Fatalf("Insert succeeded even though it should fail: %v", err) } }) return } testRunInTransaction(t, ses, func(tx *transaction) { testInsertHelper(t, ctx, db, tx, tt.child.tbl, tt.child.cols, tt.child.wcols, tt.child.values, tt.child.limit, tt.child.expected) }) }) } } func testInsertHelper( t *testing.T, ctx context.Context, db *database, tx *transaction, tbl string, cols []string, wcols []string, values []*structpb.Value, limit int64, expected [][]interface{}, ) { t.Helper() listValues := []*structpb.ListValue{ {Values: values}, } if err := db.Insert(ctx, tx, tbl, wcols, listValues); err != nil { t.Fatalf("Insert failed: %v", err) } it, err := db.Read(ctx, tx, tbl, "", cols, &KeySet{All: true}, limit) if err != nil { t.Fatalf("Read failed: %v", err) } var rows [][]interface{} err = it.Do(func(row []interface{}) error { rows = append(rows, row) return nil }) if err != nil { t.Fatalf("unexpected error in iteration: %v", err) } if diff := cmp.Diff(expected, rows); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } } func TestInterleaveDeleteParent(t *testing.T) { type tableConfig struct { tbl string wcols []string values []*structpb.Value cols []string ks *KeySet } table := map[string]struct { child *tableConfig parent *tableConfig expectsDeleteError bool }{ "DeleteDefault": { parent: &tableConfig{ tbl: "ParentTable", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("xxx"), }, cols: []string{"Id", "Value"}, ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue(makeStringValue("100")), }, }, }, child: &tableConfig{ tbl: "Interleaved", wcols: []string{"InterleavedId", "Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("100"), makeStringValue("xxx"), }, cols: []string{"InterleavedId", "Id", "Value"}, ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue( makeStringValue("100"), makeStringValue("100"), ), }, }, }, expectsDeleteError: true, }, "DeleteCascade": { parent: &tableConfig{ tbl: "ParentTableCascade", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("xxx"), }, cols: []string{"Id", "Value"}, ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue(makeStringValue("100")), }, }, }, child: &tableConfig{ tbl: "InterleavedCascade", wcols: []string{"InterleavedId", "Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("100"), makeStringValue("xxx"), }, cols: []string{"InterleavedId", "Id", "Value"}, ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue( makeStringValue("100"), makeStringValue("100"), ), }, }, }, expectsDeleteError: false, }, "DeleteNoAction": { parent: &tableConfig{ tbl: "ParentTableNoAction", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("xxx"), }, cols: []string{"Id", "Value"}, ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue(makeStringValue("100")), }, }, }, child: &tableConfig{ tbl: "InterleavedNoAction", wcols: []string{"InterleavedId", "Id", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("100"), makeStringValue("xxx"), }, cols: []string{"InterleavedId", "Id", "Value"}, ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue( makeStringValue("100"), makeStringValue("100"), ), }, }, }, expectsDeleteError: true, }, } for name, tt := range table { t.Run(name, func(t *testing.T) { ctx := context.Background() db := newDatabase() ses := newSession(db, "foo") for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } // Insert data parentListValues := []*structpb.ListValue{ {Values: tt.parent.values}, } testRunInTransaction(t, ses, func(tx *transaction) { if err := db.Insert(ctx, tx, tt.parent.tbl, tt.parent.wcols, parentListValues); err != nil { t.Fatalf("Insert failed: %v", err) } }) listValues := []*structpb.ListValue{ {Values: tt.child.values}, } testRunInTransaction(t, ses, func(tx *transaction) { if err := db.Insert(ctx, tx, tt.child.tbl, tt.child.wcols, listValues); err != nil { t.Fatalf("Insert failed: %v", err) } }) // Delete parent entry if tt.expectsDeleteError { testRunInTransaction(t, ses, func(tx *transaction) { if err := db.Delete(ctx, tx, tt.parent.tbl, tt.parent.ks); err == nil { t.Fatalf("Delete parent succeeded even though it should fail: %v", err) } }) return } testRunInTransaction(t, ses, func(tx *transaction) { if err := db.Delete(ctx, tx, tt.parent.tbl, tt.parent.ks); err != nil { t.Fatalf("Delete parent failed: %v", err) } }) // Try to read child entry testRunInTransaction(t, ses, func(tx *transaction) { it, err := db.Read(ctx, tx, tt.child.tbl, "", tt.child.cols, tt.child.ks, 1) if err != nil { t.Fatalf("Read failed: %v", err) } var rows [][]interface{} err = it.Do(func(row []interface{}) error { rows = append(rows, row) return nil }) if err != nil { t.Fatalf("unexpected error in iteration: %v", err) } if rows != nil { t.Fatalf("Child did not get deleted with parent") } }) }) } } func TestInformationSchema(t *testing.T) { ctx := context.Background() db := newDatabase() for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } table := []struct { name string sql string params map[string]Value expected [][]interface{} names []string code codes.Code msg *regexp.Regexp }{ { name: "Schemata1", sql: `SELECT * FROM INFORMATION_SCHEMA.SCHEMATA WHERE EFFECTIVE_TIMESTAMP IS NULL`, names: []string{"CATALOG_NAME", "SCHEMA_NAME", "EFFECTIVE_TIMESTAMP"}, expected: [][]interface{}{ []interface{}{"", "INFORMATION_SCHEMA", nil}, []interface{}{"", "SPANNER_SYS", nil}, }, }, { name: "Schemata2", sql: `SELECT CATALOG_NAME, SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE EFFECTIVE_TIMESTAMP IS NOT NULL`, names: []string{"CATALOG_NAME", "SCHEMA_NAME"}, expected: [][]interface{}{ []interface{}{"", ""}, }, }, { name: "Tables_Reserved", sql: `SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA != "" ORDER BY TABLE_SCHEMA, TABLE_NAME`, names: []string{"TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "PARENT_TABLE_NAME", "ON_DELETE_ACTION", "SPANNER_STATE"}, expected: [][]interface{}{ []interface{}{"", "INFORMATION_SCHEMA", "COLUMNS", nil, nil, nil}, []interface{}{"", "INFORMATION_SCHEMA", "COLUMN_OPTIONS", nil, nil, nil}, // []interface{}{"", "INFORMATION_SCHEMA", "CONSTRAINT_COLUMN_USAGE", nil, nil, nil}, // []interface{}{"", "INFORMATION_SCHEMA", "CONSTRAINT_TABLE_USAGE", nil, nil, nil}, []interface{}{"", "INFORMATION_SCHEMA", "INDEXES", nil, nil, nil}, []interface{}{"", "INFORMATION_SCHEMA", "INDEX_COLUMNS", nil, nil, nil}, // []interface{}{"", "INFORMATION_SCHEMA", "KEY_COLUMN_USAGE", nil, nil, nil}, []interface{}{"", "INFORMATION_SCHEMA", "REFERENTIAL_CONSTRAINTS", nil, nil, nil}, []interface{}{"", "INFORMATION_SCHEMA", "SCHEMATA", nil, nil, nil}, // []interface{}{"", "INFORMATION_SCHEMA", "TABLE_CONSTRAINTS", nil, nil, nil}, []interface{}{"", "INFORMATION_SCHEMA", "TABLES", nil, nil, nil}, // []interface{}{"", "SPANNER_SYS", "QUERY_STATS_TOP_10MINUTE", nil, nil, nil}, // []interface{}{"", "SPANNER_SYS", "QUERY_STATS_TOP_HOUR", nil, nil, nil}, // []interface{}{"", "SPANNER_SYS", "QUERY_STATS_TOP_MINUTE", nil, nil, nil}, // []interface{}{"", "SPANNER_SYS", "QUERY_STATS_TOTAL_10MINUTE", nil, nil, nil}, // []interface{}{"", "SPANNER_SYS", "QUERY_STATS_TOTAL_HOUR", nil, nil, nil}, // []interface{}{"", "SPANNER_SYS", "QUERY_STATS_TOTAL_MINUTE", nil, nil, nil}, }, }, { name: "Tables_NonReserved", sql: `SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = "" ORDER BY TABLE_NAME`, names: []string{"TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "PARENT_TABLE_NAME", "ON_DELETE_ACTION", "SPANNER_STATE"}, expected: [][]interface{}{ []interface{}{"", "", "ArrayTypes", nil, nil, "COMMITTED"}, []interface{}{"", "", "CompositePrimaryKeys", nil, nil, "COMMITTED"}, []interface{}{"", "", "DefaultValues", nil, nil, "COMMITTED"}, []interface{}{"", "", "ForeignChildCascade", nil, nil, "COMMITTED"}, []interface{}{"", "", "ForeignChildNoAction", nil, nil, "COMMITTED"}, []interface{}{"", "", "ForeignParentCascade", nil, nil, "COMMITTED"}, []interface{}{"", "", "ForeignParentNoAction", nil, nil, "COMMITTED"}, []interface{}{"", "", "From", nil, nil, "COMMITTED"}, []interface{}{"", "", "FullTypes", nil, nil, "COMMITTED"}, []interface{}{"", "", "GeneratedColumn", nil, nil, "COMMITTED"}, []interface{}{"", "", "GeneratedValues", nil, nil, "COMMITTED"}, []interface{}{"", "", "Interleaved", "ParentTable", nil, "COMMITTED"}, []interface{}{"", "", "InterleavedCascade", "ParentTableCascade", "CASCADE", "COMMITTED"}, []interface{}{"", "", "InterleavedNoAction", "ParentTableNoAction", "NO ACTION", "COMMITTED"}, []interface{}{"", "", "JoinA", nil, nil, "COMMITTED"}, []interface{}{"", "", "JoinB", nil, nil, "COMMITTED"}, []interface{}{"", "", "ParentTable", nil, nil, "COMMITTED"}, []interface{}{"", "", "ParentTableCascade", nil, nil, "COMMITTED"}, []interface{}{"", "", "ParentTableNoAction", nil, nil, "COMMITTED"}, []interface{}{"", "", "Simple", nil, nil, "COMMITTED"}, }, }, { name: "Columns_Reserved", sql: `SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA != "" ORDER BY TABLE_NAME, ORDINAL_POSITION`, names: []string{"TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "COLUMN_NAME", "ORDINAL_POSITION", "COLUMN_DEFAULT", "DATA_TYPE", "IS_NULLABLE", "SPANNER_TYPE"}, expected: [][]interface{}{ {"", "INFORMATION_SCHEMA", "COLUMNS", "TABLE_CATALOG", int64(1), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMNS", "TABLE_SCHEMA", int64(2), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMNS", "TABLE_NAME", int64(3), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMNS", "COLUMN_NAME", int64(4), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMNS", "ORDINAL_POSITION", int64(5), nil, nil, "NO", "INT64"}, {"", "INFORMATION_SCHEMA", "COLUMNS", "COLUMN_DEFAULT", int64(6), nil, nil, "YES", "BYTES(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMNS", "DATA_TYPE", int64(7), nil, nil, "YES", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMNS", "IS_NULLABLE", int64(8), nil, nil, "YES", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMNS", "SPANNER_TYPE", int64(9), nil, nil, "YES", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMN_OPTIONS", "TABLE_CATALOG", int64(1), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMN_OPTIONS", "TABLE_SCHEMA", int64(2), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMN_OPTIONS", "TABLE_NAME", int64(3), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMN_OPTIONS", "COLUMN_NAME", int64(4), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMN_OPTIONS", "OPTION_NAME", int64(5), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMN_OPTIONS", "OPTION_TYPE", int64(6), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMN_OPTIONS", "OPTION_VALUE", int64(7), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_COLUMN_USAGE", "TABLE_CATALOG", int64(1), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_COLUMN_USAGE", "TABLE_SCHEMA", int64(2), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_COLUMN_USAGE", "TABLE_NAME", int64(3), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_COLUMN_USAGE", "COLUMN_NAME", int64(4), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_COLUMN_USAGE", "CONSTRAINT_CATALOG", int64(5), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_COLUMN_USAGE", "CONSTRAINT_SCHEMA", int64(6), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_COLUMN_USAGE", "CONSTRAINT_NAME", int64(7), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_TABLE_USAGE", "TABLE_CATALOG", int64(1), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_TABLE_USAGE", "TABLE_SCHEMA", int64(2), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_TABLE_USAGE", "TABLE_NAME", int64(3), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_TABLE_USAGE", "CONSTRAINT_CATALOG", int64(4), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_TABLE_USAGE", "CONSTRAINT_SCHEMA", int64(5), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_TABLE_USAGE", "CONSTRAINT_NAME", int64(6), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEXES", "TABLE_CATALOG", int64(1), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEXES", "TABLE_SCHEMA", int64(2), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEXES", "TABLE_NAME", int64(3), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEXES", "INDEX_NAME", int64(4), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEXES", "INDEX_TYPE", int64(5), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEXES", "PARENT_TABLE_NAME", int64(6), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEXES", "IS_UNIQUE", int64(7), nil, nil, "NO", "BOOL"}, {"", "INFORMATION_SCHEMA", "INDEXES", "IS_NULL_FILTERED", int64(8), nil, nil, "NO", "BOOL"}, {"", "INFORMATION_SCHEMA", "INDEXES", "INDEX_STATE", int64(9), nil, nil, "YES", "STRING(100)"}, // TODO {"", "INFORMATION_SCHEMA", "INDEXES", "SPANNER_IS_MANAGED", int64(10), nil, nil, "NO", "BOOL"}, {"", "INFORMATION_SCHEMA", "INDEX_COLUMNS", "TABLE_CATALOG", int64(1), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEX_COLUMNS", "TABLE_SCHEMA", int64(2), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEX_COLUMNS", "TABLE_NAME", int64(3), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEX_COLUMNS", "INDEX_NAME", int64(4), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEX_COLUMNS", "INDEX_TYPE", int64(5), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEX_COLUMNS", "COLUMN_NAME", int64(6), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEX_COLUMNS", "ORDINAL_POSITION", int64(7), nil, nil, "YES", "INT64"}, {"", "INFORMATION_SCHEMA", "INDEX_COLUMNS", "COLUMN_ORDERING", int64(8), nil, nil, "YES", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEX_COLUMNS", "IS_NULLABLE", int64(9), nil, nil, "YES", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEX_COLUMNS", "SPANNER_TYPE", int64(10), nil, nil, "YES", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "KEY_COLUMN_USAGE", "CONSTRAINT_CATALOG", int64(1), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "KEY_COLUMN_USAGE", "CONSTRAINT_SCHEMA", int64(2), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "KEY_COLUMN_USAGE", "CONSTRAINT_NAME", int64(3), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "KEY_COLUMN_USAGE", "TABLE_CATALOG", int64(4), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "KEY_COLUMN_USAGE", "TABLE_SCHEMA", int64(5), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "KEY_COLUMN_USAGE", "TABLE_NAME", int64(6), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "KEY_COLUMN_USAGE", "COLUMN_NAME", int64(7), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "KEY_COLUMN_USAGE", "ORDINAL_POSITION", int64(8), nil, nil, "NO", "INT64"}, // {"", "INFORMATION_SCHEMA", "KEY_COLUMN_USAGE", "POSITION_IN_UNIQUE_CONSTRAINT", int64(9), nil, nil, "YES", "INT64"}, {"", "INFORMATION_SCHEMA", "REFERENTIAL_CONSTRAINTS", "CONSTRAINT_CATALOG", int64(1), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "REFERENTIAL_CONSTRAINTS", "CONSTRAINT_SCHEMA", int64(2), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "REFERENTIAL_CONSTRAINTS", "CONSTRAINT_NAME", int64(3), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "REFERENTIAL_CONSTRAINTS", "UNIQUE_CONSTRAINT_CATALOG", int64(4), nil, nil, "YES", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "REFERENTIAL_CONSTRAINTS", "UNIQUE_CONSTRAINT_SCHEMA", int64(5), nil, nil, "YES", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "REFERENTIAL_CONSTRAINTS", "UNIQUE_CONSTRAINT_NAME", int64(6), nil, nil, "YES", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "REFERENTIAL_CONSTRAINTS", "MATCH_OPTION", int64(7), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "REFERENTIAL_CONSTRAINTS", "UPDATE_RULE", int64(8), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "REFERENTIAL_CONSTRAINTS", "DELETE_RULE", int64(9), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "REFERENTIAL_CONSTRAINTS", "SPANNER_STATE", int64(10), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "SCHEMATA", "CATALOG_NAME", int64(1), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "SCHEMATA", "SCHEMA_NAME", int64(2), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "SCHEMATA", "EFFECTIVE_TIMESTAMP", int64(3), nil, nil, "YES", "INT64"}, // {"", "INFORMATION_SCHEMA", "TABLE_CONSTRAINTS", "CONSTRAINT_CATALOG", int64(1), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "TABLE_CONSTRAINTS", "CONSTRAINT_SCHEMA", int64(2), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "TABLE_CONSTRAINTS", "CONSTRAINT_NAME", int64(3), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "TABLE_CONSTRAINTS", "TABLE_CATALOG", int64(4), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "TABLE_CONSTRAINTS", "TABLE_SCHEMA", int64(5), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "TABLE_CONSTRAINTS", "TABLE_NAME", int64(6), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "TABLE_CONSTRAINTS", "CONSTRAINT_TYPE", int64(7), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "TABLE_CONSTRAINTS", "IS_DEFERRABLE", int64(8), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "TABLE_CONSTRAINTS", "INITIALLY_DEFERRED", int64(9), nil, nil, "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "TABLE_CONSTRAINTS", "ENFORCED", int64(10), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "TABLES", "TABLE_CATALOG", int64(1), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "TABLES", "TABLE_SCHEMA", int64(2), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "TABLES", "TABLE_NAME", int64(3), nil, nil, "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "TABLES", "PARENT_TABLE_NAME", int64(4), nil, nil, "YES", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "TABLES", "ON_DELETE_ACTION", int64(5), nil, nil, "YES", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "TABLES", "SPANNER_STATE", int64(6), nil, nil, "YES", "STRING(MAX)"}, }, }, { name: "Columns_NonReserved", sql: `SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = "" ORDER BY TABLE_NAME, ORDINAL_POSITION`, names: []string{"TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "COLUMN_NAME", "ORDINAL_POSITION", "COLUMN_DEFAULT", "DATA_TYPE", "IS_NULLABLE", "SPANNER_TYPE"}, expected: [][]interface{}{ {"", "", "ArrayTypes", "Id", int64(1), nil, nil, "NO", "INT64"}, {"", "", "ArrayTypes", "ArrayString", int64(2), nil, nil, "YES", "ARRAY"}, {"", "", "ArrayTypes", "ArrayBool", int64(3), nil, nil, "YES", "ARRAY"}, {"", "", "ArrayTypes", "ArrayBytes", int64(4), nil, nil, "YES", "ARRAY"}, {"", "", "ArrayTypes", "ArrayTimestamp", int64(5), nil, nil, "YES", "ARRAY"}, {"", "", "ArrayTypes", "ArrayInt", int64(6), nil, nil, "YES", "ARRAY"}, {"", "", "ArrayTypes", "ArrayFloat", int64(7), nil, nil, "YES", "ARRAY"}, {"", "", "ArrayTypes", "ArrayDate", int64(8), nil, nil, "YES", "ARRAY"}, {"", "", "CompositePrimaryKeys", "Id", int64(1), nil, nil, "NO", "INT64"}, {"", "", "CompositePrimaryKeys", "PKey1", int64(2), nil, nil, "NO", "STRING(32)"}, {"", "", "CompositePrimaryKeys", "PKey2", int64(3), nil, nil, "NO", "INT64"}, {"", "", "CompositePrimaryKeys", "Error", int64(4), nil, nil, "NO", "INT64"}, {"", "", "CompositePrimaryKeys", "X", int64(5), nil, nil, "NO", "STRING(32)"}, {"", "", "CompositePrimaryKeys", "Y", int64(6), nil, nil, "NO", "STRING(32)"}, {"", "", "CompositePrimaryKeys", "Z", int64(7), nil, nil, "NO", "STRING(32)"}, {"", "", "DefaultValues", "Id", int64(1), nil, nil, "NO", "INT64"}, {"", "", "DefaultValues", "Value", int64(2), nil, nil, "NO", "STRING(32)"}, {"", "", "DefaultValues", "N", int64(3), nil, nil, "NO", "INT64"}, {"", "", "DefaultValues", "X", int64(4), nil, nil, "NO", "STRING(32)"}, {"", "", "DefaultValues", "Y", int64(5), nil, nil, "NO", "ARRAY"}, {"", "", "DefaultValues", "T1", int64(6), nil, nil, "NO", "TIMESTAMP"}, {"", "", "DefaultValues", "T2", int64(7), nil, nil, "NO", "TIMESTAMP"}, {"", "", "DefaultValues", "Date", int64(8), nil, nil, "NO", "TIMESTAMP"}, {"", "", "ForeignChildCascade", "Id", int64(1), nil, nil, "NO", "INT64"}, {"", "", "ForeignChildCascade", "ForeignId", int64(2), nil, nil, "NO", "INT64"}, {"", "", "ForeignChildCascade", "ForeignSecondId", int64(3), nil, nil, "NO", "INT64"}, {"", "", "ForeignChildCascade", "Value", int64(4), nil, nil, "NO", "STRING(MAX)"}, {"", "", "ForeignChildNoAction", "Id", int64(1), nil, nil, "NO", "INT64"}, {"", "", "ForeignChildNoAction", "ForeignId", int64(2), nil, nil, "NO", "INT64"}, {"", "", "ForeignChildNoAction", "ForeignSecondId", int64(3), nil, nil, "NO", "INT64"}, {"", "", "ForeignChildNoAction", "Value", int64(4), nil, nil, "NO", "STRING(MAX)"}, {"", "", "ForeignParentCascade", "Id", int64(1), nil, nil, "NO", "INT64"}, {"", "", "ForeignParentCascade", "Value", int64(2), nil, nil, "NO", "STRING(MAX)"}, {"", "", "ForeignParentNoAction", "Id", int64(1), nil, nil, "NO", "INT64"}, {"", "", "ForeignParentNoAction", "Value", int64(2), nil, nil, "NO", "STRING(MAX)"}, {"", "", "From", "ALL", int64(1), nil, nil, "NO", "INT64"}, {"", "", "From", "CAST", int64(2), nil, nil, "NO", "INT64"}, {"", "", "From", "JOIN", int64(3), nil, nil, "NO", "INT64"}, {"", "", "FullTypes", "PKey", int64(1), nil, nil, "NO", "STRING(32)"}, {"", "", "FullTypes", "FTString", int64(2), nil, nil, "NO", "STRING(32)"}, {"", "", "FullTypes", "FTStringNull", int64(3), nil, nil, "YES", "STRING(32)"}, {"", "", "FullTypes", "FTBool", int64(4), nil, nil, "NO", "BOOL"}, {"", "", "FullTypes", "FTBoolNull", int64(5), nil, nil, "YES", "BOOL"}, {"", "", "FullTypes", "FTBytes", int64(6), nil, nil, "NO", "BYTES(32)"}, {"", "", "FullTypes", "FTBytesNull", int64(7), nil, nil, "YES", "BYTES(32)"}, {"", "", "FullTypes", "FTTimestamp", int64(8), nil, nil, "NO", "TIMESTAMP"}, {"", "", "FullTypes", "FTTimestampNull", int64(9), nil, nil, "YES", "TIMESTAMP"}, {"", "", "FullTypes", "FTInt", int64(10), nil, nil, "NO", "INT64"}, {"", "", "FullTypes", "FTIntNull", int64(11), nil, nil, "YES", "INT64"}, {"", "", "FullTypes", "FTFloat", int64(12), nil, nil, "NO", "FLOAT64"}, {"", "", "FullTypes", "FTFloatNull", int64(13), nil, nil, "YES", "FLOAT64"}, {"", "", "FullTypes", "FTDate", int64(14), nil, nil, "NO", "DATE"}, {"", "", "FullTypes", "FTDateNull", int64(15), nil, nil, "YES", "DATE"}, {"", "", "GeneratedColumn", "Id", int64(1), nil, nil, "NO", "INT64"}, {"", "", "GeneratedColumn", "Value", int64(2), nil, nil, "NO", "STRING(MAX)"}, {"", "", "GeneratedValues", "Id", int64(1), nil, nil, "NO", "INT64"}, {"", "", "GeneratedValues", "Value", int64(2), nil, nil, "NO", "STRING(32)"}, {"", "", "GeneratedValues", "N", int64(3), nil, nil, "NO", "INT64"}, {"", "", "GeneratedValues", "N2", int64(4), nil, nil, "NO", "INT64"}, {"", "", "GeneratedValues", "N3", int64(5), nil, nil, "NO", "INT64"}, {"", "", "GeneratedValues", "X", int64(6), nil, nil, "NO", "STRING(32)"}, {"", "", "GeneratedValues", "X2", int64(7), nil, nil, "NO", "STRING(32)"}, {"", "", "Interleaved", "InterleavedId", int64(1), nil, nil, "NO", "INT64"}, {"", "", "Interleaved", "Id", int64(2), nil, nil, "NO", "INT64"}, {"", "", "Interleaved", "Value", int64(3), nil, nil, "NO", "STRING(MAX)"}, {"", "", "InterleavedCascade", "InterleavedId", int64(1), nil, nil, "NO", "INT64"}, {"", "", "InterleavedCascade", "Id", int64(2), nil, nil, "NO", "INT64"}, {"", "", "InterleavedCascade", "Value", int64(3), nil, nil, "NO", "STRING(MAX)"}, {"", "", "InterleavedNoAction", "InterleavedId", int64(1), nil, nil, "NO", "INT64"}, {"", "", "InterleavedNoAction", "Id", int64(2), nil, nil, "NO", "INT64"}, {"", "", "InterleavedNoAction", "Value", int64(3), nil, nil, "NO", "STRING(MAX)"}, {"", "", "JoinA", "Id", int64(1), nil, nil, "NO", "INT64"}, {"", "", "JoinA", "Value", int64(2), nil, nil, "NO", "STRING(MAX)"}, {"", "", "JoinB", "Id", int64(1), nil, nil, "NO", "INT64"}, {"", "", "JoinB", "Value", int64(2), nil, nil, "NO", "STRING(MAX)"}, {"", "", "ParentTable", "Id", int64(1), nil, nil, "NO", "INT64"}, {"", "", "ParentTable", "Value", int64(2), nil, nil, "NO", "STRING(MAX)"}, {"", "", "ParentTableCascade", "Id", int64(1), nil, nil, "NO", "INT64"}, {"", "", "ParentTableCascade", "Value", int64(2), nil, nil, "NO", "STRING(MAX)"}, {"", "", "ParentTableNoAction", "Id", int64(1), nil, nil, "NO", "INT64"}, {"", "", "ParentTableNoAction", "Value", int64(2), nil, nil, "NO", "STRING(MAX)"}, {"", "", "Simple", "Id", int64(1), nil, nil, "NO", "INT64"}, {"", "", "Simple", "Value", int64(2), nil, nil, "NO", "STRING(MAX)"}, }, }, { name: "ColumnOptions", sql: `SELECT * FROM INFORMATION_SCHEMA.COLUMN_OPTIONS ORDER BY TABLE_NAME`, names: []string{"TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "COLUMN_NAME", "OPTION_NAME", "OPTION_TYPE", "OPTION_VALUE"}, expected: [][]interface{}{ {"", "", "FullTypes", "FTTimestampNull", "allow_commit_timestamp", "BOOL", "TRUE"}, }, }, { name: "Indexes_ReservedTables", sql: `SELECT * FROM INFORMATION_SCHEMA.INDEXES WHERE TABLE_SCHEMA != "" ORDER BY TABLE_NAME`, names: []string{"TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "INDEX_NAME", "INDEX_TYPE", "PARENT_TABLE_NAME", "IS_UNIQUE", "IS_NULL_FILTERED", "INDEX_STATE", "SPANNER_IS_MANAGED"}, expected: [][]interface{}{ {"", "INFORMATION_SCHEMA", "COLUMNS", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "INFORMATION_SCHEMA", "COLUMN_OPTIONS", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_COLUMN_USAGE", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_TABLE_USAGE", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "INFORMATION_SCHEMA", "INDEXES", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "INFORMATION_SCHEMA", "INDEX_COLUMNS", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, // {"", "INFORMATION_SCHEMA", "KEY_COLUMN_USAGE", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "INFORMATION_SCHEMA", "REFERENTIAL_CONSTRAINTS", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "INFORMATION_SCHEMA", "SCHEMATA", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, // {"", "INFORMATION_SCHEMA", "TABLE_CONSTRAINTS", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "INFORMATION_SCHEMA", "TABLES", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, }, }, { name: "Indexes_NonReservedTables_PrimaryKey", sql: `SELECT * FROM INFORMATION_SCHEMA.INDEXES WHERE TABLE_SCHEMA = "" AND INDEX_TYPE = "PRIMARY_KEY" ORDER BY TABLE_NAME`, names: []string{"TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "INDEX_NAME", "INDEX_TYPE", "PARENT_TABLE_NAME", "IS_UNIQUE", "IS_NULL_FILTERED", "INDEX_STATE", "SPANNER_IS_MANAGED"}, expected: [][]interface{}{ {"", "", "ArrayTypes", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "", "CompositePrimaryKeys", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "", "DefaultValues", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "", "ForeignChildCascade", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "", "ForeignChildNoAction", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "", "ForeignParentCascade", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "", "ForeignParentNoAction", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "", "From", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "", "FullTypes", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "", "GeneratedColumn", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "", "GeneratedValues", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "", "Interleaved", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "", "InterleavedCascade", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "", "InterleavedNoAction", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "", "JoinA", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "", "JoinB", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "", "ParentTable", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "", "ParentTableCascade", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "", "ParentTableNoAction", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, {"", "", "Simple", "PRIMARY_KEY", "PRIMARY_KEY", "", true, false, nil, false}, }, }, { name: "Indexes_NonReservedTables_Others", sql: `SELECT * FROM INFORMATION_SCHEMA.INDEXES WHERE TABLE_SCHEMA = "" AND INDEX_TYPE != "PRIMARY_KEY" ORDER BY TABLE_NAME, INDEX_NAME`, names: []string{"TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "INDEX_NAME", "INDEX_TYPE", "PARENT_TABLE_NAME", "IS_UNIQUE", "IS_NULL_FILTERED", "INDEX_STATE", "SPANNER_IS_MANAGED"}, expected: [][]interface{}{ {"", "", "CompositePrimaryKeys", "CompositePrimaryKeysByError", "INDEX", "", false, false, "READ_WRITE", false}, {"", "", "CompositePrimaryKeys", "CompositePrimaryKeysByXY", "INDEX", "", false, false, "READ_WRITE", false}, {"", "", "From", "ALL", "INDEX", "", false, false, "READ_WRITE", false}, {"", "", "FullTypes", "FullTypesByFTString", "INDEX", "", true, false, "READ_WRITE", false}, {"", "", "FullTypes", "FullTypesByIntDate", "INDEX", "", true, false, "READ_WRITE", false}, {"", "", "FullTypes", "FullTypesByIntTimestamp", "INDEX", "", false, false, "READ_WRITE", false}, {"", "", "FullTypes", "FullTypesByTimestamp", "INDEX", "", false, false, "READ_WRITE", false}, {"", "", "Interleaved", "InterleavedKey", "INDEX", "ParentTable", false, false, "READ_WRITE", false}, }, }, { name: "IndexColumns_ReservedTables", sql: `SELECT * FROM INFORMATION_SCHEMA.INDEX_COLUMNS WHERE TABLE_SCHEMA != "" ORDER BY TABLE_NAME, INDEX_NAME, ORDINAL_POSITION`, names: []string{"TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "INDEX_NAME", "INDEX_TYPE", "COLUMN_NAME", "ORDINAL_POSITION", "COLUMN_ORDERING", "IS_NULLABLE", "SPANNER_TYPE"}, expected: [][]interface{}{ {"", "INFORMATION_SCHEMA", "COLUMNS", "PRIMARY_KEY", "PRIMARY_KEY", "TABLE_CATALOG", int64(1), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMNS", "PRIMARY_KEY", "PRIMARY_KEY", "TABLE_SCHEMA", int64(2), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMNS", "PRIMARY_KEY", "PRIMARY_KEY", "TABLE_NAME", int64(3), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMNS", "PRIMARY_KEY", "PRIMARY_KEY", "COLUMN_NAME", int64(4), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMN_OPTIONS", "PRIMARY_KEY", "PRIMARY_KEY", "TABLE_CATALOG", int64(1), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMN_OPTIONS", "PRIMARY_KEY", "PRIMARY_KEY", "TABLE_SCHEMA", int64(2), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMN_OPTIONS", "PRIMARY_KEY", "PRIMARY_KEY", "TABLE_NAME", int64(3), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMN_OPTIONS", "PRIMARY_KEY", "PRIMARY_KEY", "COLUMN_NAME", int64(4), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "COLUMN_OPTIONS", "PRIMARY_KEY", "PRIMARY_KEY", "OPTION_NAME", int64(5), "ASC", "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_COLUMN_USAGE", "PRIMARY_KEY", "PRIMARY_KEY", "CONSTRAINT_CATALOG", int64(1), "ASC", "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_COLUMN_USAGE", "PRIMARY_KEY", "PRIMARY_KEY", "CONSTRAINT_SCHEMA", int64(2), "ASC", "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_COLUMN_USAGE", "PRIMARY_KEY", "PRIMARY_KEY", "CONSTRAINT_NAME", int64(3), "ASC", "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_COLUMN_USAGE", "PRIMARY_KEY", "PRIMARY_KEY", "COLUMN_NAME", int64(4), "ASC", "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_TABLE_USAGE", "PRIMARY_KEY", "PRIMARY_KEY", "TABLE_CATALOG", int64(1), "ASC", "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_TABLE_USAGE", "PRIMARY_KEY", "PRIMARY_KEY", "TABLE_SCHEMA", int64(2), "ASC", "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_TABLE_USAGE", "PRIMARY_KEY", "PRIMARY_KEY", "TABLE_NAME", int64(3), "ASC", "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_TABLE_USAGE", "PRIMARY_KEY", "PRIMARY_KEY", "CONSTRAINT_CATALOG", int64(4), "ASC", "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_TABLE_USAGE", "PRIMARY_KEY", "PRIMARY_KEY", "CONSTRAINT_SCHEMA", int64(5), "ASC", "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "CONSTRAINT_TABLE_USAGE", "PRIMARY_KEY", "PRIMARY_KEY", "CONSTRAINT_NAME", int64(6), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEXES", "PRIMARY_KEY", "PRIMARY_KEY", "TABLE_CATALOG", int64(1), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEXES", "PRIMARY_KEY", "PRIMARY_KEY", "TABLE_SCHEMA", int64(2), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEXES", "PRIMARY_KEY", "PRIMARY_KEY", "TABLE_NAME", int64(3), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEXES", "PRIMARY_KEY", "PRIMARY_KEY", "INDEX_NAME", int64(4), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEXES", "PRIMARY_KEY", "PRIMARY_KEY", "INDEX_TYPE", int64(5), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEX_COLUMNS", "PRIMARY_KEY", "PRIMARY_KEY", "TABLE_CATALOG", int64(1), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEX_COLUMNS", "PRIMARY_KEY", "PRIMARY_KEY", "TABLE_SCHEMA", int64(2), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEX_COLUMNS", "PRIMARY_KEY", "PRIMARY_KEY", "TABLE_NAME", int64(3), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEX_COLUMNS", "PRIMARY_KEY", "PRIMARY_KEY", "INDEX_NAME", int64(4), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEX_COLUMNS", "PRIMARY_KEY", "PRIMARY_KEY", "INDEX_TYPE", int64(5), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "INDEX_COLUMNS", "PRIMARY_KEY", "PRIMARY_KEY", "COLUMN_NAME", int64(6), "ASC", "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "KEY_COLUMN_USAGE", "PRIMARY_KEY", "PRIMARY_KEY", "CONSTRAINT_CATALOG", int64(1), "ASC", "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "KEY_COLUMN_USAGE", "PRIMARY_KEY", "PRIMARY_KEY", "CONSTRAINT_SCHEMA", int64(2), "ASC", "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "KEY_COLUMN_USAGE", "PRIMARY_KEY", "PRIMARY_KEY", "CONSTRAINT_NAME", int64(3), "ASC", "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "KEY_COLUMN_USAGE", "PRIMARY_KEY", "PRIMARY_KEY", "COLUMN_NAME", int64(4), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "REFERENTIAL_CONSTRAINTS", "PRIMARY_KEY", "PRIMARY_KEY", "CONSTRAINT_CATALOG", int64(1), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "REFERENTIAL_CONSTRAINTS", "PRIMARY_KEY", "PRIMARY_KEY", "CONSTRAINT_SCHEMA", int64(2), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "REFERENTIAL_CONSTRAINTS", "PRIMARY_KEY", "PRIMARY_KEY", "CONSTRAINT_NAME", int64(3), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "SCHEMATA", "PRIMARY_KEY", "PRIMARY_KEY", "CATALOG_NAME", int64(1), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "SCHEMATA", "PRIMARY_KEY", "PRIMARY_KEY", "SCHEMA_NAME", int64(2), "ASC", "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "TABLE_CONSTRAINTS", "PRIMARY_KEY", "PRIMARY_KEY", "CONSTRAINT_CATALOG", int64(1), "ASC", "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "TABLE_CONSTRAINTS", "PRIMARY_KEY", "PRIMARY_KEY", "CONSTRAINT_SCHEMA", int64(2), "ASC", "NO", "STRING(MAX)"}, // {"", "INFORMATION_SCHEMA", "TABLE_CONSTRAINTS", "PRIMARY_KEY", "PRIMARY_KEY", "CONSTRAINT_NAME", int64(3), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "TABLES", "PRIMARY_KEY", "PRIMARY_KEY", "TABLE_CATALOG", int64(1), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "TABLES", "PRIMARY_KEY", "PRIMARY_KEY", "TABLE_SCHEMA", int64(2), "ASC", "NO", "STRING(MAX)"}, {"", "INFORMATION_SCHEMA", "TABLES", "PRIMARY_KEY", "PRIMARY_KEY", "TABLE_NAME", int64(3), "ASC", "NO", "STRING(MAX)"}, }, }, { name: "IndexColumns_NonReservedTables", sql: `SELECT * FROM INFORMATION_SCHEMA.INDEX_COLUMNS WHERE TABLE_SCHEMA = "" ORDER BY TABLE_NAME, INDEX_NAME, ORDINAL_POSITION`, names: []string{"TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "INDEX_NAME", "INDEX_TYPE", "COLUMN_NAME", "ORDINAL_POSITION", "COLUMN_ORDERING", "IS_NULLABLE", "SPANNER_TYPE"}, expected: [][]interface{}{ {"", "", "ArrayTypes", "PRIMARY_KEY", "PRIMARY_KEY", "Id", int64(1), "ASC", "NO", "INT64"}, {"", "", "CompositePrimaryKeys", "CompositePrimaryKeysByError", "INDEX", "Error", int64(1), "ASC", "NO", "INT64"}, {"", "", "CompositePrimaryKeys", "CompositePrimaryKeysByXY", "INDEX", "Z", nil, nil, "NO", "STRING(32)"}, {"", "", "CompositePrimaryKeys", "CompositePrimaryKeysByXY", "INDEX", "X", int64(1), "ASC", "NO", "STRING(32)"}, {"", "", "CompositePrimaryKeys", "CompositePrimaryKeysByXY", "INDEX", "Y", int64(2), "DESC", "NO", "STRING(32)"}, {"", "", "CompositePrimaryKeys", "PRIMARY_KEY", "PRIMARY_KEY", "PKey1", int64(1), "ASC", "NO", "STRING(32)"}, {"", "", "CompositePrimaryKeys", "PRIMARY_KEY", "PRIMARY_KEY", "PKey2", int64(2), "DESC", "NO", "INT64"}, {"", "", "DefaultValues", "PRIMARY_KEY", "PRIMARY_KEY", "Id", int64(1), "ASC", "NO", "INT64"}, {"", "", "ForeignChildCascade", "PRIMARY_KEY", "PRIMARY_KEY", "Id", int64(1), "ASC", "NO", "INT64"}, {"", "", "ForeignChildCascade", "PRIMARY_KEY", "PRIMARY_KEY", "ForeignId", int64(2), "ASC", "NO", "INT64"}, {"", "", "ForeignChildNoAction", "PRIMARY_KEY", "PRIMARY_KEY", "Id", int64(1), "ASC", "NO", "INT64"}, {"", "", "ForeignChildNoAction", "PRIMARY_KEY", "PRIMARY_KEY", "ForeignId", int64(2), "ASC", "NO", "INT64"}, {"", "", "ForeignParentCascade", "PRIMARY_KEY", "PRIMARY_KEY", "Id", int64(1), "ASC", "NO", "INT64"}, {"", "", "ForeignParentNoAction", "PRIMARY_KEY", "PRIMARY_KEY", "Id", int64(1), "ASC", "NO", "INT64"}, {"", "", "From", "ALL", "INDEX", "ALL", int64(1), "ASC", "NO", "INT64"}, {"", "", "From", "PRIMARY_KEY", "PRIMARY_KEY", "ALL", int64(1), "ASC", "NO", "INT64"}, {"", "", "FullTypes", "FullTypesByFTString", "INDEX", "FTString", int64(1), "ASC", "NO", "STRING(32)"}, {"", "", "FullTypes", "FullTypesByIntDate", "INDEX", "FTInt", int64(1), "ASC", "NO", "INT64"}, {"", "", "FullTypes", "FullTypesByIntDate", "INDEX", "FTDate", int64(2), "ASC", "NO", "DATE"}, {"", "", "FullTypes", "FullTypesByIntTimestamp", "INDEX", "FTInt", int64(1), "ASC", "NO", "INT64"}, {"", "", "FullTypes", "FullTypesByIntTimestamp", "INDEX", "FTTimestamp", int64(2), "ASC", "NO", "TIMESTAMP"}, {"", "", "FullTypes", "FullTypesByTimestamp", "INDEX", "FTTimestamp", int64(1), "ASC", "NO", "TIMESTAMP"}, {"", "", "FullTypes", "PRIMARY_KEY", "PRIMARY_KEY", "PKey", int64(1), "ASC", "NO", "STRING(32)"}, {"", "", "GeneratedColumn", "PRIMARY_KEY", "PRIMARY_KEY", "Id", int64(1), "ASC", "NO", "INT64"}, {"", "", "GeneratedValues", "PRIMARY_KEY", "PRIMARY_KEY", "Id", int64(1), "ASC", "NO", "INT64"}, {"", "", "Interleaved", "InterleavedKey", "INDEX", "Id", int64(1), "ASC", "NO", "INT64"}, {"", "", "Interleaved", "InterleavedKey", "INDEX", "Value", int64(2), "ASC", "NO", "STRING(MAX)"}, {"", "", "Interleaved", "PRIMARY_KEY", "PRIMARY_KEY", "Id", int64(1), "ASC", "NO", "INT64"}, {"", "", "Interleaved", "PRIMARY_KEY", "PRIMARY_KEY", "InterleavedId", int64(2), "ASC", "NO", "INT64"}, {"", "", "InterleavedCascade", "PRIMARY_KEY", "PRIMARY_KEY", "Id", int64(1), "ASC", "NO", "INT64"}, {"", "", "InterleavedCascade", "PRIMARY_KEY", "PRIMARY_KEY", "InterleavedId", int64(2), "ASC", "NO", "INT64"}, {"", "", "InterleavedNoAction", "PRIMARY_KEY", "PRIMARY_KEY", "Id", int64(1), "ASC", "NO", "INT64"}, {"", "", "InterleavedNoAction", "PRIMARY_KEY", "PRIMARY_KEY", "InterleavedId", int64(2), "ASC", "NO", "INT64"}, {"", "", "JoinA", "PRIMARY_KEY", "PRIMARY_KEY", "Id", int64(1), "ASC", "NO", "INT64"}, {"", "", "JoinB", "PRIMARY_KEY", "PRIMARY_KEY", "Id", int64(1), "ASC", "NO", "INT64"}, {"", "", "ParentTable", "PRIMARY_KEY", "PRIMARY_KEY", "Id", int64(1), "ASC", "NO", "INT64"}, {"", "", "ParentTableCascade", "PRIMARY_KEY", "PRIMARY_KEY", "Id", int64(1), "ASC", "NO", "INT64"}, {"", "", "ParentTableNoAction", "PRIMARY_KEY", "PRIMARY_KEY", "Id", int64(1), "ASC", "NO", "INT64"}, {"", "", "Simple", "PRIMARY_KEY", "PRIMARY_KEY", "Id", int64(1), "ASC", "NO", "INT64"}, }, }, { name: "IndexColumns_WithAlias", sql: `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.INDEX_COLUMNS a WHERE a.TABLE_NAME = "Simple"`, expected: [][]interface{}{ []interface{}{"Id"}, }, }, { name: "IndexColumns_WithOmittedSchema", sql: `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.INDEX_COLUMNS WHERE INDEX_COLUMNS.TABLE_NAME = "Simple"`, expected: [][]interface{}{ []interface{}{"Id"}, }, }, { name: "IndexColumns_WithJoin", sql: `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.INDEX_COLUMNS LEFT JOIN INFORMATION_SCHEMA.INDEXES ON INDEX_COLUMNS.TABLE_NAME = INDEXES.TABLE_NAME WHERE INDEX_COLUMNS.TABLE_NAME = "Simple"`, expected: [][]interface{}{ []interface{}{"Id"}, }, }, { name: "IndexColumns_WithUsingJoin", sql: `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.INDEX_COLUMNS LEFT JOIN INFORMATION_SCHEMA.INDEXES USING(TABLE_NAME) WHERE INDEX_COLUMNS.TABLE_NAME = "Simple"`, expected: [][]interface{}{ []interface{}{"Id"}, }, }, { name: "ReferentialConstraints", sql: `SELECT CONSTRAINT_CATALOG, CONSTRAINT_SCHEMA, CONSTRAINT_NAME, UNIQUE_CONSTRAINT_CATALOG, UNIQUE_CONSTRAINT_SCHEMA, UNIQUE_CONSTRAINT_NAME, MATCH_OPTION, UPDATE_RULE, DELETE_RULE, SPANNER_STATE FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS`, expected: [][]interface{}{ {"", "", "FK_Cascade", "", "", "ForeignParentCascade", "SIMPLE", "NO ACTION", "CASCADE", "COMMITTED"}, {"", "", "FK_ForeignChildCascade_ForeignParentCascade_1", "", "", "ForeignParentCascade", "SIMPLE", "NO ACTION", "CASCADE", "COMMITTED"}, {"", "", "FK_NoAction", "", "", "ForeignParentNoAction", "SIMPLE", "NO ACTION", "NO ACTION", "COMMITTED"}, {"", "", "FK_ForeignChildNoAction_ForeignParentNoAction_1", "", "", "ForeignParentNoAction", "SIMPLE", "NO ACTION", "NO ACTION", "COMMITTED"}, }, }, } for _, tc := range table { t.Run(tc.name, func(t *testing.T) { ses := newSession(db, "foo") testRunInTransaction(t, ses, func(tx *transaction) { stmt, err := (&memefish.Parser{ Lexer: &memefish.Lexer{ File: &token.File{FilePath: "", Buffer: tc.sql}, }, }).ParseQuery() if err != nil { t.Fatalf("failed to parse sql: %q %v", tc.sql, err) } // The test case expects OK, it checks respons values. // otherwise it checks the error code and the error message. if tc.code == codes.OK { it, err := db.Query(ctx, tx, stmt, tc.params) if err != nil { t.Fatalf("Query failed: %v", err) } var rows [][]interface{} err = it.Do(func(row []interface{}) error { rows = append(rows, row) return nil }) if err != nil { t.Fatalf("unexpected error in iteration: %v", err) } if diff := cmp.Diff(tc.expected, rows); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } // TODO: add names to all test cases. now this is optional check if tc.names != nil { var gotnames []string for _, item := range it.ResultSet() { gotnames = append(gotnames, item.Name) } if diff := cmp.Diff(tc.names, gotnames); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } } } else { it, err := db.Query(ctx, tx, stmt, tc.params) if err == nil { err = it.Do(func([]interface{}) error { return nil }) } st := status.Convert(err) if st.Code() != tc.code { t.Errorf("expect code to be %v but got %v", tc.code, st.Code()) } if !tc.msg.MatchString(st.Message()) { t.Errorf("unexpected error message: \n %q\n expected:\n %q", st.Message(), tc.msg) } } }) }) } } func TestInsertUnderForeignConstraint(t *testing.T) { type tableConfig struct { tbl string wcols []string values []*structpb.Value cols []string limit int64 expected [][]interface{} } table := map[string]struct { parent *tableConfig child *tableConfig expectsError bool }{ "InsertWithoutMaster": { child: &tableConfig{ tbl: "ForeignChildNoAction", wcols: []string{"Id", "ForeignId", "ForeignSecondId", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("200"), makeStringValue("200"), makeStringValue("yyy"), }, cols: []string{"Id", "ForeignId", "ForeignSecondId", "Value"}, limit: 100, expected: nil, }, expectsError: true, }, "InsertWithParent": { parent: &tableConfig{ tbl: "ForeignParentNoAction", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("200"), makeStringValue("xxx"), }, cols: []string{"Id", "Value"}, limit: 100, expected: [][]interface{}{ {int64(200), "xxx"}, }, }, child: &tableConfig{ tbl: "ForeignChildNoAction", wcols: []string{"Id", "ForeignId", "ForeignSecondId", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("200"), makeStringValue("200"), makeStringValue("yyy"), }, cols: []string{"Id", "ForeignId", "ForeignSecondId", "Value"}, limit: 100, expected: [][]interface{}{ {int64(100), int64(200), int64(200), "yyy"}, }, }, expectsError: false, }, "InsertWithoutMaster(Cascade)": { child: &tableConfig{ tbl: "ForeignChildCascade", wcols: []string{"Id", "ForeignId", "ForeignSecondId", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("200"), makeStringValue("200"), makeStringValue("yyy"), }, cols: []string{"Id", "ForeignId", "ForeignSecondId", "Value"}, limit: 100, expected: nil, }, expectsError: true, }, "InsertWithParent(Cascade)": { parent: &tableConfig{ tbl: "ForeignParentCascade", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("200"), makeStringValue("xxx"), }, cols: []string{"Id", "Value"}, limit: 100, expected: [][]interface{}{ {int64(200), "xxx"}, }, }, child: &tableConfig{ tbl: "ForeignChildCascade", wcols: []string{"Id", "ForeignId", "ForeignSecondId", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("200"), makeStringValue("200"), makeStringValue("yyy"), }, cols: []string{"Id", "ForeignId", "ForeignSecondId", "Value"}, limit: 100, expected: [][]interface{}{ {int64(100), int64(200), int64(200), "yyy"}, }, }, expectsError: false, }, } for name, tt := range table { t.Run(name, func(t *testing.T) { ctx := context.Background() db := newDatabase() ses := newSession(db, "foo") for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } if tt.parent != nil { testRunInTransaction(t, ses, func(tx *transaction) { testInsertHelper(t, ctx, db, tx, tt.parent.tbl, tt.parent.cols, tt.parent.wcols, tt.parent.values, tt.parent.limit, tt.parent.expected) }) } if tt.expectsError { listValues := []*structpb.ListValue{ {Values: tt.child.values}, } testRunInTransaction(t, ses, func(tx *transaction) { if err := db.Insert(ctx, tx, tt.child.tbl, tt.child.wcols, listValues); err == nil { t.Fatalf("Insert succeeded even though it should fail: %v", err) } }) return } testRunInTransaction(t, ses, func(tx *transaction) { testInsertHelper(t, ctx, db, tx, tt.child.tbl, tt.child.cols, tt.child.wcols, tt.child.values, tt.child.limit, tt.child.expected) }) }) } } func TestDeleteUnderForeignConstraint(t *testing.T) { type tableConfig struct { tbl string wcols []string values []*structpb.Value cols []string ks *KeySet } table := map[string]struct { child *tableConfig parent *tableConfig expectsDeleteError bool }{ "DeleteNoAction": { parent: &tableConfig{ tbl: "ForeignParentNoAction", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("200"), makeStringValue("xxx"), }, cols: []string{"Id", "Value"}, ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue(makeStringValue("200")), }, }, }, child: &tableConfig{ tbl: "ForeignChildNoAction", wcols: []string{"Id", "ForeignId", "ForeignSecondId", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("200"), makeStringValue("200"), makeStringValue("yyy"), }, cols: []string{"Id", "ForeignId", "ForeignSecondId", "Value"}, ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue( makeStringValue("100"), makeStringValue("200"), ), }, }, }, expectsDeleteError: true, }, "DeleteCascade": { parent: &tableConfig{ tbl: "ForeignParentCascade", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("200"), makeStringValue("xxx"), }, cols: []string{"Id", "Value"}, ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue(makeStringValue("200")), }, }, }, child: &tableConfig{ tbl: "ForeignChildCascade", wcols: []string{"Id", "ForeignId", "ForeignSecondId", "Value"}, values: []*structpb.Value{ makeStringValue("100"), makeStringValue("200"), makeStringValue("200"), makeStringValue("yyy"), }, cols: []string{"Id", "ForeignId", "ForeignSecondId", "Value"}, ks: &KeySet{ Keys: []*structpb.ListValue{ makeListValue( makeStringValue("100"), makeStringValue("200"), ), }, }, }, expectsDeleteError: false, }, } for name, tt := range table { t.Run(name, func(t *testing.T) { ctx := context.Background() db := newDatabase() ses := newSession(db, "foo") for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } // Insert data parentListValues := []*structpb.ListValue{ {Values: tt.parent.values}, } testRunInTransaction(t, ses, func(tx *transaction) { if err := db.Insert(ctx, tx, tt.parent.tbl, tt.parent.wcols, parentListValues); err != nil { t.Fatalf("Insert failed: %v", err) } }) listValues := []*structpb.ListValue{ {Values: tt.child.values}, } testRunInTransaction(t, ses, func(tx *transaction) { if err := db.Insert(ctx, tx, tt.child.tbl, tt.child.wcols, listValues); err != nil { t.Fatalf("Insert failed: %v", err) } }) // Delete parent entry if tt.expectsDeleteError { testRunInTransaction(t, ses, func(tx *transaction) { if err := db.Delete(ctx, tx, tt.parent.tbl, tt.parent.ks); err == nil { t.Fatalf("Delete parent succeeded even though it should fail: %v", err) } }) return } testRunInTransaction(t, ses, func(tx *transaction) { if err := db.Delete(ctx, tx, tt.parent.tbl, tt.parent.ks); err != nil { t.Fatalf("Delete parent failed: %v", err) } }) // Try to read child entry testRunInTransaction(t, ses, func(tx *transaction) { it, err := db.Read(ctx, tx, tt.child.tbl, "", tt.child.cols, tt.child.ks, 1) if err != nil { t.Fatalf("Read failed: %v", err) } var rows [][]interface{} err = it.Do(func(row []interface{}) error { rows = append(rows, row) return nil }) if err != nil { t.Fatalf("unexpected error in iteration: %v", err) } if rows != nil { t.Fatalf("Child did not get deleted with parent") } }) }) } } ================================================ FILE: server/debug.go ================================================ // Copyright 2019 Masahiro Sano // // 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 server import ( "fmt" ) func Debugf(format string, args ...interface{}) { if IsDebug() { fmt.Printf(format, args...) } } func IsDebug() bool { return false } func DebugStartEnd(format string, args ...interface{}) func() { fmt.Printf(format+" start\n", args...) return func() { fmt.Printf(format+" end\n", args...) } } ================================================ FILE: server/keyset.go ================================================ // Copyright 2019 Masahiro Sano // // 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 server import ( "fmt" "strings" spannerpb "google.golang.org/genproto/googleapis/spanner/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" structpb "google.golang.org/protobuf/types/known/structpb" ) type KeySet struct { All bool Keys []*structpb.ListValue Ranges []*KeyRange } type KeyRange struct { start *structpb.ListValue end *structpb.ListValue startClosed bool endClosed bool } func makeKeySet(set *spannerpb.KeySet) *KeySet { ranges := make([]*KeyRange, 0, len(set.Ranges)) for _, r := range set.Ranges { ranges = append(ranges, makeKeyRange(r)) } return &KeySet{ All: set.All, Keys: set.Keys, Ranges: ranges, } } func makeKeyRange(r *spannerpb.KeyRange) *KeyRange { var kr KeyRange switch s := r.StartKeyType.(type) { case *spannerpb.KeyRange_StartClosed: kr.start = s.StartClosed kr.startClosed = true case *spannerpb.KeyRange_StartOpen: kr.start = s.StartOpen } switch e := r.EndKeyType.(type) { case *spannerpb.KeyRange_EndClosed: kr.end = e.EndClosed kr.endClosed = true case *spannerpb.KeyRange_EndOpen: kr.end = e.EndOpen } return &kr } func buildWhereClauseFromKeySet(keyset *KeySet, indexColumnsName string, indexColumns []*Column, indexColumnDirs []string) (string, []interface{}, error) { if keyset.All { return "", nil, nil } var conditions []string var subargs [][]interface{} if len(keyset.Keys) != 0 { q, a, err := buildKeySetQuery(indexColumnsName, indexColumns, keyset.Keys) if err != nil { return "", nil, err } conditions = append(conditions, q) subargs = append(subargs, a) } if len(keyset.Ranges) != 0 { for _, keyrange := range keyset.Ranges { q, a, err := buildKeyRangeQuery(indexColumnsName, indexColumns, indexColumnDirs, keyrange) if err != nil { return "", nil, err } conditions = append(conditions, q) subargs = append(subargs, a) } } cond := strings.Join(conditions, " OR ") var args []interface{} for i := range subargs { args = append(args, subargs[i]...) } return fmt.Sprintf("WHERE %s", cond), args, nil } func buildKeySetQuery(pkeysName string, pkeyColumns []*Column, keys []*structpb.ListValue) (string, []interface{}, error) { numPKeys := len(pkeyColumns) args := make([]interface{}, 0, len(keys)*numPKeys) for _, key := range keys { if len(key.Values) != numPKeys { return "", nil, status.Errorf(codes.InvalidArgument, "TODO: invalid keys") } values, err := convertToDatabaseValues(key, pkeyColumns) if err != nil { return "", nil, err } args = append(args, values...) } // build placeholders for values e.g. (?, ?, ?) valuesPlaceholder := "(?" + strings.Repeat(", ?", numPKeys-1) + ")" // repeat placeholders for the number of keys e.g. (?, ?, ?), (?, ?, ?), (?, ?, ?) valuesExpr := valuesPlaceholder + strings.Repeat(", "+valuesPlaceholder, len(keys)-1) // e.g. WHERE (key1, key2, key3) IN ( VALUES (?, ?, ?), (?, ?, ?) ) whereCondition := fmt.Sprintf("(%s) IN ( VALUES %s )", pkeysName, valuesExpr) return whereCondition, args, nil } func buildKeyRangeQuery(pkeysName string, pkeyColumns []*Column, indexColumnDirs []string, keyrange *KeyRange) (string, []interface{}, error) { numPKeys := len(pkeyColumns) if numPKeys < len(keyrange.start.Values) { return "", nil, status.Errorf(codes.InvalidArgument, "TODO: invalid start range key") } if numPKeys < len(keyrange.end.Values) { return "", nil, status.Errorf(codes.InvalidArgument, "TODO: invalid end range key") } startKeyValues, err := convertToDatabaseValues(keyrange.start, pkeyColumns) if err != nil { return "", nil, err } endKeyValues, err := convertToDatabaseValues(keyrange.end, pkeyColumns) if err != nil { return "", nil, err } whereClause := make([]string, 0, len(startKeyValues)+len(endKeyValues)) args := make([]interface{}, 0, len(whereClause)) var maxLen int if len(startKeyValues) > len(endKeyValues) { maxLen = len(startKeyValues) } else { maxLen = len(endKeyValues) } for i := 0; i < maxLen; i++ { pk := QuoteString(pkeyColumns[i].Name()) if len(startKeyValues) > i && len(endKeyValues) > i && startKeyValues[i] == endKeyValues[i] { whereClause = append(whereClause, fmt.Sprintf("%s = ?", pk)) args = append(args, startKeyValues[i]) continue } if len(startKeyValues) > i { if indexColumnDirs[i] == "DESC" { if keyrange.startClosed { whereClause = append(whereClause, fmt.Sprintf("%s <= ?", pk)) } else { whereClause = append(whereClause, fmt.Sprintf("%s < ?", pk)) } } else { if keyrange.startClosed { whereClause = append(whereClause, fmt.Sprintf("%s >= ?", pk)) } else { whereClause = append(whereClause, fmt.Sprintf("%s > ?", pk)) } } args = append(args, startKeyValues[i]) } if len(endKeyValues) > i { if indexColumnDirs[i] == "DESC" { if keyrange.endClosed { whereClause = append(whereClause, fmt.Sprintf("%s >= ?", pk)) } else { whereClause = append(whereClause, fmt.Sprintf("%s > ?", pk)) } } else { if keyrange.endClosed { whereClause = append(whereClause, fmt.Sprintf("%s <= ?", pk)) } else { whereClause = append(whereClause, fmt.Sprintf("%s < ?", pk)) } } args = append(args, endKeyValues[i]) } } return fmt.Sprintf("(%s)", strings.Join(whereClause, " AND ")), args, nil } ================================================ FILE: server/meta_schema.go ================================================ // Copyright 2020 Masahiro Sano // // 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 server import ( "github.com/cloudspannerecosystem/memefish/ast" ) var metaTablesMap = map[string]string{ "INFORMATION_SCHEMA.SCHEMATA": "__INFORMATION_SCHEMA__SCHEMATA", "INFORMATION_SCHEMA.TABLES": "__INFORMATION_SCHEMA__TABLES", "INFORMATION_SCHEMA.COLUMNS": "__INFORMATION_SCHEMA__COLUMNS", "INFORMATION_SCHEMA.INDEXES": "__INFORMATION_SCHEMA__INDEXES", "INFORMATION_SCHEMA.INDEX_COLUMNS": "__INFORMATION_SCHEMA__INDEX_COLUMNS", "INFORMATION_SCHEMA.COLUMN_OPTIONS": "__INFORMATION_SCHEMA__COLUMN_OPTIONS", "INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS": "__INFORMATION_SCHEMA__REFERENTIAL_CONSTRAINTS", } var metaTablesReverseMap = map[string][]string{ "__INFORMATION_SCHEMA__SCHEMATA": {"INFORMATION_SCHEMA", "SCHEMATA"}, "__INFORMATION_SCHEMA__TABLES": {"INFORMATION_SCHEMA", "TABLES"}, "__INFORMATION_SCHEMA__COLUMNS": {"INFORMATION_SCHEMA", "COLUMNS"}, "__INFORMATION_SCHEMA__INDEXES": {"INFORMATION_SCHEMA", "INDEXES"}, "__INFORMATION_SCHEMA__INDEX_COLUMNS": {"INFORMATION_SCHEMA", "INDEX_COLUMNS"}, "__INFORMATION_SCHEMA__COLUMN_OPTIONS": {"INFORMATION_SCHEMA", "COLUMN_OPTIONS"}, "__INFORMATION_SCHEMA__REFERENTIAL_CONSTRAINTS": {"INFORMATION_SCHEMA", "REFERENTIAL_CONSTRAINTS"}, } var metaTables = []*ast.CreateTable{ { Name: &ast.Path{Idents: []*ast.Ident{{Name: "__INFORMATION_SCHEMA__SCHEMATA"}}}, Columns: []*ast.ColumnDef{ { Name: &ast.Ident{Name: "CATALOG_NAME"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "SCHEMA_NAME"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "EFFECTIVE_TIMESTAMP"}, Type: &ast.ScalarSchemaType{Name: ast.Int64TypeName}, NotNull: false, }, }, PrimaryKeys: []*ast.IndexKey{ { Name: &ast.Ident{Name: "CATALOG_NAME"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "SCHEMA_NAME"}, Dir: ast.DirectionAsc, }, }, }, { Name: &ast.Path{Idents: []*ast.Ident{{Name: "__INFORMATION_SCHEMA__TABLES"}}}, Columns: []*ast.ColumnDef{ { Name: &ast.Ident{Name: "TABLE_CATALOG"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "TABLE_SCHEMA"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "TABLE_NAME"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "PARENT_TABLE_NAME"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: false, }, { Name: &ast.Ident{Name: "ON_DELETE_ACTION"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: false, }, { Name: &ast.Ident{Name: "SPANNER_STATE"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: false, }, }, PrimaryKeys: []*ast.IndexKey{ { Name: &ast.Ident{Name: "TABLE_CATALOG"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "TABLE_SCHEMA"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "TABLE_NAME"}, Dir: ast.DirectionAsc, }, }, }, { Name: &ast.Path{Idents: []*ast.Ident{{Name: "__INFORMATION_SCHEMA__COLUMNS"}}}, Columns: []*ast.ColumnDef{ { Name: &ast.Ident{Name: "TABLE_CATALOG"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "TABLE_SCHEMA"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "TABLE_NAME"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "COLUMN_NAME"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "ORDINAL_POSITION"}, Type: &ast.ScalarSchemaType{Name: ast.Int64TypeName}, NotNull: true, }, { Name: &ast.Ident{Name: "COLUMN_DEFAULT"}, Type: &ast.SizedSchemaType{Name: ast.BytesTypeName, Max: true}, NotNull: false, }, { Name: &ast.Ident{Name: "DATA_TYPE"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: false, }, { Name: &ast.Ident{Name: "IS_NULLABLE"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: false, }, { Name: &ast.Ident{Name: "SPANNER_TYPE"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: false, }, }, PrimaryKeys: []*ast.IndexKey{ { Name: &ast.Ident{Name: "TABLE_CATALOG"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "TABLE_SCHEMA"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "TABLE_NAME"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "COLUMN_NAME"}, Dir: ast.DirectionAsc, }, }, }, { Name: &ast.Path{Idents: []*ast.Ident{{Name: "__INFORMATION_SCHEMA__INDEXES"}}}, Columns: []*ast.ColumnDef{ { Name: &ast.Ident{Name: "TABLE_CATALOG"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "TABLE_SCHEMA"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "TABLE_NAME"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "INDEX_NAME"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "INDEX_TYPE"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "PARENT_TABLE_NAME"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "IS_UNIQUE"}, Type: &ast.ScalarSchemaType{Name: ast.BoolTypeName}, NotNull: true, }, { Name: &ast.Ident{Name: "IS_NULL_FILTERED"}, Type: &ast.ScalarSchemaType{Name: ast.BoolTypeName}, NotNull: true, }, { Name: &ast.Ident{Name: "INDEX_STATE"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Size: &ast.IntLiteral{Value: "100", Base: 10}}, // NotNull: true, NotNull: false, // INFORMATION_SCHEMA.COLUMNS reports this column is non-nullable, but it returns NULL... }, { Name: &ast.Ident{Name: "SPANNER_IS_MANAGED"}, Type: &ast.ScalarSchemaType{Name: ast.BoolTypeName}, NotNull: true, }, }, PrimaryKeys: []*ast.IndexKey{ { Name: &ast.Ident{Name: "TABLE_CATALOG"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "TABLE_SCHEMA"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "TABLE_NAME"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "INDEX_NAME"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "INDEX_TYPE"}, Dir: ast.DirectionAsc, }, }, }, { Name: &ast.Path{Idents: []*ast.Ident{{Name: "__INFORMATION_SCHEMA__INDEX_COLUMNS"}}}, Columns: []*ast.ColumnDef{ { Name: &ast.Ident{Name: "TABLE_CATALOG"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "TABLE_SCHEMA"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "TABLE_NAME"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "INDEX_NAME"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "INDEX_TYPE"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "COLUMN_NAME"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "ORDINAL_POSITION"}, Type: &ast.ScalarSchemaType{Name: ast.Int64TypeName}, NotNull: false, }, { Name: &ast.Ident{Name: "COLUMN_ORDERING"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: false, }, { Name: &ast.Ident{Name: "IS_NULLABLE"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: false, }, { Name: &ast.Ident{Name: "SPANNER_TYPE"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: false, }, }, PrimaryKeys: []*ast.IndexKey{ { Name: &ast.Ident{Name: "TABLE_CATALOG"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "TABLE_SCHEMA"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "TABLE_NAME"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "INDEX_NAME"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "INDEX_TYPE"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "COLUMN_NAME"}, Dir: ast.DirectionAsc, }, }, }, { Name: &ast.Path{Idents: []*ast.Ident{{Name: "__INFORMATION_SCHEMA__COLUMN_OPTIONS"}}}, Columns: []*ast.ColumnDef{ { Name: &ast.Ident{Name: "TABLE_CATALOG"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "TABLE_SCHEMA"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "TABLE_NAME"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "COLUMN_NAME"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "OPTION_NAME"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "OPTION_TYPE"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "OPTION_VALUE"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, }, PrimaryKeys: []*ast.IndexKey{ { Name: &ast.Ident{Name: "TABLE_CATALOG"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "TABLE_SCHEMA"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "TABLE_NAME"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "COLUMN_NAME"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "OPTION_NAME"}, Dir: ast.DirectionAsc, }, }, }, { Name: &ast.Path{Idents: []*ast.Ident{{Name: "__INFORMATION_SCHEMA__REFERENTIAL_CONSTRAINTS"}}}, Columns: []*ast.ColumnDef{ { Name: &ast.Ident{Name: "CONSTRAINT_CATALOG"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "CONSTRAINT_SCHEMA"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "CONSTRAINT_NAME"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "UNIQUE_CONSTRAINT_CATALOG"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: false, }, { Name: &ast.Ident{Name: "UNIQUE_CONSTRAINT_SCHEMA"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: false, }, { Name: &ast.Ident{Name: "UNIQUE_CONSTRAINT_NAME"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: false, }, { Name: &ast.Ident{Name: "MATCH_OPTION"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "UPDATE_RULE"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "DELETE_RULE"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, { Name: &ast.Ident{Name: "SPANNER_STATE"}, Type: &ast.SizedSchemaType{Name: ast.StringTypeName, Max: true}, NotNull: true, }, }, PrimaryKeys: []*ast.IndexKey{ { Name: &ast.Ident{Name: "CONSTRAINT_CATALOG"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "CONSTRAINT_SCHEMA"}, Dir: ast.DirectionAsc, }, { Name: &ast.Ident{Name: "CONSTRAINT_NAME"}, Dir: ast.DirectionAsc, }, }, }, } ================================================ FILE: server/query.go ================================================ // Copyright 2019 Masahiro Sano // // 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 server import ( "fmt" "reflect" "strconv" "strings" "time" "github.com/cloudspannerecosystem/memefish/ast" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) var defaultTimeZone = "America/Los_Angeles" var parseLocation *time.Location func init() { loc, err := time.LoadLocation(defaultTimeZone) if err != nil { panic(err) } parseLocation = loc } type QueryBuilder struct { db *database tx *transaction stmt ast.QueryExpr dml ast.DML views map[string]*TableView rootView *TableView params map[string]Value args []interface{} unnestViewNum int subqueryViewNum int // forceColumnAlias is used only for array subquery. // array subquery requires column name for the result item. // When forceColumnAlias is specified, random column names is used if the item does not have name. forceColumnAlias bool } func BuildQuery(db *database, tx *transaction, stmt ast.QueryExpr, params map[string]Value, forceColumnAlias bool) (string, []interface{}, []ResultItem, error) { b := &QueryBuilder{ db: db, tx: tx, stmt: stmt, views: make(map[string]*TableView), params: params, forceColumnAlias: forceColumnAlias, } return b.Build() } func BuildDML(db *database, tx *transaction, dml ast.DML, params map[string]Value) (string, []interface{}, error) { b := &QueryBuilder{ db: db, tx: tx, dml: dml, views: make(map[string]*TableView), params: params, } return b.BuildDML() } func (b *QueryBuilder) Build() (string, []interface{}, []ResultItem, error) { switch q := b.stmt.(type) { case *ast.Query: // ast.Query is a wrapper that contains OrderBy/Limit/WITH/PipeOperators // Delegate to the inner query and handle the additional clauses return b.buildQueryWithClauses(q) case *ast.Select: return b.buildSelectQuery(q) case *ast.SubQuery: return b.buildSubQuery(q) case *ast.CompoundQuery: return b.buildCompoundQuery(q) } return "", nil, nil, status.Errorf(codes.Unimplemented, "unsupported query expression %T", b.stmt) } func (b *QueryBuilder) buildQueryWithClauses(query *ast.Query) (string, []interface{}, []ResultItem, error) { // First, build the inner query var baseQuery string var baseData []interface{} var resultItems []ResultItem var err error switch innerQuery := query.Query.(type) { case *ast.Select: baseQuery, baseData, resultItems, err = b.buildSelectQuery(innerQuery) case *ast.SubQuery: baseQuery, baseData, resultItems, err = b.buildSubQuery(innerQuery) case *ast.CompoundQuery: baseQuery, baseData, resultItems, err = b.buildCompoundQuery(innerQuery) case *ast.Query: // Nested ast.Query (recursive case) baseQuery, baseData, resultItems, err = b.buildQueryWithClauses(innerQuery) default: return "", nil, nil, status.Errorf(codes.Unimplemented, "unsupported inner query type %T in ast.Query", innerQuery) } if err != nil { return "", nil, nil, err } // Add the collected data b.args = append(b.args, baseData...) // Build additional clauses from ast.Query var orderByClause string if query.OrderBy != nil { view := createTableViewFromItems(resultItems, nil) s, data, err := b.buildQueryOrderByClause(query.OrderBy, view) if err != nil { return "", nil, nil, err } orderByClause = fmt.Sprintf(" ORDER BY %s", s) b.args = append(b.args, data...) } var limitClause string if query.Limit != nil { s, data, err := b.buildQueryLimitOffset(query.Limit) if err != nil { return "", nil, nil, err } limitClause = fmt.Sprintf(" LIMIT %s", s) b.args = append(b.args, data...) } // TODO: Handle WITH clause and PipeOperators if needed // if query.With != nil { ... } // if len(query.PipeOperators) > 0 { ... } finalQuery := fmt.Sprintf("%s%s%s", baseQuery, orderByClause, limitClause) return finalQuery, b.args, resultItems, nil } func (b *QueryBuilder) BuildDML() (string, []interface{}, error) { switch q := b.dml.(type) { case *ast.Insert: return b.buildInsert(q) case *ast.Delete: return b.buildDelete(q) case *ast.Update: return b.buildUpdate(q) } return "", nil, status.Errorf(codes.Unimplemented, "not unknown expression %T", b.stmt) } func (b *QueryBuilder) registerTableAlias(view *TableView, name string) error { if _, ok := b.views[name]; ok { return newExprErrorf(nil, true, "Duplicate table alias a in the same FROM clause") } b.views[name] = view return nil } func (b *QueryBuilder) buildSelectQuery(selectStmt *ast.Select) (string, []interface{}, []ResultItem, error) { if len(selectStmt.Results) == 0 { return "", nil, nil, status.Errorf(codes.InvalidArgument, "Invalid query") } var fromClause string var fromData []interface{} if selectStmt.From != nil { view, s, d, err := b.buildQueryTable(selectStmt.From.Source) if err != nil { return "", nil, nil, err } fromClause = "FROM " + s fromData = d b.rootView = view } resultItems, selectQuery, err := b.buildResultSet(selectStmt.Results) if err != nil { return "", nil, nil, err } b.args = append(b.args, fromData...) whereClause, err := b.buildQuery(selectStmt) if err != nil { return "", nil, nil, err } if selectStmt.AllOrDistinct == ast.AllOrDistinctDistinct { selectQuery = "DISTINCT " + selectQuery } query := fmt.Sprintf(`SELECT %s %s %s`, selectQuery, fromClause, whereClause) var originalNames []string if b.forceColumnAlias || (selectStmt.As != nil && isAsStruct(selectStmt.As)) { newResultItems := make([]ResultItem, len(resultItems)) names := make([]string, len(resultItems)) originalNames = make([]string, len(resultItems)) for i := range resultItems { name := fmt.Sprintf("___column%d", i) names[i] = name originalNames[i] = resultItems[i].Name newResultItems[i] = ResultItem{ Name: name, ValueType: resultItems[i].ValueType, Expr: Expr{ Raw: name, ValueType: resultItems[i].ValueType, }, } } query = fmt.Sprintf(`WITH ___CTE(%s) as (%s) select * from ___CTE`, strings.Join(names, ", "), query) resultItems = newResultItems } if selectStmt.As != nil && isAsStruct(selectStmt.As) { useSqliteJSON() values := make([]string, len(resultItems)) quotedNames := make([]string, len(resultItems)) vts := make([]*ValueType, len(resultItems)) for i := range resultItems { if resultItems[i].ValueType.Code == TCArray { // column with JSON type needs converting to JSON explictly when reading from table // otherwise it is parsed as string // TODO: do this in referring timing values[i] = fmt.Sprintf("JSON(%s)", resultItems[i].Name) } else { values[i] = resultItems[i].Name } quotedNames[i] = fmt.Sprintf("'%s'", originalNames[i]) vts[i] = &resultItems[i].ValueType } vt := ValueType{ Code: TCStruct, StructType: &StructType{ FieldNames: originalNames, FieldTypes: vts, }, } result := ResultItem{ Name: "", ValueType: vt, Expr: Expr{ Raw: "___AsStruct", ValueType: vt, }, } namesObj := fmt.Sprintf("JSON_ARRAY(%s)", strings.Join(quotedNames, ", ")) valuesObj := fmt.Sprintf("JSON_ARRAY(%s)", strings.Join(values, ", ")) query = fmt.Sprintf(`SELECT JSON_OBJECT("keys", %s, "values", %s) AS ___AsStruct FROM (%s)`, namesObj, valuesObj, query) resultItems = []ResultItem{result} } return query, b.args, resultItems, nil } func (b *QueryBuilder) buildSubQuery(sub *ast.SubQuery) (string, []interface{}, []ResultItem, error) { s, data, items, err := BuildQuery(b.db, b.tx, sub.Query, b.params, b.forceColumnAlias) if err != nil { return "", nil, nil, err } // Note: OrderBy/Limit are not part of ast.SubQuery // They are only available in ast.Query wrapper. If this SubQuery // has OrderBy/Limit, it would be wrapped in ast.Query and handled there. return fmt.Sprintf("(%s)", s), data, items, nil } func (b *QueryBuilder) buildCompoundQuery(compound *ast.CompoundQuery) (string, []interface{}, []ResultItem, error) { var ss []string var op string var fullOpName string switch compound.Op { case ast.SetOpUnion: if compound.AllOrDistinct == ast.AllOrDistinctDistinct { // distinct is used by default in sqlite op = "UNION" fullOpName = "UNION DISTINCT" } else { op = "UNION ALL" fullOpName = "UNION ALL" } case ast.SetOpIntersect: if compound.AllOrDistinct == ast.AllOrDistinctDistinct { // distinct is used by default in sqlite op = "INTERSECT" fullOpName = "INTERSECT DISTINCT" } else { // sqlite does not support INTERSECT ALL // TODO: simulation of INTERSECT ALL return "", nil, nil, status.Errorf(codes.Unimplemented, "INTERSECT ALL is not supported yet") } case ast.SetOpExcept: if compound.AllOrDistinct == ast.AllOrDistinctDistinct { // distinct is used by default in sqlite op = "EXCEPT" fullOpName = "EXCEPT DISTINCT" } else { // sqlite does not support EXCEPT ALL // TODO: simulation of EXCEPT ALL return "", nil, nil, status.Errorf(codes.Unimplemented, "EXCEPT ALL is not supported yet") } } s, data, items, err := BuildQuery(b.db, b.tx, compound.Queries[0], b.params, b.forceColumnAlias) if err != nil { return "", nil, nil, err } // sqlite does not allow to put parentheses around select statement like: // (SELECT .. FROM xxx) UNION ALL (SELECT ... FROM yyy) // // This causes ambiguous LIMIT clause like: // SELECT .. FROM xxx UNION ALL SELECT ... FROM yyy LIMIT 2 // // This is possibly interpreted as // SELECT .. FROM xxx UNION ALL (SELECT ... FROM yyy LIMIT 2) // SELECT .. FROM xxx UNION ALL (SELECT ... FROM yyy) LIMIT 2 // // So use subquery to fix the issue like this: // SELECT * FROM (SELECT .. FROM xxx) UNION ALL SELECT * FROM (SELECT ... FROM yyy) ss = append(ss, fmt.Sprintf("SELECT * FROM (%s)", s)) for i, query := range compound.Queries[1:] { s, d, items2, err := BuildQuery(b.db, b.tx, query, b.params, false) if err != nil { return "", nil, nil, err } if len(items) != len(items2) { return "", nil, nil, status.Errorf(codes.InvalidArgument, "Queries in %s have mismatched column count; query 1 has %d column, query %d has %d columns", fullOpName, len(items), i+2, len(items2)) } // check the result items match to the first query's items for j := range items { if !compareValueType(items[j].ValueType, items2[j].ValueType) { return "", nil, nil, status.Errorf(codes.InvalidArgument, "Column %d in %s has incompatible types: %s, %s", j+1, fullOpName, items[j].ValueType, items2[j].ValueType) } } ss = append(ss, fmt.Sprintf("SELECT * FROM (%s)", s)) data = append(data, d...) } // Note: OrderBy/Limit are not part of ast.CompoundQuery // They are only available in ast.Query wrapper. If this CompoundQuery // has OrderBy/Limit, it would be wrapped in ast.Query and handled there. query := strings.Join(ss, fmt.Sprintf(" %s ", op)) return query, data, items, nil } func (b *QueryBuilder) buildUpdate(up *ast.Update) (string, []interface{}, error) { t, err := b.db.useTableExclusive(up.TableName.Idents[0].Name, b.tx) if err != nil { if status.Code(err) == codes.NotFound { return "", nil, status.Error(codes.InvalidArgument, err.Error()) } return "", nil, err } view := t.TableView() b.rootView = view // build FROM var fromClause string var tableAliiasName string if up.As == nil { fromClause = QuoteString(t.Name) tableAliiasName = t.Name if err := b.registerTableAlias(view, t.Name); err != nil { return "", nil, err } } else { fromClause = fmt.Sprintf("%s AS %s", QuoteString(t.Name), QuoteString(up.As.Alias.Name)) tableAliiasName = up.As.Alias.Name if err := b.registerTableAlias(view, up.As.Alias.Name); err != nil { return "", nil, err } } // build SET var setClause []string usedPaths := make(map[string]struct{}, len(up.Updates)) for _, item := range up.Updates { var fullPathName string var normalizedFullPathName string var path Expr if len(item.Path) == 1 { ex, err := b.buildExpr(item.Path[0]) if err != nil { return "", nil, wrapExprError(err, item.DefaultExpr.Expr, "Updates") } path = ex fullPathName = item.Path[0].Name normalizedFullPathName = item.Path[0].Name } else { ex, err := b.buildExpr(&ast.Path{Idents: item.Path}) if err != nil { return "", nil, wrapExprError(err, item.DefaultExpr.Expr, "Updates") } path = ex // generate full path name paths := make([]string, len(item.Path)) for i := range item.Path { paths[i] = item.Path[i].Name } fullPathName = strings.Join(paths, ".") // if the first path is same to table alias name, normalize the full path name if paths[0] == tableAliiasName { normalizedFullPathName = strings.Join(paths[1:], ".") } else { normalizedFullPathName = fullPathName } } // check duplicate path in set clause if _, ok := usedPaths[normalizedFullPathName]; ok { return "", nil, status.Errorf(codes.InvalidArgument, "Update item %s assigned more than once", fullPathName) } usedPaths[normalizedFullPathName] = struct{}{} value, err := b.buildExpr(item.DefaultExpr.Expr) if err != nil { return "", nil, wrapExprError(err, item.DefaultExpr.Expr, "Updates") } b.args = append(b.args, value.Args...) setClause = append(setClause, fmt.Sprintf("%s = %s", path.Raw, value.Raw)) } // build WHERE whereClause, data, err := b.buildQueryWhereClause(up.Where) if err != nil { return "", nil, err } b.args = append(b.args, data...) query := fmt.Sprintf(`UPDATE %s SET %s WHERE %s`, fromClause, strings.Join(setClause, ", "), whereClause) return query, b.args, nil } func (b *QueryBuilder) buildInsert(up *ast.Insert) (string, []interface{}, error) { t, err := b.db.useTableExclusive(up.TableName.Idents[0].Name, b.tx) if err != nil { if status.Code(err) == codes.NotFound { return "", nil, status.Error(codes.InvalidArgument, err.Error()) } return "", nil, err } view := t.TableView() b.rootView = view tableName := t.Name var columns []string for _, c := range up.Columns { columns = append(columns, c.Name) } // Check columns exist in the table for _, c := range columns { if _, err := t.getColumn(c); err != nil { return "", nil, status.Errorf(codes.InvalidArgument, "Column %s is not present in table %s", c, tableName) } } // Ensure multiple values are not specified usedColumns := make(map[string]struct{}, len(columns)) for _, name := range columns { if _, ok := usedColumns[name]; ok { return "", nil, status.Errorf(codes.InvalidArgument, "INSERT has columns with duplicate name: %s", name) } usedColumns[name] = struct{}{} } // Check not nullable columns if exist, nonNullables := t.NonNullableAndNonGeneratedColumnsExist(columns); exist { columns := strings.Join(nonNullables, ", ") return "", nil, status.Errorf(codes.FailedPrecondition, "A new row in table %s does not specify a non-null value for these NOT NULL columns: %s", tableName, columns, ) } var values string switch input := up.Input.(type) { case *ast.ValuesInput: var rows []string for _, row := range input.Rows { if len(row.Exprs) != len(columns) { return "", nil, status.Errorf(codes.InvalidArgument, "Inserted row has wrong column count; Has %d, expected %d", len(row.Exprs), len(columns), ) } var values []string for _, e := range row.Exprs { if e.Default { values = append(values, "NULL") continue } ex, err := b.buildExpr(e.Expr) if err != nil { return "", nil, wrapExprError(err, e.Expr, "INSERT") } b.args = append(b.args, ex.Args...) values = append(values, ex.Raw) } rows = append(rows, fmt.Sprintf("(%s)", strings.Join(values, ", "))) } values = fmt.Sprintf("VALUES %s", strings.Join(rows, ", ")) case *ast.SubQueryInput: query, data, items, err := BuildQuery(b.db, b.tx, input.Query, b.params, false) if err != nil { return "", nil, fmt.Errorf("Subquery error: %v", err) } b.args = append(b.args, data...) if len(items) != len(columns) { return "", nil, status.Errorf(codes.InvalidArgument, "Inserted row has wrong column count; Has %d, expected %d", len(items), len(columns), ) } values = query } query := fmt.Sprintf(`INSERT INTO %s (%s) %s`, QuoteString(tableName), strings.Join(QuoteStringSlice(columns), ", "), values) return query, b.args, nil } func (b *QueryBuilder) buildDelete(up *ast.Delete) (string, []interface{}, error) { t, err := b.db.useTableExclusive(up.TableName.Idents[0].Name, b.tx) if err != nil { if status.Code(err) == codes.NotFound { return "", nil, status.Error(codes.InvalidArgument, err.Error()) } return "", nil, err } view := t.TableView() b.rootView = view // build FROM var fromClause string if up.As == nil { fromClause = QuoteString(t.Name) if err := b.registerTableAlias(view, t.Name); err != nil { return "", nil, err } } else { fromClause = fmt.Sprintf("%s AS %s", QuoteString(t.Name), QuoteString(up.As.Alias.Name)) if err := b.registerTableAlias(view, up.As.Alias.Name); err != nil { return "", nil, err } } // build WHERE whereClause, data, err := b.buildQueryWhereClause(up.Where) if err != nil { return "", nil, err } b.args = append(b.args, data...) query := fmt.Sprintf(`DELETE FROM %s WHERE %s`, fromClause, whereClause) return query, b.args, nil } func (b *QueryBuilder) buildQueryTable(exp ast.TableExpr) (*TableView, string, []interface{}, error) { switch src := exp.(type) { case *ast.TableName: t, err := b.db.useTable(src.Table.Name, b.tx) if err != nil { if status.Code(err) == codes.NotFound { return nil, "", nil, status.Error(codes.InvalidArgument, err.Error()) } return nil, "", nil, err } query := QuoteString(t.Name) alias := t.Name if src.As != nil { alias = src.As.Alias.Name query = fmt.Sprintf("%s AS %s", QuoteString(t.Name), QuoteString(alias)) } else if schema, ok := metaTablesReverseMap[t.Name]; ok { alias = schema[1] query = fmt.Sprintf("%s AS %s", QuoteString(t.Name), QuoteString(alias)) } view := t.TableViewWithAlias(alias) if err := b.registerTableAlias(view, alias); err != nil { return nil, "", nil, err } return view, query, nil, nil case *ast.PathTableExpr: // PathTableExpr is used for tables that might be in a schema or implicit UNNEST if len(src.Path.Idents) == 0 { return nil, "", nil, status.Error(codes.InvalidArgument, "Empty path in table expression") } // Build the full path string to check for INFORMATION_SCHEMA tables pathNames := make([]string, len(src.Path.Idents)) for i, ident := range src.Path.Idents { pathNames[i] = ident.Name } fullPath := strings.Join(pathNames, ".") // Check if this is a meta table (like INFORMATION_SCHEMA.SCHEMATA) var tableName string if specialTableName, ok := metaTablesMap[fullPath]; ok { tableName = specialTableName } else { // Fall back to using the last identifier as table name tableName = src.Path.Idents[len(src.Path.Idents)-1].Name } t, err := b.db.useTable(tableName, b.tx) if err != nil { if status.Code(err) == codes.NotFound { return nil, "", nil, status.Error(codes.InvalidArgument, err.Error()) } return nil, "", nil, err } query := QuoteString(t.Name) alias := t.Name if src.As != nil { alias = src.As.Alias.Name query = fmt.Sprintf("%s AS %s", QuoteString(t.Name), QuoteString(alias)) } else if schema, ok := metaTablesReverseMap[t.Name]; ok { alias = schema[1] query = fmt.Sprintf("%s AS %s", QuoteString(t.Name), QuoteString(alias)) } view := t.TableViewWithAlias(alias) if err := b.registerTableAlias(view, alias); err != nil { return nil, "", nil, err } return view, query, nil, nil case *ast.Join: return b.buildJoinedView(src) case *ast.Unnest: // TODO: fix this hack // memefish handles _SCHEMA_._TABLE_ table name as unnest. // So if the unnest path is the same to _SCHEMA_._TABLE_ it is handled as table name. if path, ok := src.Expr.(*ast.Path); ok { names := make([]string, len(path.Idents)) for i := range path.Idents { names[i] = path.Idents[i].Name } tableName := strings.ToUpper(strings.Join(names, ".")) if specialTableName, ok := metaTablesMap[tableName]; ok { return b.buildQueryTable(&ast.TableName{ Table: &ast.Ident{Name: specialTableName}, As: src.As, }) } } return b.buildUnnestView(src) case *ast.SubQueryTableExpr: query, data, items, err := BuildQuery(b.db, b.tx, src.Query, b.params, false) if err != nil { return nil, "", nil, fmt.Errorf("Subquery error: %v", err) } view := createTableViewFromItems(items, nil) var viewName string if src.As == nil { viewName = fmt.Sprintf("__SUBQUERY%d", b.subqueryViewNum) b.subqueryViewNum++ } else { viewName = src.As.Alias.Name if err := b.registerTableAlias(view, viewName); err != nil { return nil, "", nil, err } } return view, fmt.Sprintf("(%s) AS %s", query, viewName), data, nil case *ast.ParenTableExpr: view, q, d, err := b.buildQueryTable(src.Source) if err != nil { return nil, "", nil, err } return view, fmt.Sprintf("(%s)", q), d, nil default: return nil, "", nil, status.Errorf(codes.Unknown, "unknown expression %T for FROM", src) } } func (b *QueryBuilder) buildJoinedView(src *ast.Join) (*TableView, string, []interface{}, error) { leftView, leftQuery, leftData, err := b.buildQueryTable(src.Left) if err != nil { return nil, "", nil, fmt.Errorf("left table error %v", err) } if leftView == nil { return nil, "", nil, fmt.Errorf("left table view is nil") } rightView, rightQuery, rightData, err := b.buildQueryTable(src.Right) if err != nil { return nil, "", nil, fmt.Errorf("right table error %v", err) } if rightView == nil { return nil, "", nil, fmt.Errorf("right table view is nil") } leftItems := leftView.AllItems() rightItems := rightView.AllItems() var condData []interface{} var condition string var usingColumns []string switch cond := src.Cond.(type) { case *ast.On: switch expr := cond.Expr.(type) { case *ast.BinaryExpr: ex, err := b.buildExpr(expr) if err != nil { return nil, "", nil, wrapExprError(err, expr, "ON") } condition = fmt.Sprintf("ON %s", ex.Raw) condData = append(condData, ex.Args...) default: return nil, "", nil, status.Errorf(codes.Unimplemented, "not supported expression %T for JOIN condition", expr) } case *ast.Using: names := make([]string, len(cond.Idents)) for i := range cond.Idents { name := cond.Idents[i].Name if _, ok := leftView.ResultItemsMap[name]; !ok { return nil, "", nil, newExprErrorf(nil, true, "Column %s in USING clause not found on left side of join", name) } if _, ok := rightView.ResultItemsMap[name]; !ok { return nil, "", nil, newExprErrorf(nil, true, "Column %s in USING clause not found on right side of join", name) } names[i] = name } usingColumns = names // used for FULL OUTER JOIN condition = fmt.Sprintf("USING (%s)", strings.Join(QuoteStringSlice(names), ", ")) // filter ResultItems that used in USING columns from right view var newRightItems []ResultItem for i := range rightItems { var found bool for _, name := range names { if rightItems[i].Name == name { found = true break } } if !found { newRightItems = append(newRightItems, rightItems[i]) } } rightItems = newRightItems } switch src.Op { case ast.CommaJoin, ast.CrossJoin, ast.InnerJoin, ast.LeftOuterJoin: newView := createTableViewFromItems(leftItems, rightItems) data := append(append(leftData, rightData...), condData...) return newView, fmt.Sprintf("%s %s %s %s", leftQuery, src.Op, rightQuery, condition), data, nil case ast.RightOuterJoin: // RIGHT OUTER JOIN is not supported by sqlite // Reverse left and right, then use LEFT OUTER JOIN to simulate RIGHT OUTER JOIN newView := createTableViewFromItems(leftItems, rightItems) data := append(append(rightData, leftData...), condData...) return newView, fmt.Sprintf("%s LEFT OUTER JOIN %s %s", rightQuery, leftQuery, condition), data, nil case ast.FullOuterJoin: // FULL OUTER JOIN is not supported by sqlite // Use LEFT OUTER JOIN and RIGHT OUTER JOIN, then UNION ALL to simulate FULL OUTER JOIN // Ref. https://www.sqlitetutorial.net/sqlite-full-outer-join/ if len(usingColumns) == 0 { msg := "hanndy-spanner: Full Outer Join with ON condition is not supported yet." return nil, "", nil, status.Errorf(codes.Unknown, msg) } var leftUsingColumns []string var leftRemainingColumns []string for _, item := range leftView.AllItems() { var found bool for _, name := range usingColumns { if item.Name == name { found = true break } } if found { leftUsingColumns = append(leftUsingColumns, item.Expr.Raw) } else { leftRemainingColumns = append(leftRemainingColumns, item.Expr.Raw) } } var rightUsingColumns []string var rightRemainingColumns []string for _, item := range rightView.AllItems() { var found bool for _, name := range usingColumns { if item.Name == name { found = true break } } if found { rightUsingColumns = append(rightUsingColumns, item.Expr.Raw) } else { rightRemainingColumns = append(rightRemainingColumns, item.Expr.Raw) } } remainingColumns := append(leftRemainingColumns, rightRemainingColumns...) var secondWhere []string for _, c := range leftUsingColumns { secondWhere = append(secondWhere, fmt.Sprintf("%s IS NULL", c)) } first := fmt.Sprintf(`SELECT %s FROM %s LEFT JOIN %s %s`, strings.Join(append(leftUsingColumns, remainingColumns...), ", "), leftQuery, rightQuery, condition) second := fmt.Sprintf(`SELECT %s FROM %s LEFT JOIN %s %s WHERE %s`, strings.Join(append(rightUsingColumns, remainingColumns...), ", "), rightQuery, leftQuery, condition, strings.Join(secondWhere, " AND "), ) query := fmt.Sprintf("(%s UNION ALL %s)", first, second) newView := createTableViewFromItems(leftItems, rightItems) data1 := append(append(leftData, rightData...), condData...) data2 := append(append(rightData, leftData...), condData...) data := append(data1, data2...) // TODO: FullOuterJoin is simulated by using subquery with UNION, so table alias cannot be used // See the test case return newView, query, data, nil } return nil, "", nil, status.Errorf(codes.Unknown, "unknown Join operation: %v", src.Op) } // buildUnnestView creates a Tableview from an UNNEST() expression. // // buildUnnestExpr creates a table like `VALUES (a), (b), (c)`, but the columns are unnamed. // If aliases for unnested values or offsets are used, they can be specified by the name. // e.g. SELECT MAX(x), MAX(y) FROM UNNEST() AS x OFFSET WITH AS y // // See the doc comment of buildUnnestExpr about the generated query. func (b *QueryBuilder) buildUnnestView(src *ast.Unnest) (*TableView, string, []interface{}, error) { useSqliteJSON() var offset bool var offsetAlias string if src.WithOffset != nil { offset = true if src.WithOffset.As != nil { offsetAlias = src.WithOffset.As.Alias.Name } } s, err := b.buildUnnestExpr(src.Expr, offset, true) if err != nil { return nil, "", nil, wrapExprError(err, src.Expr, "UNNEST") } viewName := fmt.Sprintf("__UNNEST%d", b.unnestViewNum) b.unnestViewNum++ // If alias is specified, it names unnested column itemName := "" if src.As != nil { itemName = src.As.Alias.Name } items := []ResultItem{ { Name: itemName, ValueType: *s.ValueType.ArrayType, Expr: Expr{ Raw: fmt.Sprintf("%s.value", viewName), // implicitly named column1 ValueType: *s.ValueType.ArrayType, }, }, } if offset { items = append(items, ResultItem{ Name: offsetAlias, ValueType: ValueType{Code: TCInt64}, Expr: Expr{ Raw: fmt.Sprintf("%s.offset", viewName), ValueType: ValueType{Code: TCInt64}, }, }) } view := createTableViewFromItems(items, nil) raw := fmt.Sprintf("(%s) AS %s", s.Raw, viewName) // if the element type is struct, it needs to be expanded // otherwise return as is if s.ValueType.ArrayType.Code != TCStruct { return view, raw, s.Args, nil } viewItems := view.AllItems() st := viewItems[0] strItems := st.ValueType.StructType.AllItems() var newItems []ResultItem for i := range strItems { newItems = append(newItems, ResultItem{ Name: strItems[i].Name, ValueType: strItems[i].ValueType, Expr: Expr{ Raw: strItems[i].Expr.Raw, ValueType: strItems[i].Expr.ValueType, Args: strItems[i].Expr.Args, }, }) } var exprs []string for i, item := range newItems { if item.Name == "" { exprs = append(exprs, fmt.Sprintf("JSON_EXTRACT(%s, '$.values[%d]')", "value", i)) } else { exprs = append(exprs, fmt.Sprintf("JSON_EXTRACT(%s, '$.values[%d]') AS %s", "value", i, item.Name)) } } if src.As != nil { viewName := src.As.Alias.Name view2 := createTableViewFromItems(newItems, nil) if err := b.registerTableAlias(view2, viewName); err != nil { return nil, "", nil, err } } var offsetItem []ResultItem if offset { offsetItem = []ResultItem{ { Name: viewItems[1].Name, ValueType: viewItems[1].ValueType, Expr: Expr{ Raw: viewItems[1].Name, ValueType: viewItems[1].Expr.ValueType, Args: viewItems[1].Expr.Args, }, }, } if viewItems[1].Name == "" { exprs = append(exprs, viewItems[1].Expr.Raw) } else { exprs = append(exprs, fmt.Sprintf("%s AS %s", viewItems[1].Expr.Raw, viewItems[1].Name)) } } view3 := createTableViewFromItems(newItems, offsetItem) selectQuery := strings.Join(exprs, ", ") return view3, fmt.Sprintf("(SELECT %s FROM %s)", selectQuery, raw), s.Args, nil } func (b *QueryBuilder) buildResultSet(selectItems []ast.SelectItem) ([]ResultItem, string, error) { var data []interface{} n := len(selectItems) if b.rootView != nil { n += len(b.rootView.ResultItems) } items := make([]ResultItem, 0, n) exprs := make([]string, 0, len(selectItems)) for _, item := range selectItems { switch i := item.(type) { case *ast.Star: if b.rootView == nil { return nil, "", status.Errorf(codes.InvalidArgument, `SELECT * must have a FROM clause`) } items = append(items, b.rootView.AllItems()...) exprs = append(exprs, "*") case *ast.DotStar: ex, err := b.buildExpr(i.Expr) if err != nil { return nil, "", wrapExprError(err, i.Expr, "DotStar") } data = append(data, ex.Args...) if ex.ValueType.Code != TCStruct { return nil, "", status.Errorf(codes.InvalidArgument, "Dot-star is not supported for type %s", ex.ValueType) } st := ex.ValueType.StructType items = append(items, st.AllItems()...) if st.IsTable { exprs = append(exprs, fmt.Sprintf("%s.*", ex.Raw)) } else { useSqliteJSON() n := len(st.FieldTypes) for i := 0; i < n; i++ { exprs = append(exprs, fmt.Sprintf("JSON_EXTRACT(%s, '$.values[%d]')", ex.Raw, i)) } } case *ast.ExprSelectItem: var alias string switch e := i.Expr.(type) { case *ast.Ident: alias = e.Name case *ast.Path: alias = e.Idents[len(e.Idents)-1].Name } ex, err := b.buildExpr(i.Expr) if err != nil { return nil, "", wrapExprError(err, i.Expr, "Path") } data = append(data, ex.Args...) items = append(items, ResultItem{ Name: alias, ValueType: ex.ValueType, Expr: Expr{ // This is a trick. This Expr is referred as subquery. // At the reference, it should be just referred as alias name instead of s.Raw. Raw: QuoteString(alias), ValueType: ex.ValueType, }, }) exprs = append(exprs, ex.Raw) case *ast.Alias: alias := i.As.Alias.Name ex, err := b.buildExpr(i.Expr) if err != nil { return nil, "", wrapExprError(err, i.Expr, "Alias") } data = append(data, ex.Args...) items = append(items, ResultItem{ Name: alias, ValueType: ex.ValueType, Expr: Expr{ // This is a trick. This Expr is referred as subquery. // At the reference, it should be just referred as alias name instead of s.Raw. Raw: QuoteString(alias), ValueType: ex.ValueType, }, }) exprs = append(exprs, fmt.Sprintf("%s AS %s", ex.Raw, QuoteString(alias))) default: return nil, "", status.Errorf(codes.Unimplemented, "not supported %T in result set", item) } } b.args = append(b.args, data...) seletQuery := strings.Join(exprs, ", ") return items, seletQuery, nil } func (b *QueryBuilder) buildQuery(stmt *ast.Select) (string, error) { var whereClause string if stmt.Where != nil { s, data, err := b.buildQueryWhereClause(stmt.Where) if err != nil { return "", err } whereClause = s b.args = append(b.args, data...) } var groupByClause string if stmt.GroupBy != nil { s, data, err := b.buildQueryGroupByClause(stmt.GroupBy) if err != nil { return "", err } groupByClause = s b.args = append(b.args, data...) } var havingClause string if stmt.Having != nil { ex, err := b.buildExpr(stmt.Having.Expr) if err != nil { return "", wrapExprError(err, stmt.Having.Expr, "Having") } havingClause = ex.Raw b.args = append(b.args, ex.Args...) } var query string if whereClause != "" { query += fmt.Sprintf(" WHERE %s", whereClause) } if groupByClause != "" { query += fmt.Sprintf(" GROUP BY %s", groupByClause) } if havingClause != "" { query += fmt.Sprintf(" HAVING %s", havingClause) } // Note: OrderBy and Limit are now handled at the Query level return query, nil } func (b *QueryBuilder) buildQueryWhereClause(where *ast.Where) (string, []interface{}, error) { ex, err := b.buildExpr(where.Expr) if err != nil { return "", nil, wrapExprError(err, where.Expr, "Building WHERE clause error") } return ex.Raw, ex.Args, nil } func (b *QueryBuilder) buildQueryGroupByClause(groupby *ast.GroupBy) (string, []interface{}, error) { var groupByClause []string var args []interface{} for _, expr := range groupby.Exprs { ex, err := b.buildExpr(expr) if err != nil { return "", nil, wrapExprError(err, expr, "Building GROUP BY error") } groupByClause = append(groupByClause, ex.Raw) args = append(args, ex.Args...) } return strings.Join(groupByClause, ", "), args, nil } func (b *QueryBuilder) buildQueryOrderByClause(orderby *ast.OrderBy, view *TableView) (string, []interface{}, error) { var orderByClause []string var data []interface{} for _, item := range orderby.Items { // TODO: use b.buildExpr() var expr Expr switch e := item.Expr.(type) { case *ast.Ident: i, ambiguous, notfound := view.Get(e.Name) if notfound { return "", nil, newExprErrorf(nil, true, "Unrecognized name: %s", e.Name) } if ambiguous { return "", nil, newExprErrorf(nil, true, "Column name %s is ambiguous", e.Name) } expr = i.Expr case *ast.Path: ex, err := b.buildExpr(item.Expr) if err != nil { return "", nil, wrapExprError(err, item.Expr, "OrderBy") } expr = ex } collate := "" if item.Collate != nil { switch v := item.Collate.Value.(type) { case *ast.Param: vv, ok := b.params[v.Name] if !ok { return "", nil, fmt.Errorf("No parameter found for binding: %s", v.Name) } collate = "?" data = append(data, vv) case *ast.StringLiteral: collate = v.Value } } orderByClause = append(orderByClause, fmt.Sprintf("%s %s %s", expr.Raw, collate, item.Dir)) } return strings.Join(orderByClause, ", "), data, nil } func (b *QueryBuilder) buildQueryLimitOffset(limit *ast.Limit) (string, []interface{}, error) { var data []interface{} e, err := b.buildIntValue(limit.Count, "LIMIT") if err != nil { return "", nil, err } limitClause := e.Raw data = append(data, e.Args...) if limit.Offset != nil { e, err := b.buildIntValue(limit.Offset.Value, "OFFSET") if err != nil { return "", nil, err } data = append(data, e.Args...) limitClause += fmt.Sprintf(" OFFSET %s", e.Raw) } return limitClause, data, nil } func (b *QueryBuilder) buildIntValue(intValue ast.IntValue, caller string) (Expr, error) { switch iv := intValue.(type) { case *ast.Param: ex, err := b.buildExpr(iv) if err != nil { return NullExpr, wrapExprError(err, nil, "buildIntValue") } if ex.ValueType.Code != TCInt64 { return NullExpr, newExprErrorf(nil, true, "%s expects an integer literal or parameter", caller) } return ex, nil case *ast.IntLiteral: ex, err := b.buildExpr(iv) if err != nil { return NullExpr, wrapExprError(err, nil, "buildIntValue") } if ex.ValueType.Code != TCInt64 { return NullExpr, newExprErrorf(nil, true, "%s expects an integer literal or parameter", caller) } return ex, nil case *ast.CastIntValue: // CAST expression can be used but it seems not working actually. return NullExpr, newExprErrorf(nil, true, "handy-spanner: CAST in Limit/Offset seems not working in Spanner") default: return NullExpr, fmt.Errorf("unknown LIMIT type") } } func (b *QueryBuilder) buildInCondition(cond ast.InCondition) (Expr, error) { switch c := cond.(type) { case *ast.ValuesInCondition: var ss []string var args []interface{} for _, e := range c.Exprs { ex, err := b.buildExpr(e) if err != nil { return NullExpr, wrapExprError(err, e, "IN condition") } raw := b.unkeysStruct(ex.Raw, ex.ValueType) ss = append(ss, raw) args = append(args, ex.Args...) } return Expr{ ValueType: ValueType{Code: TCBool}, Raw: "(" + strings.Join(ss, ", ") + ")", Args: args, }, nil case *ast.SubQueryInCondition: query, data, items, err := BuildQuery(b.db, b.tx, c.Query, b.params, false) if err != nil { return NullExpr, newExprErrorf(nil, false, "BuildQuery error for SubqueryInCondition: %v", err) } if len(items) != 1 { return NullExpr, newExprErrorf(nil, true, "Subquery of type IN must have only one output column") } // TODO: if subquery uses SELECT AS STRUCT, it needs to expand struct return Expr{ ValueType: items[0].ValueType, // inherit ValueType from result item Raw: fmt.Sprintf("(%s)", query), Args: data, }, nil case *ast.UnnestInCondition: s, err := b.buildUnnestExpr(c.Expr, false, false) if err != nil { return NullExpr, err } return Expr{ ValueType: ValueType{Code: TCBool}, Raw: fmt.Sprintf("(%s)", s.Raw), Args: s.Args, }, nil default: return NullExpr, fmt.Errorf("not supported InCondition %T", c) } } // buildUnnestExpr creates a table from UNNEST() expression. // // sqlite cannot handle array type and UNNEST() directly. // Array type is handled as JSON, so UNNEST canbe simulated by JSON_EACH(). // // `UNNEST([a, b, c])` in spanner results in the following expression in sqlite: // `SELECT value JSON_EACH(JSON_ARRAY((a), (b), (c))` // // When array param is specified like `UNNEST(@foo)`, it expands all elements of the array like: // `SELECT value JSON_EACH(JSON_ARRAY((?), (?) (?)))` // // If offset for unnest is needed `UNNEST([a, b, c])` generates: // `SELECT value, key as offset JSON_EACH(JSON_ARRAY(a, b, c))` func (b *QueryBuilder) buildUnnestExpr(expr ast.Expr, offset bool, asview bool) (Expr, error) { useSqliteJSON() ex, err := b.buildExpr(expr) if err != nil { return NullExpr, wrapExprError(err, expr, "UNNEST") } if ex.ValueType.Code != TCArray { if asview { msg := "Values referenced in UNNEST must be arrays. UNNEST contains expression of type %s" return NullExpr, newExprErrorf(expr, true, msg, ex.ValueType.Code) } else { msg := "Second argument of IN UNNEST must be an array but was %s" return NullExpr, newExprErrorf(expr, true, msg, ex.ValueType.Code) } } var raw string if asview { if offset { raw = fmt.Sprintf("SELECT value, key as offset FROM JSON_EACH(%s)", ex.Raw) } else { raw = fmt.Sprintf("SELECT value FROM JSON_EACH(%s)", ex.Raw) } } else { value := b.unkeysStruct("value", *ex.ValueType.ArrayType) raw = fmt.Sprintf("SELECT %s FROM JSON_EACH(%s)", value, ex.Raw) } return Expr{ ValueType: ex.ValueType, Raw: raw, Args: ex.Args, }, nil } // unkeysStruct removes "keys" from object for struct type. // This is required to compare struct values. func (b *QueryBuilder) unkeysStruct(raw string, vt ValueType) string { if vt.Code != TCStruct { return raw } return fmt.Sprintf("JSON_REMOVE(%s, '$.keys')", raw) } func (b *QueryBuilder) expandParamByPlaceholders(v Value) (Expr, error) { switch v.Data.(type) { case nil, bool, int64, float64, string, []byte: return Expr{ ValueType: v.Type, Raw: "?", Args: []interface{}{v.Data}, }, nil case []bool, []int64, []float64, []string, [][]byte: useSqliteJSON() vv := reflect.ValueOf(v.Data) n := vv.Len() placeholders := make([]string, n) args := make([]interface{}, n) for i := 0; i < n; i++ { placeholders[i] = "(?)" args[i] = vv.Index(i).Interface() } var raw string if n > 0 { raw = "JSON_ARRAY(" + strings.Join(placeholders, ", ") + ")" } return Expr{ ValueType: v.Type, // TODO: check correct type or not Raw: raw, Args: args, }, nil case ArrayValue: useSqliteJSON() rv := reflect.ValueOf(v.Data.(ArrayValue).Elements()) n := rv.Len() placeholders := make([]string, n) args := make([]interface{}, n) for i := 0; i < n; i++ { placeholders[i] = "(?)" rvv := rv.Index(i) if rvv.IsNil() { args[i] = nil } else { args[i] = rvv.Interface() } } var raw string if n > 0 { raw = "JSON_ARRAY(" + strings.Join(placeholders, ", ") + ")" } return Expr{ ValueType: v.Type, // TODO: check correct type or not Raw: raw, Args: args, }, nil } return NullExpr, fmt.Errorf("unexpected parameter type for expandParamByPlaceholders: %T", v) } func (b *QueryBuilder) accessField(expr Expr, name string) (Expr, error) { if expr.ValueType.Code != TCStruct { msg := "Cannot access field %s on a value with type %s" return NullExpr, newExprErrorf(nil, true, msg, name, expr.ValueType) } st := expr.ValueType.StructType idx := -1 for i := range st.FieldNames { if st.FieldNames[i] == name { if idx != -1 { return NullExpr, newExprErrorf(nil, true, "Column name %s is ambiguous", name) } idx = i } } if idx == -1 { if st.IsTable { // TODO: stop adhoc fix for unescape // This unescapes the expression if it is quoted exp := expr.Raw if exp[0] == '`' && exp[len(exp)-1] == '`' { exp = exp[1 : len(exp)-1] } msg := "Name %s not found inside %s" return NullExpr, newExprErrorf(nil, true, msg, name, exp) } else { msg := "Field name %s does not exist in %s" return NullExpr, newExprErrorf(nil, true, msg, name, expr.ValueType) } } var raw string if st.IsTable { raw = fmt.Sprintf("%s.%s", expr.Raw, QuoteString(name)) } else { useSqliteJSON() raw = fmt.Sprintf("JSON_EXTRACT(%s, '$.values[%d]')", expr.Raw, idx) } return Expr{ ValueType: *st.FieldTypes[idx], Raw: raw, Args: expr.Args, }, nil } func (b *QueryBuilder) buildExpr(expr ast.Expr) (Expr, error) { switch e := expr.(type) { case *ast.UnaryExpr: ex, err := b.buildExpr(e.Expr) if err != nil { return NullExpr, wrapExprError(err, expr, "Unary") } return Expr{ ValueType: ex.ValueType, Raw: fmt.Sprintf("%s %s", e.Op, ex.Raw), Args: ex.Args, }, nil case *ast.BinaryExpr: left, lerr := b.buildExpr(e.Left) right, rerr := b.buildExpr(e.Right) if lerr != nil { return NullExpr, wrapExprError(lerr, expr, "Left") } if rerr != nil { return NullExpr, wrapExprError(rerr, expr, "Right") } var args []interface{} args = append(args, left.Args...) args = append(args, right.Args...) opName := map[ast.BinaryOp]string{ ast.OpLess: "Less than", ast.OpGreater: "Greater than", ast.OpLessEqual: "Less than", ast.OpGreaterEqual: "Greater than", } var vt ValueType switch e.Op { case ast.OpEqual, ast.OpNotEqual: if !isComparable(left.ValueType, right.ValueType) { return NullExpr, newExprErrorf(expr, true, "No matching signature for operator %s for argument types: %s, %s.", e.Op, left.ValueType, right.ValueType) } vt = ValueType{ Code: TCBool, } case ast.OpLess, ast.OpGreater, ast.OpLessEqual, ast.OpGreaterEqual: if !isComparable(left.ValueType, right.ValueType) { return NullExpr, newExprErrorf(expr, true, "No matching signature for operator %s for argument types: %s, %s.", e.Op, left.ValueType, right.ValueType) } if left.ValueType.Code == TCStruct { msg := "%s is not defined for arguments of type %s" return NullExpr, newExprErrorf(expr, true, msg, opName[e.Op], left.ValueType) } vt = ValueType{ Code: TCBool, } case ast.OpOr, ast.OpAnd: vt = ValueType{ Code: TCBool, } case ast.OpLike, ast.OpNotLike: vt = ValueType{ Code: TCBool, } case ast.OpBitOr, ast.OpBitXor, ast.OpBitAnd, ast.OpBitLeftShift, ast.OpBitRightShift: vt = ValueType{ Code: TCInt64, // TODO } case ast.OpAdd, ast.OpSub, ast.OpMul: code := TCInt64 if left.ValueType.Code == TCFloat64 || right.ValueType.Code == TCFloat64 { code = TCFloat64 } vt = ValueType{ Code: code, } case ast.OpDiv: left.Raw = fmt.Sprintf("CAST(%s AS REAL)", left.Raw) vt = ValueType{ Code: TCFloat64, } default: return NullExpr, fmt.Errorf("%T: unknown op %v", e, e.Op) } raw := fmt.Sprintf("%s %s %s", left.Raw, e.Op, right.Raw) if e.Op == ast.OpBitXor { raw = fmt.Sprintf("(%s | %s) - (%s & %s)", left.Raw, right.Raw, left.Raw, right.Raw) // TODO: args } if e.Op == ast.OpEqual || e.Op == ast.OpNotEqual { leftRaw := b.unkeysStruct(left.Raw, left.ValueType) rightRaw := b.unkeysStruct(right.Raw, right.ValueType) raw = fmt.Sprintf("%s %s %s", leftRaw, e.Op, rightRaw) } return Expr{ ValueType: vt, Raw: raw, Args: args, }, nil case *ast.InExpr: left, lerr := b.buildExpr(e.Left) right, rerr := b.buildInCondition(e.Right) if lerr != nil { return NullExpr, wrapExprError(lerr, expr, "Left") } if rerr != nil { return NullExpr, wrapExprError(rerr, expr, "Right") } var args []interface{} args = append(args, left.Args...) args = append(args, right.Args...) op := "IN" if e.Not { op = "NOT IN" } leftRaw := b.unkeysStruct(left.Raw, left.ValueType) return Expr{ ValueType: ValueType{Code: TCBool}, Raw: fmt.Sprintf("%s %s %s", leftRaw, op, right.Raw), Args: args, }, nil case *ast.BetweenExpr: left, lerr := b.buildExpr(e.Left) rstart, rserr := b.buildExpr(e.RightStart) rend, reerr := b.buildExpr(e.RightEnd) if lerr != nil { return NullExpr, wrapExprError(lerr, expr, "Left") } if rserr != nil { return NullExpr, wrapExprError(rserr, expr, "RightStart") } if reerr != nil { return NullExpr, wrapExprError(reerr, expr, "RightEnd") } var args []interface{} args = append(args, left.Args...) args = append(args, rstart.Args...) args = append(args, rend.Args...) op := "BETWEEN" if e.Not { op = "NOT BETWEEN" } return Expr{ ValueType: ValueType{Code: TCBool}, Raw: fmt.Sprintf("%s %s %s AND %s", left.Raw, op, rstart.Raw, rend.Raw), Args: args, }, nil case *ast.SelectorExpr: ex, err := b.buildExpr(e.Expr) if err != nil { return NullExpr, wrapExprError(err, expr, "Expr") } ex2, err := b.accessField(ex, e.Ident.Name) if err != nil { return NullExpr, wrapExprError(err, expr, "Expr") } return Expr{ ValueType: ex2.ValueType, Raw: ex2.Raw, Args: ex2.Args, }, nil case *ast.IndexExpr: useSqliteJSON() ex1, err := b.buildExpr(e.Expr) if err != nil { return NullExpr, wrapExprError(err, expr, "Expr") } var indexExpr ast.Expr switch typ := e.Index.(type) { case *ast.ExprArg: indexExpr = typ.Expr case *ast.SubscriptSpecifierKeyword: indexExpr = typ.Expr default: return NullExpr, newExprErrorf(expr, true, "Unknown index expression: %T", typ) } ex2, err := b.buildExpr(indexExpr) if err != nil { return NullExpr, wrapExprError(err, expr, "Index") } if ex1.ValueType.Code != TCArray { msg := "Element access using [] is not supported on values of type %s" return NullExpr, newExprErrorf(expr, true, msg, ex1.ValueType) } if ex2.ValueType.Code != TCInt64 { msg := "Array position in [] must be coercible to INT64 type, but has type %s" return NullExpr, newExprErrorf(expr, true, msg, ex2.ValueType) } var args []interface{} args = append(args, ex1.Args...) args = append(args, ex2.Args...) var raw string if k, ok := e.Index.(*ast.SubscriptSpecifierKeyword); ok && k.Keyword == ast.PositionKeywordOrdinal { raw = fmt.Sprintf("JSON_EXTRACT(%s, '$[' || (%s-1) || ']')", ex1.Raw, ex2.Raw) } else { raw = fmt.Sprintf("JSON_EXTRACT(%s, '$[' || %s || ']')", ex1.Raw, ex2.Raw) } return Expr{ ValueType: *ex1.ValueType.ArrayType, Raw: raw, Args: args, }, nil case *ast.IsNullExpr: left, lerr := b.buildExpr(e.Left) if lerr != nil { return NullExpr, wrapExprError(lerr, expr, "Left") } op := "IS NULL" if e.Not { op = "IS NOT NULL" } return Expr{ ValueType: ValueType{Code: TCBool}, Raw: fmt.Sprintf("%s %s", left.Raw, op), Args: left.Args, }, nil case *ast.IsBoolExpr: left, lerr := b.buildExpr(e.Left) if lerr != nil { return NullExpr, wrapExprError(lerr, expr, "Left") } op := "IS" if e.Not { op = "IS NOT" } b := "TRUE" if e.Right { b = "FALSE" } return Expr{ ValueType: ValueType{Code: TCBool}, Raw: fmt.Sprintf("%s %s %s", left.Raw, op, b), Args: left.Args, }, nil case *ast.CallExpr: name := strings.ToUpper(e.Func.Idents[0].Name) if v, ok := customFunctionNamesMap[name]; ok { name = v } fn, ok := customFunctions[name] if !ok { return NullExpr, newExprErrorf(expr, false, "unsupported CALL function: %s", name) } // TODO: distinct // when fn.NArgs < 0, args is variadic if fn.NArgs >= 0 && fn.NArgs != len(e.Args) { return NullExpr, newExprErrorf(expr, true, "%s requires %d arguments", name, fn.NArgs) } var args []interface{} var ss []string var vts []ValueType for _, arg := range e.Args { var expr ast.Expr switch arg := arg.(type) { case *ast.ExprArg: expr = arg.Expr case *ast.SequenceArg: msg := `Unsupported query shape: SequenceArg in function call is not supported yet.` return NullExpr, newExprUnimplementedErrorf(expr, msg) default: msg := `Invalid query shape: unknown argument type was detected.` return NullExpr, newExprErrorf(expr, true, msg) } ex, err := b.buildExpr(expr) if err != nil { return NullExpr, wrapExprError(err, expr, "Args") } args = append(args, ex.Args...) ss = append(ss, ex.Raw) vts = append(vts, ex.ValueType) } if ok := fn.ArgTypes(vts); !ok { return NullExpr, newExprErrorf(expr, true, "arguments does not match for %s", name) } var distinct string if e.Distinct { distinct = "DISTINCT " } return Expr{ ValueType: fn.ReturnType(vts), Raw: fmt.Sprintf(`%s(%s%s)`, name, distinct, strings.Join(ss, ", ")), Args: args, }, nil case *ast.CountStarExpr: return Expr{ ValueType: ValueType{Code: TCInt64}, Raw: "COUNT(*)", }, nil case *ast.CastExpr: ex, err := b.buildExpr(e.Expr) if err != nil { return NullExpr, wrapExprError(err, expr, "Cast") } target := astTypeToValueType(e.Type) return castTo(ex, target) case *ast.ExtractExpr: ex, err := b.buildExpr(e.Expr) if err != nil { return NullExpr, wrapExprError(err, expr, "Paren") } args := ex.Args var raw string code := TCInt64 switch ex.ValueType.Code { case TCTimestamp: if e.AtTimeZone == nil { msg := fmt.Sprintf("handy-spanner: please specify timezone explicitly. Use %q for default timezone", defaultTimeZone) return NullExpr, newExprErrorf(expr, true, msg) } tz, err := b.buildExpr(e.AtTimeZone.Expr) if err != nil { return NullExpr, wrapExprError(err, expr, "AT TIME ZONE") } if tz.ValueType.Code != TCString { msg := "No matching signature for function EXTRACT for argument types: DATE_TIME_PART FROM TIMESTAMP AT TIME ZONE %s. Supported signatures: EXTRACT(DATE_TIME_PART FROM DATE); EXTRACT(DATE_TIME_PART FROM TIMESTAMP [AT TIME ZONE STRING])" return NullExpr, newExprErrorf(expr, true, msg, tz.ValueType) } args = append(args, tz.Args...) part := strings.ToUpper(e.Part.Name) switch part { case "NANOSECOND", "MICROSECOND", "MILLISECOND", "SECOND", "MINUTE", "HOUR", "DAYOFWEEK", "DAY", "DAYOFYEAR", "WEEK", "ISOWEEK", "MONTH", "QUARTER", "YEAR", "ISOYEAR": raw = fmt.Sprintf("___EXTRACT_FROM_TIMESTAMP(%q, %s, %s)", part, ex.Raw, tz.Raw) case "DATE": code = TCDate raw = fmt.Sprintf("DATE(%s)", ex.Raw) default: return NullExpr, newExprErrorf(expr, true, "A valid date part name is required but found %s", e.Part.Name) } case TCDate: if e.AtTimeZone != nil { return NullExpr, newExprErrorf(expr, true, "EXTRACT from DATE does not support AT TIME ZONE") } part := strings.ToUpper(e.Part.Name) switch part { case "DAYOFWEEK", "DAY", "DAYOFYEAR", "WEEK", "ISOWEEK", "MONTH", "QUARTER", "YEAR", "ISOYEAR": raw = fmt.Sprintf("___EXTRACT_FROM_DATE(%q, %s)", part, ex.Raw) case "NANOSECOND", "MICROSECOND", "MILLISECOND", "SECOND", "MINUTE", "HOUR", "DATE": return NullExpr, newExprErrorf(expr, true, "EXTRACT from DATE does not support the %s date part", part) default: return NullExpr, newExprErrorf(expr, true, "A valid date part name is required but found %s", e.Part.Name) } default: return NullExpr, newExprErrorf(expr, true, "EXTRACT does not support literal %s arguments", ex.ValueType) } return Expr{ ValueType: ValueType{Code: code}, Raw: raw, Args: args, }, nil case *ast.CaseExpr: return NullExpr, newExprErrorf(expr, false, "Case not supported yet") case *ast.ParenExpr: ex, err := b.buildExpr(e.Expr) if err != nil { return NullExpr, wrapExprError(err, expr, "Paren") } return Expr{ ValueType: ex.ValueType, Raw: fmt.Sprintf("(%s)", ex.Raw), Args: ex.Args, }, nil case *ast.ScalarSubQuery: query, args, items, err := BuildQuery(b.db, b.tx, e.Query, b.params, false) if err != nil { return NullExpr, wrapExprError(err, expr, "Scalar") } if len(items) != 1 { return NullExpr, newExprErrorf(expr, true, "Scalar subquery cannot have more than one column unless using SELECT AS STRUCT to build STRUCT values") } return Expr{ ValueType: items[0].ValueType, // inherit ValueType from result item Raw: fmt.Sprintf("(%s)", query), Args: args, }, nil case *ast.ArraySubQuery: useSqliteJSON() query, args, items, err := BuildQuery(b.db, b.tx, e.Query, b.params, true) if err != nil { return NullExpr, wrapExprError(err, expr, "Array") } if len(items) != 1 { return NullExpr, newExprErrorf(expr, true, "ARRAY subquery cannot have more than one column unless using SELECT AS STRUCT to build STRUCT values") } if items[0].ValueType.Code == TCArray { msg := "Cannot use array subquery with column of type %s because nested arrays are not supported" return NullExpr, newExprErrorf(expr, true, msg, items[0].ValueType) } return Expr{ ValueType: ValueType{ Code: TCArray, ArrayType: &items[0].ValueType, }, Raw: fmt.Sprintf("(SELECT JSON_GROUP_ARRAY(%s) FROM (%s))", items[0].Expr.Raw, query), Args: args, }, nil case *ast.ExistsSubQuery: query, args, _, err := BuildQuery(b.db, b.tx, e.Query, b.params, false) if err != nil { return NullExpr, newExprErrorf(expr, false, "BuildQuery error for %T: %v", err, e) } return Expr{ ValueType: ValueType{ Code: TCBool, }, Raw: fmt.Sprintf("EXISTS(%s)", query), Args: args, }, nil case *ast.ArrayLiteral: useSqliteJSON() var args []interface{} var ss []string var vts []ValueType var nestedStrType *ast.StructType if e.Type != nil { if st, ok := e.Type.(*ast.StructType); ok { nestedStrType = st } } for i := range e.Values { // If the element is struct, need to populate the struct type. // Because only top node knows the details of the type if it's a compound type. if str, ok := e.Values[i].(*ast.TypedStructLiteral); ok && nestedStrType != nil { str.Fields = nestedStrType.Fields } ex, err := b.buildExpr(e.Values[i]) if err != nil { return NullExpr, wrapExprError(err, expr, "ArrayLiteral") } // If the element is tuple struct literal, need to populate the filed name. if _, ok := e.Values[i].(*ast.TupleStructLiteral); ok && nestedStrType != nil { if len(ex.ValueType.StructType.FieldNames) != len(nestedStrType.Fields) { return NullExpr, newExprErrorf(expr, true, "STRUCT type has %d fields but nested STRUCT type has %d fields", len(ex.ValueType.StructType.FieldNames), len(nestedStrType.Fields)) } for key, field := range nestedStrType.Fields { if field.Ident == nil { continue } ex.ValueType.StructType.FieldNames[key] = field.Ident.Name } } args = append(args, ex.Args...) ss = append(ss, ex.Raw) vts = append(vts, ex.ValueType) } vt, err := decideArrayElementsValueType(vts...) if err != nil { return NullExpr, newExprErrorf(expr, true, err.Error()) } if vt.Code == TCArray { msg := "Cannot construct array with element type %s because nested arrays are not supported" return NullExpr, newExprErrorf(expr, true, msg, vt) } // TODO: allow to use both Int64 and Float64 return Expr{ ValueType: ValueType{ Code: TCArray, ArrayType: &vt, }, Raw: fmt.Sprintf("JSON_ARRAY(%s)", strings.Join(ss, ", ")), Args: args, }, nil case *ast.TupleStructLiteral: useSqliteJSON() // TupleStructLiteral is for unnamed struct syntax like (1, 2) var names []string var vt ValueType // For tuple syntax, all fields are unnamed (empty string) for i := 0; i < len(e.Values); i++ { names = append(names, `""`) } // Initialize struct type vt = ValueType{ Code: TCStruct, StructType: &StructType{}, } namesObj := fmt.Sprintf("JSON_ARRAY(%s)", strings.Join(names, ", ")) var args []interface{} var values []string for _, v := range e.Values { ex, err := b.buildExpr(v) if err != nil { return NullExpr, wrapExprError(err, expr, "Values") } args = append(args, ex.Args...) if ex.ValueType.Code == TCStruct { msg := `Unsupported query shape: A struct value cannot be returned as a column value. Rewrite the query to flatten the struct fields in the result.` return NullExpr, newExprUnimplementedErrorf(expr, msg) } // For tuple syntax, field names are empty and types are inferred from values vt.StructType.FieldNames = append(vt.StructType.FieldNames, "") vt.StructType.FieldTypes = append(vt.StructType.FieldTypes, &ex.ValueType) values = append(values, ex.Raw) } valuesObj := fmt.Sprintf("JSON_ARRAY(%s)", strings.Join(values, ", ")) raw := fmt.Sprintf(`JSON_OBJECT("keys", %s, "values", %s)`, namesObj, valuesObj) return Expr{ Raw: raw, ValueType: vt, Args: args, }, nil case *ast.TypedStructLiteral: useSqliteJSON() var names []string var vt ValueType if len(e.Fields) != len(e.Values) { return NullExpr, newExprErrorf(expr, true, "STRUCT type has %d fields but constructor call has %d fields", len(e.Fields), len(e.Values)) } vt = astTypeToValueType(&ast.StructType{ Fields: e.Fields, }) for _, name := range vt.StructType.FieldNames { names = append(names, `"`+name+`"`) } namesObj := fmt.Sprintf("JSON_ARRAY(%s)", strings.Join(names, ", ")) var args []interface{} var values []string for i, v := range e.Values { ex, err := b.buildExpr(v) if err != nil { return NullExpr, wrapExprError(err, expr, "Values") } args = append(args, ex.Args...) if ex.ValueType.Code == TCStruct { msg := `Unsupported query shape: A struct value cannot be returned as a column value. Rewrite the query to flatten the struct fields in the result.` return NullExpr, newExprUnimplementedErrorf(expr, msg) } if !compareValueType(ex.ValueType, *vt.StructType.FieldTypes[i]) { msg := "Struct field %d has type literal %s which does not coerce to %s" return NullExpr, newExprErrorf(expr, true, msg, i+1, ex.ValueType, *vt.StructType.FieldTypes[i]) } values = append(values, ex.Raw) } valuesObj := fmt.Sprintf("JSON_ARRAY(%s)", strings.Join(values, ", ")) raw := fmt.Sprintf(`JSON_OBJECT("keys", %s, "values", %s)`, namesObj, valuesObj) return Expr{ Raw: raw, ValueType: vt, Args: args, }, nil case *ast.NullLiteral: return Expr{ ValueType: ValueType{Code: TCInt64}, Raw: "NULL", }, nil case *ast.BoolLiteral: if e.Value { return Expr{ ValueType: ValueType{Code: TCBool}, Raw: "TRUE", }, nil } return Expr{ ValueType: ValueType{Code: TCBool}, Raw: "FALSE", }, nil case *ast.FloatLiteral: return Expr{ ValueType: ValueType{Code: TCFloat64}, Raw: e.Value, }, nil case *ast.IntLiteral: n, err := strconv.ParseInt(e.Value, 0, 64) if err != nil { return NullExpr, newExprErrorf(expr, false, "unexpected format %q as int64: %v", e.Value, err) } return Expr{ ValueType: ValueType{Code: TCInt64}, Raw: strconv.FormatInt(n, 10), }, nil case *ast.StringLiteral: return Expr{ ValueType: ValueType{Code: TCString}, Raw: fmt.Sprintf("%q", e.Value), }, nil case *ast.BytesLiteral: return Expr{ ValueType: ValueType{Code: TCBytes}, Raw: `"` + string(e.Value) + `"`, }, nil case *ast.DateLiteral: t, ok := parseDateLiteral(e.Value.Value) if !ok { return NullExpr, newExprErrorf(expr, true, "Invalid DATE literal") } return Expr{ ValueType: ValueType{Code: TCDate}, Raw: `"` + t.Format("2006-01-02") + `"`, }, nil case *ast.TimestampLiteral: t, ok := parseTimestampLiteral(e.Value.Value) if !ok { return NullExpr, newExprErrorf(expr, true, "Invalid TIMESTAMP literal") } return Expr{ ValueType: ValueType{Code: TCTimestamp}, Raw: `"` + t.Format(time.RFC3339Nano) + `"`, }, nil case *ast.Param: v, ok := b.params[e.Name] if !ok { return NullExpr, newExprErrorf(expr, true, "No parameter found for binding: %s", e.Name) } return b.expandParamByPlaceholders(v) case *ast.Ident: tbl, ok := b.views[e.Name] if ok { return Expr{ ValueType: ValueType{ Code: TCStruct, StructType: tbl.ToStruct(), }, Raw: QuoteString(e.Name), }, nil } else { if b.rootView == nil { return NullExpr, newExprErrorf(expr, true, "Unrecognized name: %s", e.Name) } item, ambiguous, notfound := b.rootView.Get(e.Name) if notfound { return NullExpr, newExprErrorf(expr, true, "Unrecognized name: %s", e.Name) } if ambiguous { return NullExpr, newExprErrorf(expr, true, "Column name %s is ambiguous", e.Name) } return item.Expr, nil } case *ast.Path: firstName := e.Idents[0].Name var remains []*ast.Ident var curExpr Expr // Look tables first tbl, ok := b.views[firstName] if ok { curExpr = Expr{ ValueType: ValueType{ Code: TCStruct, StructType: tbl.ToStruct(), }, Raw: QuoteString(firstName), } } else { // If not found in tables, look the results items of root if b.rootView == nil { return NullExpr, newExprErrorf(expr, true, "Unrecognized name: %s", firstName) } item, ambiguous, notfound := b.rootView.Get(firstName) if notfound { return NullExpr, newExprErrorf(expr, true, "Unrecognized name: %s", firstName) } if ambiguous { return NullExpr, newExprErrorf(expr, true, "Column name %s is ambiguous", firstName) } curExpr = item.Expr } remains = e.Idents[1:] for _, ident := range remains { next, err := b.accessField(curExpr, ident.Name) if err != nil { return NullExpr, wrapExprError(err, expr, "Path") } curExpr = next } return curExpr, nil default: return NullExpr, newExprErrorf(expr, false, "unknown expression") } } type exprError struct { expr ast.Expr msg string invalid bool unimplemented bool } func (e *exprError) Error() string { return fmt.Sprintf("%T: %s", e.expr, e.msg) } func (e exprError) GRPCStatus() *status.Status { code := codes.Unknown if e.invalid { code = codes.InvalidArgument } if e.unimplemented { code = codes.Unimplemented } return status.New(code, e.msg) } func newExprErrorf(expr ast.Expr, invalid bool, format string, a ...interface{}) error { return &exprError{ expr: expr, msg: fmt.Sprintf(format, a...), invalid: invalid, } } func newExprUnimplementedErrorf(expr ast.Expr, format string, a ...interface{}) error { return &exprError{ expr: expr, msg: fmt.Sprintf(format, a...), unimplemented: true, } } func wrapExprError(err error, expr ast.Expr, msg string) error { exprErr, ok := err.(*exprError) if !ok { return fmt.Errorf("unknown error in wrapExprError: %v", err) } // if error is invalid or unimplemented it is invalig argument, so return it as is if exprErr.invalid || exprErr.unimplemented { return err } return newExprErrorf(expr, false, "%s, %s", msg, err.Error()) } func astTypeToValueType(astType ast.Type) ValueType { switch t := astType.(type) { case *ast.SimpleType: return ValueType{Code: astTypeToTypeCode(t.Name)} case *ast.ArrayType: vt := astTypeToValueType(t.Item) return ValueType{ Code: TCArray, ArrayType: &vt, } case *ast.StructType: names := make([]string, 0, len(t.Fields)) types := make([]*ValueType, 0, len(t.Fields)) for _, field := range t.Fields { var name string if field.Ident != nil { name = field.Ident.Name } names = append(names, name) vt := astTypeToValueType(field.Type) types = append(types, &vt) } return ValueType{ Code: TCStruct, StructType: &StructType{ FieldNames: names, FieldTypes: types, }, } } panic("unknown type") } func parseDateLiteral(s string) (time.Time, bool) { if t, err := time.ParseInLocation("2006-1-2", s, parseLocation); err == nil { return t, true } return time.Time{}, false } func parseTimestampLiteral(s string) (time.Time, bool) { // TODO: cannot parse these format // 1999-01-02 12:02:03.123456789+3 // 1999-01-02 12:02:03.123456789 UTC if t, err := time.ParseInLocation("2006-1-2 15:4:5.999999999Z07:00", s, parseLocation); err == nil { return t.UTC(), true } if t, err := time.ParseInLocation("2006-1-2 15:4:5.999999999Z07", s, parseLocation); err == nil { return t.UTC(), true } if t, err := time.ParseInLocation("2006-1-2 15:4:5.999999999", s, parseLocation); err == nil { return t.UTC(), true } if t, err := time.ParseInLocation("2006-1-2T15:4:5.999999999Z07:00", s, parseLocation); err == nil { return t.UTC(), true } if t, err := time.ParseInLocation("2006-1-2T15:4:5.999999999Z07", s, parseLocation); err == nil { return t.UTC(), true } if t, err := time.ParseInLocation("2006-1-2T15:4:5.999999999", s, parseLocation); err == nil { return t.UTC(), true } if t, err := time.ParseInLocation("2006-1-2", s, parseLocation); err == nil { return t.UTC(), true } return time.Time{}, false } func castTo(ex Expr, target ValueType) (Expr, error) { if target.Code == TCStruct { return NullExpr, newExprErrorf(nil, false, "Struct type is not supported in Cast") } raw := ex.Raw switch ex.ValueType.Code { case TCInt64: switch target.Code { case TCBool: raw = fmt.Sprintf("___CAST_INT64_TO_BOOL(%s)", raw) case TCString: raw = fmt.Sprintf("___CAST_INT64_TO_STRING(%s)", raw) case TCInt64: // do nothing case TCFloat64: raw = fmt.Sprintf("___CAST_INT64_TO_FLOAT64(%s)", raw) default: return NullExpr, newExprErrorf(nil, true, "Invalid cast from %s to %s", ex.ValueType.Code, target.Code) } case TCFloat64: switch target.Code { case TCString: raw = fmt.Sprintf("___CAST_FLOAT64_TO_STRING(%s)", raw) case TCInt64: raw = fmt.Sprintf("___CAST_FLOAT64_TO_INT64(%s)", raw) case TCFloat64: // do nothing default: return NullExpr, newExprErrorf(nil, true, "Invalid cast from %s to %s", ex.ValueType.Code, target.Code) } case TCBool: switch target.Code { case TCString: raw = fmt.Sprintf("___CAST_BOOL_TO_STRING(%s)", raw) case TCBool: // do nothing case TCInt64: raw = fmt.Sprintf("___CAST_BOOL_TO_INT64(%s)", raw) default: return NullExpr, newExprErrorf(nil, true, "Invalid cast from %s to %s", ex.ValueType, target) } case TCString: switch target.Code { case TCString: // do nothing case TCBool: raw = fmt.Sprintf("___CAST_STRING_TO_BOOL(%s)", raw) case TCInt64: raw = fmt.Sprintf("___CAST_STRING_TO_INT64(%s)", raw) case TCFloat64: raw = fmt.Sprintf("___CAST_STRING_TO_FLOAT64(%s)", raw) case TCDate: raw = fmt.Sprintf("___CAST_STRING_TO_DATE(%s)", raw) case TCTimestamp: raw = fmt.Sprintf("___CAST_STRING_TO_TIMESTAMP(%s)", raw) default: return NullExpr, newExprErrorf(nil, true, "Invalid cast from %s to %s", ex.ValueType, target) } case TCBytes: switch target.Code { case TCBytes: // do nothing case TCString: // do nothing? default: return NullExpr, newExprErrorf(nil, true, "Invalid cast from %s to %s", ex.ValueType, target) } case TCDate: switch target.Code { case TCString: raw = fmt.Sprintf("___CAST_DATE_TO_STRING(%s)", raw) case TCDate: // do nothing case TCTimestamp: raw = fmt.Sprintf("___CAST_DATE_TO_TIMESTAMP(%s)", raw) default: return NullExpr, newExprErrorf(nil, true, "Invalid cast from %s to %s", ex.ValueType, target) } case TCTimestamp: switch target.Code { case TCString: raw = fmt.Sprintf("___CAST_TIMESTAMP_TO_STRING(%s)", raw) case TCDate: raw = fmt.Sprintf("___CAST_TIMESTAMP_TO_DATE(%s)", raw) case TCTimestamp: // do nothing default: return NullExpr, newExprErrorf(nil, true, "Invalid cast from %s to %s", ex.ValueType, target) } case TCArray: if !compareValueType(ex.ValueType, target) { if ex.ValueType.Code == TCArray && target.Code == TCArray { msg := "Casting between arrays with incompatible element types is not supported: Invalid cast from %s to %s" return NullExpr, newExprErrorf(nil, true, msg, ex.ValueType, target) } return NullExpr, newExprErrorf(nil, true, "Invalid cast from %s to %s", ex.ValueType, target) } case TCStruct: if !compareValueType(ex.ValueType, target) { return NullExpr, newExprErrorf(nil, true, "Invalid cast from %s to %s", ex.ValueType, target) } return NullExpr, newExprErrorf(nil, false, "struct is not supported") } return Expr{ ValueType: target, Raw: raw, Args: ex.Args, }, nil } func isComparable(a, b ValueType) bool { t1 := a.Code t2 := b.Code switch t1 { case TCInt64: if t2 == TCInt64 || t2 == TCFloat64 { return true } return false case TCFloat64: if t2 == TCInt64 || t2 == TCFloat64 { return true } return false case TCString: if t2 == TCString { return true } if t2 == TCDate || t2 == TCTimestamp { return true } return false case TCDate: if t2 == TCDate { return true } if t2 == TCString || t2 == TCTimestamp { return true } return false case TCTimestamp: if t2 == TCTimestamp { return true } if t2 == TCDate || t2 == TCTimestamp { return true } return false } // TODO: false by default return true } func isCoerceableTo(a, b ValueType) bool { t1 := a.Code t2 := b.Code switch t1 { case TCInt64: return t2 == TCFloat64 case TCString: return t2 == TCDate || t2 == TCTimestamp } return false } func isCastableTo(a, b ValueType) bool { t1 := a.Code t2 := b.Code // See: https://cloud.google.com/spanner/docs/functions-and-operators#casting switch t1 { case TCInt64: return t2 == TCString || t2 == TCFloat64 || t2 == TCBool case TCFloat64: return t2 == TCString || t2 == TCInt64 case TCBool: return t2 == TCString || t2 == TCInt64 case TCString: return true case TCBytes: return t2 == TCString case TCDate: return t2 == TCString || t2 == TCTimestamp case TCTimestamp: return t2 == TCString || t2 == TCDate } return false } // isAsStruct checks if the SelectAs is specifically an AsStruct node func isAsStruct(as ast.SelectAs) bool { _, ok := as.(*ast.AsStruct) return ok } ================================================ FILE: server/query_test.go ================================================ // Copyright 2019 Masahiro Sano // // 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 server import ( "testing" cmp "github.com/google/go-cmp/cmp" ) func newTestQueryBuilder() *QueryBuilder { return &QueryBuilder{} } func TestQueryBuilder_ExpandParamByPlaceholders(t *testing.T) { b := newTestQueryBuilder() table := []struct { v Value ph string args []interface{} }{ { v: Value{ Data: []bool{true, false}, }, ph: "JSON_ARRAY((?), (?))", args: []interface{}{true, false}, }, { v: Value{ Data: []int64{(100), int64(101)}, }, ph: "JSON_ARRAY((?), (?))", args: []interface{}{int64(100), int64(101)}, }, { v: Value{ Data: []float64{float64(1.1), float64(1.2)}, }, ph: "JSON_ARRAY((?), (?))", args: []interface{}{float64(1.1), float64(1.2)}, }, { v: Value{ Data: []string{"aa", "bb", "cc"}, }, ph: "JSON_ARRAY((?), (?), (?))", args: []interface{}{"aa", "bb", "cc"}, }, { v: Value{ Data: [][]byte{[]byte("aa"), []byte("bb"), []byte("cc")}, }, ph: "JSON_ARRAY((?), (?), (?))", args: []interface{}{[]byte("aa"), []byte("bb"), []byte("cc")}, }, } for _, tc := range table { s, err := b.expandParamByPlaceholders(tc.v) if err != nil { t.Fatalf("unexpected error: %v", err) } if s.Raw != tc.ph { t.Errorf("expect placeholder %q, but got %q", tc.ph, s.Raw) } if diff := cmp.Diff(tc.args, s.Args); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } } } ================================================ FILE: server/server.go ================================================ // Copyright 2019 Masahiro Sano // // 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 server import ( "context" "fmt" "strings" "sync" "time" iamv1pb "cloud.google.com/go/iam/apiv1/iampb" lropb "cloud.google.com/go/longrunning/autogen/longrunningpb" adminv1pb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" spannerpb "cloud.google.com/go/spanner/apiv1/spannerpb" "github.com/cloudspannerecosystem/memefish" "github.com/cloudspannerecosystem/memefish/ast" "github.com/cloudspannerecosystem/memefish/token" rpcstatus "google.golang.org/genproto/googleapis/rpc/status" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" ) type FakeSpannerServer interface { ApplyDDL(ctx context.Context, databaseName string, stmt ast.DDL) error spannerpb.SpannerServer adminv1pb.DatabaseAdminServer lropb.OperationsServer } func NewFakeServer() FakeSpannerServer { return &server{ autoCreateDatabase: true, db: make(map[string]*database), sessions: make(map[string]*session), } } type server struct { autoCreateDatabase bool dbMu sync.RWMutex db map[string]*database sessionMu sync.RWMutex sessions map[string]*session // Embed the unimplemented Server structs to avoid having to update the server // implementation whenever there is an addition to any of the three interfaces. spannerpb.UnimplementedSpannerServer adminv1pb.UnimplementedDatabaseAdminServer lropb.UnimplementedOperationsServer } func (s *server) ApplyDDL(ctx context.Context, databaseName string, stmt ast.DDL) error { db, err := s.getOrCreateDatabase(databaseName) if err != nil { return err } return db.ApplyDDL(ctx, stmt) } // CreateDatabase implements adminv1pb.DatabaseAdminServer. func (s *server) CreateDatabase(ctx context.Context, req *adminv1pb.CreateDatabaseRequest) (*lropb.Operation, error) { ddl, err := (&memefish.Parser{ Lexer: &memefish.Lexer{ File: &token.File{FilePath: "", Buffer: req.GetCreateStatement()}, }, }).ParseDDL() if err != nil { return nil, status.Errorf(codes.InvalidArgument, "Errors parsing Spanner DDL statement: %s : %s", req.GetCreateStatement(), err) } createStmt, ok := ddl.(*ast.CreateDatabase) if !ok { return nil, status.Error(codes.InvalidArgument, "Create statement is not CREATE DATABASE") } var stmts []ast.DDL for _, s := range req.GetExtraStatements() { stmt, err := (&memefish.Parser{ Lexer: &memefish.Lexer{ File: &token.File{FilePath: "", Buffer: s}, }, }).ParseDDL() if err != nil { return nil, status.Errorf(codes.InvalidArgument, "Errors parsing Spanner DDL statement: %s : %s", s, err) } stmts = append(stmts, stmt) } databaseName := strings.Join([]string{req.GetParent(), "databases", createStmt.Name.Name}, "/") if _, err := s.createDatabase(databaseName); err != nil { return nil, err } for _, ddl := range stmts { if err := s.ApplyDDL(ctx, databaseName, ddl); err != nil { return nil, err } } // TODO: save operation resp, _ := anypb.New(&adminv1pb.Database{ Name: databaseName, State: adminv1pb.Database_READY, }) op := &lropb.Operation{ Name: fmt.Sprintf("%s/operations/_auto_%d", databaseName, time.Now().UnixNano()/1000), Metadata: resp, Done: true, Result: &lropb.Operation_Response{ Response: resp, }, } return op, nil } // Gets the state of a Cloud Spanner database. func (s *server) GetDatabase(ctx context.Context, req *adminv1pb.GetDatabaseRequest) (*adminv1pb.Database, error) { _, err := s.getDatabase(req.Name) if err != nil { return nil, err } return &adminv1pb.Database{ Name: req.Name, State: adminv1pb.Database_READY, }, nil } // Lists Cloud Spanner databases. func (s *server) ListDatabases(ctx context.Context, req *adminv1pb.ListDatabasesRequest) (*adminv1pb.ListDatabasesResponse, error) { s.dbMu.RLock() defer s.dbMu.RUnlock() // TODO: filter by parent dbs := make([]*adminv1pb.Database, 0, len(s.db)) for name := range s.db { dbs = append(dbs, &adminv1pb.Database{ Name: name, State: adminv1pb.Database_READY, }) } // TODO: respect page token return &adminv1pb.ListDatabasesResponse{Databases: dbs}, nil } func (s *server) UpdateDatabaseDdl(ctx context.Context, req *adminv1pb.UpdateDatabaseDdlRequest) (*lropb.Operation, error) { var stmts []ast.DDL for _, s := range req.Statements { stmt, err := (&memefish.Parser{ Lexer: &memefish.Lexer{ File: &token.File{FilePath: "", Buffer: s}, }, }).ParseDDL() if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid ddl %q: %v", s, err) } stmts = append(stmts, stmt) } for _, ddl := range stmts { _ = s.ApplyDDL(ctx, req.Database, ddl) } op := &lropb.Operation{ Name: "TODO:xxx", } return op, nil } func (s *server) UpdateDatabase(context.Context, *adminv1pb.UpdateDatabaseRequest) (*lropb.Operation, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: UpdateDatabase") } // DropDatabase implements adminv1pb.DatabaseAdminServer. func (s *server) DropDatabase(ctx context.Context, req *adminv1pb.DropDatabaseRequest) (*emptypb.Empty, error) { if err := s.dropDatabase(req.GetDatabase()); err != nil { return nil, err } return &emptypb.Empty{}, nil } // Returns the schema of a Cloud Spanner database as a list of formatted // DDL statements. This method does not show pending schema updates, those may // be queried using the [Operations][google.longrunning.Operations] API. func (s *server) GetDatabaseDdl(context.Context, *adminv1pb.GetDatabaseDdlRequest) (*adminv1pb.GetDatabaseDdlResponse, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: ExecuteSql") } func (s *server) SetIamPolicy(context.Context, *iamv1pb.SetIamPolicyRequest) (*iamv1pb.Policy, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: SetIamPolixy") } func (s *server) GetIamPolicy(context.Context, *iamv1pb.GetIamPolicyRequest) (*iamv1pb.Policy, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: GetIamPolicy") } func (s *server) TestIamPermissions(context.Context, *iamv1pb.TestIamPermissionsRequest) (*iamv1pb.TestIamPermissionsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: TestIamPermissions") } func (s *server) ListOperations(ctx context.Context, req *lropb.ListOperationsRequest) (*lropb.ListOperationsResponse, error) { // TODO return &lropb.ListOperationsResponse{}, nil } func (s *server) GetOperation(ctx context.Context, req *lropb.GetOperationRequest) (*lropb.Operation, error) { name := "TODO:xxx" if req.Name != name { return nil, status.Errorf(codes.NotFound, "Operation not found: %s", name) } any, _ := anypb.New(&emptypb.Empty{}) op := &lropb.Operation{ Name: "TODO:xxx", Metadata: any, Done: true, Result: &lropb.Operation_Response{ Response: any, }, } return op, nil } func (s *server) DeleteOperation(ctx context.Context, req *lropb.DeleteOperationRequest) (*emptypb.Empty, error) { return &emptypb.Empty{}, nil } func (s *server) CancelOperation(ctx context.Context, req *lropb.CancelOperationRequest) (*emptypb.Empty, error) { return &emptypb.Empty{}, nil } func (s *server) WaitOperation(ctx context.Context, req *lropb.WaitOperationRequest) (*lropb.Operation, error) { name := "TODO:xxx" if req.Name != name { return nil, status.Errorf(codes.NotFound, "Operation not found: %s", name) } any, _ := anypb.New(&emptypb.Empty{}) op := &lropb.Operation{ Name: "TODO:xxx", Metadata: any, Done: true, Result: &lropb.Operation_Response{ Response: any, }, } return op, nil } func parseDatabaseName(fullDatabaseName string) ([]string, bool) { parts := strings.Split(fullDatabaseName, "/") if len(parts) != 6 { return nil, false } if parts[0] != "projects" { return nil, false } if parts[2] != "instances" { return nil, false } if parts[4] != "databases" { return nil, false } if parts[1] == "" || parts[3] == "" || parts[5] == "" { return nil, false } return []string{parts[1], parts[3], parts[5]}, true } func (s *server) getSession(name string) (*session, error) { s.sessionMu.RLock() defer s.sessionMu.RUnlock() if !validateSessionName(name) { return nil, status.Errorf(codes.InvalidArgument, "Invalid BeginTransaction request") } session, ok := s.sessions[name] if !ok { return nil, newSpannerSessionNotFoundError(name) } return session, nil } func (s *server) createSession(db *database, dbName string) (*session, error) { s.sessionMu.Lock() defer s.sessionMu.Unlock() for i := 0; i < 3; i++ { session := newSession(db, dbName) if _, ok := s.sessions[session.Name()]; ok { continue } s.sessions[session.Name()] = session return session, nil } return nil, status.Errorf(codes.Unknown, "create session failed") } func (s *server) createDatabase(name string) (*database, error) { if _, ok := parseDatabaseName(name); !ok { return nil, status.Errorf(codes.InvalidArgument, "Invalid CreateDatabase request.") } // read lock to check database exists s.dbMu.RLock() _, ok := s.db[name] s.dbMu.RUnlock() if ok { return nil, status.Errorf(codes.AlreadyExists, "Database already exists: %s", name) } // write lock s.dbMu.Lock() defer s.dbMu.Unlock() // re-check after lock if _, ok := s.db[name]; ok { return nil, status.Errorf(codes.AlreadyExists, "Database already exists: %s", name) } db := newDatabase() s.db[name] = db return db, nil } func (s *server) getOrCreateDatabase(name string) (*database, error) { s.dbMu.RLock() db, ok := s.db[name] s.dbMu.RUnlock() if !ok { if !s.autoCreateDatabase { return nil, newSpannerDatabaseNotFoundError(name) } var err error db, err = s.createDatabase(name) if err != nil { st, ok := status.FromError(err) if ok && st.Code() == codes.AlreadyExists { return s.getOrCreateDatabase(name) } return nil, err } } return db, nil } func (s *server) getDatabase(name string) (*database, error) { s.dbMu.RLock() db, ok := s.db[name] s.dbMu.RUnlock() if !ok { return nil, newSpannerDatabaseNotFoundError(name) } return db, nil } func (s *server) dropDatabase(name string) error { if _, ok := parseDatabaseName(name); !ok { return status.Error(codes.InvalidArgument, "Invalid DropDatabase request.") } s.dbMu.RLock() db, found := s.db[name] s.dbMu.RUnlock() if !found { return nil } if err := db.Close(); err != nil { return status.Errorf(codes.Unknown, "Failed to close the database: %s", err) } s.dbMu.Lock() delete(s.db, name) s.dbMu.Unlock() return nil } func (s *server) CreateSession(ctx context.Context, req *spannerpb.CreateSessionRequest) (*spannerpb.Session, error) { _, ok := parseDatabaseName(req.Database) if !ok { return nil, status.Errorf(codes.InvalidArgument, "Invalid CreateSession request") } db, err := s.getOrCreateDatabase(req.Database) if err != nil { return nil, err } session, err := s.createSession(db, req.Database) if err != nil { return nil, err } return session.Proto(), nil } func (s *server) BatchCreateSessions(ctx context.Context, req *spannerpb.BatchCreateSessionsRequest) (*spannerpb.BatchCreateSessionsResponse, error) { _, ok := parseDatabaseName(req.Database) if !ok { return nil, status.Errorf(codes.InvalidArgument, "Invalid BatchCreateSessions request") } db, err := s.getOrCreateDatabase(req.Database) if err != nil { return nil, err } sessions := make([]*spannerpb.Session, req.SessionCount) for i := 0; i < int(req.SessionCount); i++ { s, err := s.createSession(db, req.Database) if err != nil { return nil, err } sessions[i] = s.Proto() } return &spannerpb.BatchCreateSessionsResponse{ Session: sessions, }, nil } func (s *server) GetSession(ctx context.Context, req *spannerpb.GetSessionRequest) (*spannerpb.Session, error) { session, err := s.getSession(req.Name) if err != nil { return nil, err } return session.Proto(), nil } func (s *server) ListSessions(ctx context.Context, req *spannerpb.ListSessionsRequest) (*spannerpb.ListSessionsResponse, error) { s.sessionMu.RLock() defer s.sessionMu.RUnlock() // TODO: respect page size prefix := req.Database + "/" var sessions []*spannerpb.Session for name, session := range s.sessions { if !strings.HasPrefix(name, prefix) { continue } sessions = append(sessions, session.Proto()) } return &spannerpb.ListSessionsResponse{ Sessions: sessions, }, nil } func (s *server) DeleteSession(ctx context.Context, req *spannerpb.DeleteSessionRequest) (*emptypb.Empty, error) { session, err := s.getSession(req.Name) if err != nil { return nil, err } s.sessionMu.Lock() defer s.sessionMu.Unlock() delete(s.sessions, session.Name()) return &emptypb.Empty{}, nil } func (s *server) ExecuteSql(ctx context.Context, req *spannerpb.ExecuteSqlRequest) (*spannerpb.ResultSet, error) { ctx, cancel := context.WithCancel(ctx) defer cancel() session, err := s.getSession(req.Session) if err != nil { return nil, err } tx, txCreated, err := session.GetTransactionBySelector(req.GetTransaction()) if err != nil { return nil, err } if tx.SingleUse() { tx.Done(TransactionRollbacked) msg := "DML statements may not be performed in single-use transactions, to avoid replay." return nil, status.Errorf(codes.InvalidArgument, msg) } checkAvailability := func() error { switch tx.Status() { case TransactionInvalidated: return status.Errorf(codes.FailedPrecondition, "This transaction has been invalidated by a later transaction in the same session.") case TransactionCommited, TransactionRollbacked: return status.Errorf(codes.FailedPrecondition, "Cannot start a read or query within a transaction after Commit() or Rollback() has been called.") case TransactionAborted: return status.Errorf(codes.Aborted, "transaction aborted") } return status.Errorf(codes.Unknown, "unknown status") } rollbackCreatedTx := func() { if txCreated { tx.Done(TransactionAborted) } } if !tx.Available() { return nil, checkAvailability() } if !tx.ReadWrite() { msg := "DML statements can only be performed in a read-write transaction." return nil, status.Errorf(codes.FailedPrecondition, msg) } if req.Sql == "" { return nil, status.Error(codes.InvalidArgument, "Invalid ExecuteSql request.") } result, err := s.executeDML(ctx, session, tx, &spannerpb.ExecuteBatchDmlRequest_Statement{ Sql: req.GetSql(), Params: req.GetParams(), ParamTypes: req.GetParamTypes(), }) if err != nil { rollbackCreatedTx() if !tx.Available() { return nil, checkAvailability() } return nil, err } if txCreated { result.Metadata = &spannerpb.ResultSetMetadata{ Transaction: tx.Proto(), } } return result, nil } func (s *server) ExecuteStreamingSql(req *spannerpb.ExecuteSqlRequest, stream spannerpb.Spanner_ExecuteStreamingSqlServer) error { receivedAt := time.Now().UTC() ctx, cancel := context.WithCancel(stream.Context()) defer cancel() session, err := s.getSession(req.Session) if err != nil { return err } tx, txCreated, err := session.GetTransactionBySelector(req.GetTransaction()) if err != nil { return err } checkAvailability := func() error { switch tx.Status() { case TransactionInvalidated: return status.Errorf(codes.FailedPrecondition, "This transaction has been invalidated by a later transaction in the same session.") case TransactionCommited, TransactionRollbacked: return status.Errorf(codes.FailedPrecondition, "Cannot start a read or query within a transaction after Commit() or Rollback() has been called.") case TransactionAborted: return status.Errorf(codes.Aborted, "transaction aborted") } return status.Errorf(codes.Unknown, "unknown status") } rollbackCreatedTx := func() { if txCreated { tx.Done(TransactionAborted) } } if !tx.Available() { return checkAvailability() } if tx.SingleUse() { go func(ctx context.Context) { // make sure to call tx.Done() after the request finished <-ctx.Done() tx.Done(TransactionCommited) }(stream.Context()) } if req.Sql == "" { return status.Error(codes.InvalidArgument, "Invalid ExecuteStreamingSql request.") } stmt, err := (&memefish.Parser{ Lexer: &memefish.Lexer{ File: &token.File{FilePath: "", Buffer: req.Sql}, }, }).ParseQuery() if err != nil { return status.Errorf(codes.InvalidArgument, "Syntax error: %q: %v", req.Sql, err) } fields := req.GetParams().GetFields() paramTypes := req.ParamTypes params := make(map[string]Value, len(fields)) defaultType := &spannerpb.Type{Code: spannerpb.TypeCode_INT64} for key, val := range fields { typ := defaultType if paramTypes != nil { if t, ok := paramTypes[key]; ok { typ = t } } v, err := makeValueFromSpannerValue(key, val, typ) if err != nil { return err } params[key] = v } iter, err := session.database.Query(ctx, tx, stmt, params) if err != nil { rollbackCreatedTx() if !tx.Available() { return checkAvailability() } return err } stats := queryStats{ Mode: req.QueryMode, ReceivedAt: receivedAt, QueryText: req.Sql, } if err := sendResult(stream, tx, iter, txCreated, stats); err != nil { if !tx.Available() { return checkAvailability() } return err } return nil } func (s *server) ExecuteBatchDml(ctx context.Context, req *spannerpb.ExecuteBatchDmlRequest) (*spannerpb.ExecuteBatchDmlResponse, error) { ctx, cancel := context.WithCancel(ctx) defer cancel() session, err := s.getSession(req.Session) if err != nil { return nil, err } tx, txCreated, err := session.GetTransactionBySelector(req.GetTransaction()) if err != nil { return nil, err } if tx.SingleUse() { tx.Done(TransactionRollbacked) msg := "DML statements may not be performed in single-use transactions, to avoid replay." return nil, status.Errorf(codes.InvalidArgument, msg) } checkAvailability := func() error { switch tx.Status() { case TransactionInvalidated: return status.Errorf(codes.FailedPrecondition, "This transaction has been invalidated by a later transaction in the same session.") case TransactionCommited, TransactionRollbacked: return status.Errorf(codes.FailedPrecondition, "Cannot start a read or query within a transaction after Commit() or Rollback() has been called.") case TransactionAborted: return status.Errorf(codes.Aborted, "transaction aborted") } return status.Errorf(codes.Unknown, "unknown status") } if !tx.Available() { return nil, checkAvailability() } if !tx.ReadWrite() { msg := "DML statements can only be performed in a read-write transaction." return nil, status.Errorf(codes.FailedPrecondition, msg) } if len(req.Statements) == 0 { msg := "No statements in batch DML request." return nil, status.Errorf(codes.InvalidArgument, msg) } var resultSets []*spannerpb.ResultSet var resultStatus rpcstatus.Status for i, stmt := range req.Statements { result, err := s.executeDML(ctx, session, tx, stmt) if err != nil { // TODO: confirm to require rollback transaction in partial success if !tx.Available() { return nil, checkAvailability() } st := status.Convert(err) resultStatus.Code = int32(st.Code()) resultStatus.Message = fmt.Sprintf("Statement %d: %s", i, st.Message()) break } resultSets = append(resultSets, result) } if txCreated { if len(resultSets) > 0 { resultSets[0].Metadata = &spannerpb.ResultSetMetadata{ Transaction: tx.Proto(), } } } return &spannerpb.ExecuteBatchDmlResponse{ ResultSets: resultSets, Status: &resultStatus, }, nil } func (s *server) executeDML(ctx context.Context, session *session, tx *transaction, stmt *spannerpb.ExecuteBatchDmlRequest_Statement) (*spannerpb.ResultSet, error) { dml, err := (&memefish.Parser{ Lexer: &memefish.Lexer{ File: &token.File{FilePath: "", Buffer: stmt.Sql}, }, }).ParseDML() if err != nil { return nil, status.Errorf(codes.InvalidArgument, "%q is not valid DML: %v", stmt.Sql, err) } fields := stmt.GetParams().GetFields() paramTypes := stmt.ParamTypes params := make(map[string]Value, len(fields)) defaultType := &spannerpb.Type{Code: spannerpb.TypeCode_INT64} for key, val := range fields { typ := defaultType if paramTypes != nil { if t, ok := paramTypes[key]; ok { typ = t } } v, err := makeValueFromSpannerValue(key, val, typ) if err != nil { return nil, err } params[key] = v } count, err := session.database.Execute(ctx, tx, dml, params) if err != nil { return nil, err } return &spannerpb.ResultSet{ Stats: &spannerpb.ResultSetStats{ RowCount: &spannerpb.ResultSetStats_RowCountExact{ RowCountExact: count, }, }, }, nil } func (s *server) Read(ctx context.Context, req *spannerpb.ReadRequest) (*spannerpb.ResultSet, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: Read") } func (s *server) StreamingRead(req *spannerpb.ReadRequest, stream spannerpb.Spanner_StreamingReadServer) error { receivedAt := time.Now().UTC() ctx, cancel := context.WithCancel(stream.Context()) defer cancel() session, err := s.getSession(req.Session) if err != nil { return err } tx, txCreated, err := session.GetTransactionBySelector(req.GetTransaction()) if err != nil { return err } checkAvailability := func() error { switch tx.Status() { case TransactionInvalidated: return status.Errorf(codes.FailedPrecondition, "This transaction has been invalidated by a later transaction in the same session.") case TransactionCommited, TransactionRollbacked: return status.Errorf(codes.FailedPrecondition, "Cannot start a read or query within a transaction after Commit() or Rollback() has been called.") case TransactionAborted: return status.Errorf(codes.Aborted, "transaction aborted") } return status.Errorf(codes.Unknown, "unknown status") } rollbackCreatedTx := func() { if txCreated { tx.Done(TransactionAborted) } } if !tx.Available() { return checkAvailability() } if tx.SingleUse() { go func(ctx context.Context) { // make sure to call tx.Done() after the request finished <-ctx.Done() tx.Done(TransactionCommited) }(stream.Context()) } iter, err := session.database.Read(ctx, tx, req.Table, req.Index, req.Columns, makeKeySet(req.KeySet), req.Limit) if err != nil { rollbackCreatedTx() if !tx.Available() { return checkAvailability() } return err } stats := queryStats{ Mode: spannerpb.ExecuteSqlRequest_NORMAL, ReceivedAt: receivedAt, QueryText: "", } if err := sendResult(stream, tx, iter, txCreated, stats); err != nil { if !tx.Available() { return checkAvailability() } return err } return nil } func sendResult(stream spannerpb.Spanner_StreamingReadServer, tx *transaction, iter RowIterator, returnTx bool, qs queryStats) error { // Create metadata about columns fields := make([]*spannerpb.StructType_Field, len(iter.ResultSet())) for i, item := range iter.ResultSet() { fields[i] = &spannerpb.StructType_Field{ Name: item.Name, Type: makeSpannerTypeFromValueType(item.ValueType), } } var txProto *spannerpb.Transaction if returnTx { txProto = tx.Proto() } metadata := &spannerpb.ResultSetMetadata{ RowType: &spannerpb.StructType{Fields: fields}, Transaction: txProto, } var rowCount int64 values := make([]*structpb.Value, 0, 100) err := iter.Do(func(row []interface{}) error { rowCount++ for _, x := range row { v, err := spannerValueFromValue(x) if err != nil { return err } values = append(values, v) } if len(values) > 100 { if err := stream.Send(&spannerpb.PartialResultSet{ Metadata: metadata, Values: values, }); err != nil { return err } // From documents: // Metadata about the result set, such as row type information. // Only present in the first response. metadata = nil // // Stats is only present once in the last response. // // But set the first response for now. // stats = nil values = values[:0] } return nil }) if err != nil { return err } qs.RowCount = rowCount stats := &spannerpb.ResultSetStats{ QueryStats: createQueryStats(qs), } if err := stream.Send(&spannerpb.PartialResultSet{ Metadata: metadata, Values: values, Stats: stats, }); err != nil { return err } return nil } type queryStats struct { Mode spannerpb.ExecuteSqlRequest_QueryMode ReceivedAt time.Time QueryText string RowCount int64 } func toMillisecondString(d time.Duration) string { return fmt.Sprintf("%d.%d msecs", d/time.Millisecond, (d%time.Millisecond)/time.Microsecond) } func createQueryStats(stats queryStats) *structpb.Struct { elapsedTime := time.Since(stats.ReceivedAt) return &structpb.Struct{ Fields: map[string]*structpb.Value{ "cpu_time": { Kind: &structpb.Value_StringValue{ StringValue: toMillisecondString(elapsedTime), }, }, "remote_server_calls": { Kind: &structpb.Value_StringValue{ StringValue: "0/0", }, }, "bytes_returned": { Kind: &structpb.Value_StringValue{ StringValue: "8", }, }, "query_text": { Kind: &structpb.Value_StringValue{ StringValue: stats.QueryText, }, }, "query_plan_creation_time": { Kind: &structpb.Value_StringValue{ StringValue: "0 msecs", }, }, "runtime_creation_time": { Kind: &structpb.Value_StringValue{ StringValue: "0 msecs", }, }, "deleted_rows_scanned": { Kind: &structpb.Value_StringValue{ StringValue: "0", }, }, "optimizer_version": { Kind: &structpb.Value_StringValue{ StringValue: "2", }, }, "elapsed_time": { Kind: &structpb.Value_StringValue{ StringValue: toMillisecondString(elapsedTime), }, }, "rows_returned": { Kind: &structpb.Value_StringValue{ StringValue: fmt.Sprintf("%d", stats.RowCount), }, }, "filesystem_delay_seconds": { Kind: &structpb.Value_StringValue{ StringValue: "0 msecs", }, }, "rows_scanned": { Kind: &structpb.Value_StringValue{ StringValue: "0", }, }, }, } } func (s *server) BeginTransaction(ctx context.Context, req *spannerpb.BeginTransactionRequest) (*spannerpb.Transaction, error) { session, err := s.getSession(req.Session) if err != nil { return nil, err } tx, err := session.BeginTransaction(req.GetOptions()) if err != nil { return nil, err } return tx.Proto(), nil } func (s *server) Commit(ctx context.Context, req *spannerpb.CommitRequest) (*spannerpb.CommitResponse, error) { ctx, cancel := context.WithCancel(ctx) defer cancel() session, err := s.getSession(req.Session) if err != nil { return nil, err } tx, err := session.GetTransactionForCommit(req.GetTransaction()) if err != nil { return nil, err } checkAvailability := func() (*spannerpb.CommitResponse, error) { switch tx.Status() { case TransactionInvalidated: return nil, status.Errorf(codes.FailedPrecondition, "This transaction has been invalidated by a later transaction in the same session.") case TransactionCommited: return &spannerpb.CommitResponse{}, nil case TransactionRollbacked: return nil, status.Errorf(codes.FailedPrecondition, "Cannot commit a transaction after Rollback() has been called.") case TransactionAborted: return nil, status.Errorf(codes.Aborted, "transaction aborted") } return nil, status.Errorf(codes.Unknown, "unknown status") } if !tx.Available() { return checkAvailability() } if !tx.ReadWrite() { var msg string if tx.SingleUse() { msg = "Cannot commit a single-use read-only transaction." } else { msg = "Cannot commit a read-only transaction." } return nil, status.Errorf(codes.FailedPrecondition, msg) } err = func() error { for _, m := range req.Mutations { switch op := m.Operation.(type) { case *spannerpb.Mutation_Insert: mut := op.Insert if err := session.database.Insert(ctx, tx, mut.Table, mut.Columns, mut.Values); err != nil { return err } case *spannerpb.Mutation_Update: mut := op.Update if err := session.database.Update(ctx, tx, mut.Table, mut.Columns, mut.Values); err != nil { return err } case *spannerpb.Mutation_Replace: mut := op.Replace if err := session.database.Replace(ctx, tx, mut.Table, mut.Columns, mut.Values); err != nil { return err } case *spannerpb.Mutation_InsertOrUpdate: mut := op.InsertOrUpdate if err := session.database.InsertOrUpdate(ctx, tx, mut.Table, mut.Columns, mut.Values); err != nil { return err } case *spannerpb.Mutation_Delete_: mut := op.Delete if err := session.database.Delete(ctx, tx, mut.Table, makeKeySet(mut.KeySet)); err != nil { return err } default: return fmt.Errorf("unknown mutation operation: %v", op) } } return nil }() if err != nil { if !tx.Available() { return checkAvailability() } tx.Done(TransactionRollbacked) return nil, err } if err := session.database.Commit(tx); err != nil { if !tx.Available() { return checkAvailability() } tx.Done(TransactionRollbacked) return nil, err // TODO } tx.Done(TransactionCommited) // TODO: more accurate time commitTime := time.Now() commitTimeProto := timestamppb.New(commitTime) return &spannerpb.CommitResponse{ CommitTimestamp: commitTimeProto, }, nil } func (s *server) Rollback(ctx context.Context, req *spannerpb.RollbackRequest) (*emptypb.Empty, error) { session, err := s.getSession(req.Session) if err != nil { return nil, err } tx, ok := session.GetTransaction(req.TransactionId) if !ok { return nil, status.Errorf(codes.InvalidArgument, "Transaction was started in a different session") } if !tx.Available() { switch tx.Status() { case TransactionInvalidated: return nil, status.Errorf(codes.FailedPrecondition, "This transaction has been invalidated by a later transaction in the same session.") case TransactionCommited: return nil, status.Errorf(codes.FailedPrecondition, "Cannot rollback a transaction after Commit() has been called.") case TransactionRollbacked: return &emptypb.Empty{}, nil case TransactionAborted: return nil, status.Errorf(codes.Aborted, "transaction aborted") } } if !tx.ReadWrite() { return nil, status.Errorf(codes.FailedPrecondition, "Cannot rollback a read-only transaction.") } if err := session.database.Rollback(tx); err != nil { return nil, err // TODO } tx.Done(TransactionRollbacked) return &emptypb.Empty{}, nil } func (s *server) PartitionQuery(ctx context.Context, req *spannerpb.PartitionQueryRequest) (*spannerpb.PartitionResponse, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: PartitionQuery") } func (s *server) PartitionRead(ctx context.Context, req *spannerpb.PartitionReadRequest) (*spannerpb.PartitionResponse, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: PartitionRead") } func (s *server) CreateBackup(ctx context.Context, req *adminv1pb.CreateBackupRequest) (*lropb.Operation, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: CreateBackup") } func (s *server) GetBackup(ctx context.Context, req *adminv1pb.GetBackupRequest) (*adminv1pb.Backup, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: GetBackup") } func (s *server) UpdateBackup(ctx context.Context, req *adminv1pb.UpdateBackupRequest) (*adminv1pb.Backup, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: UpdateBackup") } func (s *server) DeleteBackup(ctx context.Context, req *adminv1pb.DeleteBackupRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: DeleteBackup") } func (s *server) ListBackups(ctx context.Context, req *adminv1pb.ListBackupsRequest) (*adminv1pb.ListBackupsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: ListBackups") } func (s *server) RestoreDatabase(ctx context.Context, req *adminv1pb.RestoreDatabaseRequest) (*lropb.Operation, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: RestoreDatabase") } func (s *server) ListDatabaseOperations(ctx context.Context, req *adminv1pb.ListDatabaseOperationsRequest) (*adminv1pb.ListDatabaseOperationsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: ListDatabaseOperations") } func (s *server) ListBackupOperations(ctx context.Context, req *adminv1pb.ListBackupOperationsRequest) (*adminv1pb.ListBackupOperationsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: ListBackupOperations") } func (s *server) CopyBackup(ctx context.Context, req *adminv1pb.CopyBackupRequest) (*lropb.Operation, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: CopyBackup") } func (s *server) ListDatabaseRoles(ctx context.Context, req *adminv1pb.ListDatabaseRolesRequest) (*adminv1pb.ListDatabaseRolesResponse, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: ListDatabaseRoles") } func (*server) BatchWrite(*spannerpb.BatchWriteRequest, spannerpb.Spanner_BatchWriteServer) error { return status.Errorf(codes.Unimplemented, "not implemented yet: BatchWrite") } func (s *server) CreateBackupSchedule(ctx context.Context, req *adminv1pb.CreateBackupScheduleRequest) (*adminv1pb.BackupSchedule, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: CreateBackupSchedule") } func (s *server) GetBackupSchedule(ctx context.Context, req *adminv1pb.GetBackupScheduleRequest) (*adminv1pb.BackupSchedule, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: GetBackupSchedule") } func (s *server) UpdateBackupSchedule(ctx context.Context, req *adminv1pb.UpdateBackupScheduleRequest) (*adminv1pb.BackupSchedule, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: UpdateBackupSchedule") } func (s *server) DeleteBackupSchedule(ctx context.Context, req *adminv1pb.DeleteBackupScheduleRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: DeleteBackupSchedule") } func (s *server) ListBackupSchedules(ctx context.Context, req *adminv1pb.ListBackupSchedulesRequest) (*adminv1pb.ListBackupSchedulesResponse, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: ListBackupSchedules") } func (s *server) AddSplitPoints(ctx context.Context, req *adminv1pb.AddSplitPointsRequest) (*adminv1pb.AddSplitPointsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "not implemented yet: AddSplitPoints") } ================================================ FILE: server/server_test.go ================================================ // Copyright 2019 Masahiro Sano // // 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 server import ( "context" "fmt" "regexp" "sort" "strconv" "strings" "testing" "time" cmp "github.com/google/go-cmp/cmp" uuidpkg "github.com/google/uuid" lropb "google.golang.org/genproto/googleapis/longrunning" "google.golang.org/genproto/googleapis/rpc/errdetails" adminv1pb "google.golang.org/genproto/googleapis/spanner/admin/database/v1" spannerpb "google.golang.org/genproto/googleapis/spanner/v1" grpc "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/structpb" ) var ( simpleFields = []*spannerpb.StructType_Field{ { Name: "Id", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, { Name: "Value", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_STRING, }, }, } fullTypesFields = []*spannerpb.StructType_Field{ { Name: "PKey", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_STRING, }, }, { Name: "FTString", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_STRING, }, }, { Name: "FTStringNull", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_STRING, }, }, { Name: "FTBool", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_BOOL, }, }, { Name: "FTBoolNull", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_BOOL, }, }, { Name: "FTBytes", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_BYTES, }, }, { Name: "FTBytesNull", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_BYTES, }, }, { Name: "FTTimestamp", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_TIMESTAMP, }, }, { Name: "FTTimestampNull", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_TIMESTAMP, }, }, { Name: "FTInt", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, { Name: "FTIntNull", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, { Name: "FTFloat", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_FLOAT64, }, }, { Name: "FTFloatNull", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_FLOAT64, }, }, { Name: "FTDate", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_DATE, }, }, { Name: "FTDateNull", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_DATE, }, }, } arrayTypesFields = []*spannerpb.StructType_Field{ { Name: "Id", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, { Name: "ArrayString", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_STRING, }, }, }, { Name: "ArrayBool", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_BOOL, }, }, }, { Name: "ArrayBytes", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_BYTES, }, }, }, { Name: "ArrayTimestamp", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_TIMESTAMP, }, }, }, { Name: "ArrayInt", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, }, { Name: "ArrayFloat", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_FLOAT64, }, }, }, { Name: "ArrayDate", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_DATE, }, }, }, } ) func newTestServer() *server { return NewFakeServer().(*server) } func assertGRPCError(t *testing.T, err error, code codes.Code, msg string) { t.Helper() st := status.Convert(err) if st.Code() != code { t.Errorf("expect code to be %v but got %v", code, st.Code()) } if st.Message() != msg { t.Errorf("unexpected error message: \n %q\n expected:\n %q", st.Message(), msg) } } func assertStatusCode(t *testing.T, err error, code codes.Code) { t.Helper() st := status.Convert(err) if st.Code() != code { t.Errorf("expect code to be %v but got %v: msg=%v", code, st.Code(), st.Message()) } } func assertResourceInfo(t *testing.T, err error, expected *errdetails.ResourceInfo) { t.Helper() st := status.Convert(err) details := st.Details() if len(details) != 1 { t.Fatalf("error should have a detail: %v", len(details)) } ri, ok := details[0].(*errdetails.ResourceInfo) if !ok { t.Fatalf("error detail should be ResourceInfo: %T", details[0]) } if diff := cmp.Diff(expected, ri, protocmp.Transform()); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } } func testCreateSession(t *testing.T, s *server) (*spannerpb.Session, string) { name := fmt.Sprintf("projects/fake/instances/fakse/databases/%s", uuidpkg.New().String()) session, err := s.CreateSession(context.Background(), &spannerpb.CreateSessionRequest{ Database: name, }) if err != nil { t.Fatalf("failed to create session: %v", err) } return session, name } func testInvalidatedTransaction(t *testing.T, s *server, session *spannerpb.Session) *spannerpb.Transaction { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(tx.Id) == 0 { t.Error("transaction id must not be empty") } for i := 0; i < 32; i++ { tx2, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(tx2.Id) == 0 { t.Error("transaction id must not be empty") } } return tx } func TestCreateSession(t *testing.T) { ctx := context.Background() s := newTestServer() validDatabaseName := "projects/fake/instances/fake/databases/fake" t.Run("Success", func(t *testing.T) { for i := 0; i < 3; i++ { session, err := s.CreateSession(ctx, &spannerpb.CreateSessionRequest{ Database: validDatabaseName, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if session.Name == "" { t.Error("session name must not be empty") } } }) t.Run("Invalid", func(t *testing.T) { names := []string{ "projects/fake/instances/fake/databases/", "projects/fake/instances//databases/fake", "projects//instances/fake/databases/fake", "xx/fake/instances/fake/databases/fake", "projects/fake/xx/fake/databases/fake", "projects/fake/instances/fake/xx/fake", "xxx", } for _, name := range names { _, err := s.CreateSession(ctx, &spannerpb.CreateSessionRequest{ Database: name, }) st := status.Convert(err) if want, got := codes.InvalidArgument, st.Code(); want != got { t.Errorf("expect %v but got %v for %v", want, got, name) } } }) } func TestBatchCreateSessions(t *testing.T) { ctx := context.Background() s := newTestServer() validDatabaseName := "projects/fake/instances/fake/databases/fake" t.Run("Success", func(t *testing.T) { for i := 0; i < 3; i++ { sessions, err := s.BatchCreateSessions(ctx, &spannerpb.BatchCreateSessionsRequest{ Database: validDatabaseName, SessionCount: 3, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(sessions.Session) != 3 { t.Error("the number of sessions should be 3") } } }) t.Run("Invalid", func(t *testing.T) { names := []string{ "projects/fake/instances/fake/databases/", "projects/fake/instances//databases/fake", "projects//instances/fake/databases/fake", "xx/fake/instances/fake/databases/fake", "projects/fake/xx/fake/databases/fake", "projects/fake/instances/fake/xx/fake", "xxx", } for _, name := range names { _, err := s.BatchCreateSessions(ctx, &spannerpb.BatchCreateSessionsRequest{ Database: name, SessionCount: 1, }) st := status.Convert(err) if want, got := codes.InvalidArgument, st.Code(); want != got { t.Errorf("expect %v but got %v for %v", want, got, name) } } }) } func TestGetSession(t *testing.T) { ctx := context.Background() s := newTestServer() session, _ := testCreateSession(t, s) _, err := s.GetSession(ctx, &spannerpb.GetSessionRequest{ Name: session.Name, }) if err != nil { t.Fatalf("GetSession must success: %v", err) _, err = s.GetSession(ctx, &spannerpb.GetSessionRequest{ Name: "xx", }) st := status.Convert(err) if want, got := codes.InvalidArgument, st.Code(); want != got { t.Errorf("expect %v but got %v", want, got) } } _, err = s.GetSession(ctx, &spannerpb.GetSessionRequest{ Name: session.Name + "x", }) st := status.Convert(err) if want, got := codes.NotFound, st.Code(); want != got { t.Errorf("expect %v but got %v", want, got) } expectedRI := &errdetails.ResourceInfo{ ResourceType: "type.googleapis.com/google.spanner.v1.Session", ResourceName: session.Name + "x", Description: "Session does not exist.", } assertResourceInfo(t, err, expectedRI) } func TestListSessions(t *testing.T) { ctx := context.Background() s := newTestServer() dbName1 := fmt.Sprintf("projects/fake/instances/fakse/databases/%s", uuidpkg.New().String()) dbName2 := fmt.Sprintf("projects/fake/instances/fakse/databases/%s", uuidpkg.New().String()) var sessions1 []*spannerpb.Session var sessions2 []*spannerpb.Session for i := 0; i < 3; i++ { session, err := s.CreateSession(context.Background(), &spannerpb.CreateSessionRequest{ Database: dbName1, }) if err != nil { t.Fatalf("failed to create session: %v", err) } sessions1 = append(sessions1, session) } for i := 0; i < 2; i++ { session, err := s.CreateSession(context.Background(), &spannerpb.CreateSessionRequest{ Database: dbName2, }) if err != nil { t.Fatalf("failed to create session: %v", err) } sessions2 = append(sessions2, session) } res1, err := s.ListSessions(ctx, &spannerpb.ListSessionsRequest{ Database: dbName1, }) if err != nil { t.Fatalf("ListSession must succeed: %v", err) } for _, s := range res1.Sessions { var found bool for _, s2 := range sessions1 { if s.Name == s2.Name { found = true continue } } if !found { t.Errorf("session %s not found", s.Name) } } res2, err := s.ListSessions(ctx, &spannerpb.ListSessionsRequest{ Database: dbName2, }) if err != nil { t.Fatalf("ListSession must succeed: %v", err) } for _, s := range res2.Sessions { var found bool for _, s2 := range sessions2 { if s.Name == s2.Name { found = true continue } } if !found { t.Errorf("session %s not found", s.Name) } } } func TestDeleteSession(t *testing.T) { ctx := context.Background() s := newTestServer() session, _ := testCreateSession(t, s) _, err := s.DeleteSession(ctx, &spannerpb.DeleteSessionRequest{ Name: session.Name, }) if err != nil { t.Fatalf("DeleteSession must success: %v", err) } _, err = s.DeleteSession(ctx, &spannerpb.DeleteSessionRequest{ Name: session.Name, }) st := status.Convert(err) if want, got := codes.NotFound, st.Code(); want != got { t.Errorf("expect %v but got %v", want, got) } expectedRI := &errdetails.ResourceInfo{ ResourceType: "type.googleapis.com/google.spanner.v1.Session", ResourceName: session.Name, Description: "Session does not exist.", } assertResourceInfo(t, err, expectedRI) } func TestBeginTransaction(t *testing.T) { ctx := context.Background() s := newTestServer() t.Run("Success", func(t *testing.T) { session, _ := testCreateSession(t, s) tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(tx.Id) == 0 { t.Error("transaction id must not be empty") } tx2, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadOnly_{ ReadOnly: &spannerpb.TransactionOptions_ReadOnly{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(tx2.Id) == 0 { t.Error("transaction id must not be empty") } tx3, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_PartitionedDml_{ PartitionedDml: &spannerpb.TransactionOptions_PartitionedDml{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(tx3.Id) == 0 { t.Error("transaction id must not be empty") } }) t.Run("NoOption", func(t *testing.T) { session, _ := testCreateSession(t, s) _, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, }) if err == nil { t.Fatalf("unexpected success") } st := status.Convert(err) if st.Code() != codes.InvalidArgument { t.Fatalf("err codes must be InvalidArgument but got %v", st.Code()) } }) } func NewFakeExecuteStreamingSqlServer(ctx context.Context) *fakeExecuteStreamingSqlServer { return &fakeExecuteStreamingSqlServer{ctx: ctx} } type fakeExecuteStreamingSqlServer struct { ctx context.Context cancel func() sets []*spannerpb.PartialResultSet grpc.ServerStream } func (s *fakeExecuteStreamingSqlServer) Send(set *spannerpb.PartialResultSet) error { s.sets = append(s.sets, set) return nil } func (s *fakeExecuteStreamingSqlServer) Context() context.Context { if s.ctx == nil { return context.Background() } return s.ctx } func TestExecuteStreamingSql_Success(t *testing.T) { ctx := context.Background() s := newTestServer() session, dbName := testCreateSession(t, s) // TODO: prepare initial data db, ok := s.db[dbName] if !ok { t.Fatalf("database not found") } for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } for _, query := range []string{ `INSERT INTO Simple VALUES(100, "xxx")`, `INSERT INTO Simple VALUES(200, "yyy")`, `INSERT INTO Simple VALUES(300, "zzz")`, `INSERT INTO CompositePrimaryKeys VALUES(1, "aaa", 1, 0, "x1", "y1", "z")`, `INSERT INTO CompositePrimaryKeys VALUES(2, "bbb", 2, 0, "x1", "y2", "z")`, `INSERT INTO CompositePrimaryKeys VALUES(3, "bbb", 3, 0, "x1", "y3", "z")`, `INSERT INTO CompositePrimaryKeys VALUES(4, "ccc", 3, 0, "x2", "y4", "z")`, `INSERT INTO CompositePrimaryKeys VALUES(5, "ccc", 4, 0, "x2", "y5", "z")`, `INSERT INTO FullTypes VALUES("xxx", "xxx", "xxx", true, true, "eHl6", "eHl6", "2012-03-04T12:34:56.123456789Z", "2012-03-04T12:34:56.123456789Z", 100, 100, 0.5, 0.5, "2012-03-04", "2012-03-04" )`, `INSERT INTO ArrayTypes VALUES(100, json_array("xxx1", "xxx2"), json_array(true, false), json_array("eHl6", "eHl6"), json_array("2012-03-04T12:34:56.123456789Z", "2012-03-04T12:34:56.999999999Z"), json_array(1, 2), json_array(0.1, 0.2), json_array("2012-03-04", "2012-03-05") )`, } { if _, err := db.db.ExecContext(ctx, query); err != nil { t.Fatalf("Insert failed: %v", err) } } table := map[string]struct { sql string types map[string]*spannerpb.Type params *structpb.Struct fields []*spannerpb.StructType_Field expected [][]*structpb.Value }{ "Simple": { sql: `SELECT * FROM Simple`, fields: simpleFields, expected: [][]*structpb.Value{ { makeStringValue("100"), makeStringValue("xxx"), makeStringValue("200"), makeStringValue("yyy"), makeStringValue("300"), makeStringValue("zzz"), }, }, }, "Simple_QueryParam": { sql: `SELECT * FROM Simple WHERE Id IN (@foo, @bar)`, types: map[string]*spannerpb.Type{ "foo": &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, "bar": &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, params: &structpb.Struct{ Fields: map[string]*structpb.Value{ "foo": makeStringValue("100"), "bar": makeStringValue("200"), }, }, fields: simpleFields, expected: [][]*structpb.Value{ { makeStringValue("100"), makeStringValue("xxx"), makeStringValue("200"), makeStringValue("yyy"), }, }, }, "Simple_QueryParam_ImplicitTypeAsInt64": { sql: `SELECT @foo`, params: &structpb.Struct{ Fields: map[string]*structpb.Value{ "foo": makeStringValue("100"), }, }, fields: []*spannerpb.StructType_Field{ { Name: "", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, }, expected: [][]*structpb.Value{ { makeStringValue("100"), }, }, }, "Simple_UnusedTypes": { sql: `SELECT 100 v`, types: map[string]*spannerpb.Type{ "foo": &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, fields: []*spannerpb.StructType_Field{ { Name: "v", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, }, expected: [][]*structpb.Value{ { makeStringValue("100"), }, }, }, "Simple_UnusedParams": { sql: `SELECT 100 v`, types: map[string]*spannerpb.Type{ "foo": &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, params: &structpb.Struct{ Fields: map[string]*structpb.Value{ "foo": makeStringValue("200"), }, }, fields: []*spannerpb.StructType_Field{ { Name: "v", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, }, expected: [][]*structpb.Value{ { makeStringValue("100"), }, }, }, "FromUnnest_ArrayLiteral": { sql: `SELECT x, y FROM UNNEST (["xxx", "yyy"]) AS x WITH OFFSET y`, fields: []*spannerpb.StructType_Field{ { Name: "x", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_STRING, }, }, { Name: "y", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, }, expected: [][]*structpb.Value{ { makeStringValue("xxx"), makeStringValue("0"), makeStringValue("yyy"), makeStringValue("1"), }, }, }, "FromUnnest_Params": { sql: `SELECT x, y FROM UNNEST (@foo) AS x WITH OFFSET y`, types: map[string]*spannerpb.Type{ "foo": &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, }, params: &structpb.Struct{ Fields: map[string]*structpb.Value{ "foo": makeListValueAsValue(makeListValue(makeStringValue("100"), makeStringValue("200"))), }, }, fields: []*spannerpb.StructType_Field{ { Name: "x", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, { Name: "y", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, }, expected: [][]*structpb.Value{ { makeStringValue("100"), makeStringValue("0"), makeStringValue("200"), makeStringValue("1"), }, }, }, "Simple_Unnest_Array": { sql: `SELECT * FROM Simple WHERE Id IN UNNEST([100, 200])`, fields: simpleFields, expected: [][]*structpb.Value{ { makeStringValue("100"), makeStringValue("xxx"), makeStringValue("200"), makeStringValue("yyy"), }, }, }, "Simple_Unnest_Params": { sql: `SELECT * FROM Simple WHERE Id IN UNNEST(@ids)`, types: map[string]*spannerpb.Type{ "ids": &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, }, params: &structpb.Struct{ Fields: map[string]*structpb.Value{ "ids": makeListValueAsValue(makeListValue(makeStringValue("100"), makeStringValue("200"))), }, }, fields: simpleFields, expected: [][]*structpb.Value{ { makeStringValue("100"), makeStringValue("xxx"), makeStringValue("200"), makeStringValue("yyy"), }, }, }, "Simple_Unnest_Params_EmptyArray": { sql: `SELECT * FROM Simple WHERE Id IN UNNEST(@ids)`, types: map[string]*spannerpb.Type{ "ids": &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, }, params: &structpb.Struct{ Fields: map[string]*structpb.Value{ "ids": makeNullValue(), }, }, fields: nil, expected: [][]*structpb.Value{ {}, }, }, "CompositePrimaryKeys_Condition": { sql: `SELECT Id, PKey1, PKey2 FROM CompositePrimaryKeys WHERE PKey1 = "bbb" AND (PKey2 = 3 OR PKey2 = 4)`, fields: []*spannerpb.StructType_Field{ { Name: "Id", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, { Name: "PKey1", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_STRING, }, }, { Name: "PKey2", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, }, expected: [][]*structpb.Value{ {makeStringValue("3"), makeStringValue("bbb"), makeStringValue("3")}, }, }, "CompositePrimaryKeys_Condition2": { sql: fmt.Sprintf(`SELECT %s FROM CompositePrimaryKeys`, strings.Join(strings.Split(strings.Repeat("XYZ", 10), ""), ", ")), expected: [][]*structpb.Value{ { makeStringValue("x2"), makeStringValue("y5"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y5"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y5"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y5"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y5"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y5"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y5"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y5"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y5"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y5"), makeStringValue("z"), makeStringValue("x1"), makeStringValue("y2"), makeStringValue("z"), makeStringValue("x1"), makeStringValue("y2"), makeStringValue("z"), makeStringValue("x1"), makeStringValue("y2"), makeStringValue("z"), makeStringValue("x1"), makeStringValue("y2"), makeStringValue("z"), makeStringValue("x1"), makeStringValue("y2"), makeStringValue("z"), makeStringValue("x1"), makeStringValue("y2"), makeStringValue("z"), makeStringValue("x1"), makeStringValue("y2"), makeStringValue("z"), makeStringValue("x1"), makeStringValue("y2"), makeStringValue("z"), makeStringValue("x1"), makeStringValue("y2"), makeStringValue("z"), makeStringValue("x1"), makeStringValue("y2"), makeStringValue("z"), makeStringValue("x1"), makeStringValue("y3"), makeStringValue("z"), makeStringValue("x1"), makeStringValue("y3"), makeStringValue("z"), makeStringValue("x1"), makeStringValue("y3"), makeStringValue("z"), makeStringValue("x1"), makeStringValue("y3"), makeStringValue("z"), makeStringValue("x1"), makeStringValue("y3"), makeStringValue("z"), makeStringValue("x1"), makeStringValue("y3"), makeStringValue("z"), makeStringValue("x1"), makeStringValue("y3"), makeStringValue("z"), makeStringValue("x1"), makeStringValue("y3"), makeStringValue("z"), makeStringValue("x1"), makeStringValue("y3"), makeStringValue("z"), makeStringValue("x1"), makeStringValue("y3"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y4"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y4"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y4"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y4"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y4"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y4"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y4"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y4"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y4"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y4"), makeStringValue("z"), }, { makeStringValue("x2"), makeStringValue("y5"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y5"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y5"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y5"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y5"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y5"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y5"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y5"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y5"), makeStringValue("z"), makeStringValue("x2"), makeStringValue("y5"), makeStringValue("z"), }, }, }, "ArrayOfStruct": { sql: `SELECT ARRAY(SELECT STRUCT(1,"xx") x)`, fields: []*spannerpb.StructType_Field{ { Name: "", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_STRUCT, StructType: &spannerpb.StructType{ Fields: []*spannerpb.StructType_Field{ { Name: "Id", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, { Name: "Value", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_STRING, }, }, }, }, }, }, }, }, expected: [][]*structpb.Value{ { makeListValueAsValue(makeListValue( makeStructValue(map[string]*structpb.Value{ "Id": makeStringValue("1"), "Value": makeStringValue("xx"), }), )), }, }, }, "ArrayOfStruct_FullTypes": { sql: `SELECT ARRAY(SELECT AS STRUCT * FROM FullTypes)`, fields: []*spannerpb.StructType_Field{ { Name: "", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_STRUCT, StructType: &spannerpb.StructType{ Fields: fullTypesFields, }, }, }, }, }, // fields: simpleFields, expected: [][]*structpb.Value{ { makeListValueAsValue(makeListValue( makeStructValue(map[string]*structpb.Value{ "PKey": makeStringValue("xxx"), "FTString": makeStringValue("xxx"), "FTStringNull": makeStringValue("xxx"), "FTBool": makeBoolValue(true), "FTBoolNull": makeBoolValue(true), "FTBytes": makeStringValue("eHl6"), "FTBytesNull": makeStringValue("eHl6"), "FTTimestamp": makeStringValue("2012-03-04T12:34:56.123456789Z"), "FTTimestampNull": makeStringValue("2012-03-04T12:34:56.123456789Z"), "FTInt": makeStringValue("100"), "FTIntNull": makeStringValue("100"), "FTFloat": makeNumberValue(0.5), "FTFloatNull": makeNumberValue(0.5), "FTDate": makeStringValue("2012-03-04"), "FTDateNull": makeStringValue("2012-03-04"), }), )), }, }, }, "ArrayOfStruct_ArrayTypes": { sql: `SELECT ARRAY(SELECT AS STRUCT * FROM ArrayTypes)`, fields: []*spannerpb.StructType_Field{ { Name: "", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_STRUCT, StructType: &spannerpb.StructType{ Fields: arrayTypesFields, }, }, }, }, }, // fields: simpleFields, expected: [][]*structpb.Value{ { makeListValueAsValue(makeListValue( makeStructValue(map[string]*structpb.Value{ "Id": makeStringValue("100"), "ArrayString": makeListValueAsValue(makeListValue( makeStringValue("xxx1"), makeStringValue("xxx2"), )), "ArrayBool": makeListValueAsValue(makeListValue( makeBoolValue(true), makeBoolValue(false), )), "ArrayBytes": makeListValueAsValue(makeListValue( makeStringValue("eHl6"), makeStringValue("eHl6"), )), "ArrayTimestamp": makeListValueAsValue(makeListValue( makeStringValue("2012-03-04T12:34:56.123456789Z"), makeStringValue("2012-03-04T12:34:56.999999999Z"), )), "ArrayInt": makeListValueAsValue(makeListValue( makeStringValue("1"), makeStringValue("2"), )), "ArrayFloat": makeListValueAsValue(makeListValue( makeNumberValue(0.1), makeNumberValue(0.2), )), "ArrayDate": makeListValueAsValue(makeListValue( makeStringValue("2012-03-04"), makeStringValue("2012-03-05"), )), }), )), }, }, }, } for name, tc := range table { t.Run(name, func(t *testing.T) { fake := &fakeExecuteStreamingSqlServer{} if err := s.ExecuteStreamingSql(&spannerpb.ExecuteSqlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{}, Sql: tc.sql, ParamTypes: tc.types, Params: tc.params, }, fake); err != nil { t.Fatalf("unexpected error: %v", err) } var results [][]*structpb.Value for _, set := range fake.sets { results = append(results, set.Values) } if tc.fields != nil { if len(results) == 0 { t.Fatalf("unexpected number of results") } if diff := cmp.Diff(tc.fields, fake.sets[0].Metadata.RowType.Fields, protocmp.Transform()); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } } if diff := cmp.Diff(tc.expected, results, protocmp.Transform()); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) } } func TestStreamingRead_ValueType(t *testing.T) { table := map[string]struct { table string wcols []string values []*structpb.Value rcols []string fields []*spannerpb.StructType_Field expected []*structpb.Value }{ "Simple": { table: "Simple", wcols: []string{"Id", "Value"}, values: []*structpb.Value{ makeStringValue("300"), makeStringValue("zzz"), }, rcols: []string{"Id", "Value"}, fields: []*spannerpb.StructType_Field{ { Name: "Id", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, { Name: "Value", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_STRING, }, }, }, expected: []*structpb.Value{ makeStringValue("300"), makeStringValue("zzz"), }, }, "FullTypes": { table: "FullTypes", wcols: fullTypesKeys, values: []*structpb.Value{ makeStringValue("xxx"), // PKey STRING(32) NOT NULL, makeStringValue("xxx"), // FTString STRING(32) NOT NULL, makeNullValue(), // FTStringNull STRING(32), makeBoolValue(true), // FTBool BOOL NOT NULL, makeNullValue(), // FTBoolNull BOOL, makeStringValue("eHh4"), // FTBytes BYTES(32) NOT NULL, makeNullValue(), // FTBytesNull BYTES(32), makeStringValue("2012-03-04T12:34:56.123456789Z"), // FTTimestamp TIMESTAMP NOT NULL, makeNullValue(), // FTTimestampNull TIMESTAMP, makeStringValue("100"), // FTInt INT64 NOT NULL, makeNullValue(), // FTIntNull INT64, makeNumberValue(0.5), // FTFloat FLOAT64 NOT NULL, makeNullValue(), // FTFloatNull FLOAT64, makeStringValue("2012-03-04"), // FTDate DATE NOT NULL, makeNullValue(), // FTDateNull DATE, }, rcols: fullTypesKeys, fields: fullTypesFields, expected: []*structpb.Value{ makeStringValue("xxx"), // PKey STRING(32) NOT NULL, makeStringValue("xxx"), // FTString STRING(32) NOT NULL, makeNullValue(), // FTStringNull STRING(32), makeBoolValue(true), // FTBool BOOL NOT NULL, makeNullValue(), // FTBoolNull BOOL, makeStringValue("eHh4"), // FTBytes BYTES(32) NOT NULL, makeNullValue(), // FTBytesNull BYTES(32), makeStringValue("2012-03-04T12:34:56.123456789Z"), // FTTimestamp TIMESTAMP NOT NULL, makeNullValue(), // FTTimestampNull TIMESTAMP, makeStringValue("100"), // FTInt INT64 NOT NULL, makeNullValue(), // FTIntNull INT64, makeNumberValue(0.5), // FTFloat FLOAT64 NOT NULL, makeNullValue(), // FTFloatNull FLOAT64, makeStringValue("2012-03-04"), // FTDate DATE NOT NULL, makeNullValue(), // FTDateNull DATE, }, }, // Spanner returns array but it possiblly includes NullValue in the elements "ArrayTypes_NonNull": { table: "ArrayTypes", wcols: arrayTypesKeys, values: []*structpb.Value{ makeStringValue("100"), makeListValueAsValue(makeListValue( makeStringValue("xxx"), makeNullValue(), )), makeListValueAsValue(makeListValue( makeBoolValue(true), makeNullValue(), )), makeListValueAsValue(makeListValue( makeStringValue("eHh4"), makeNullValue(), )), makeListValueAsValue(makeListValue( makeStringValue("2012-03-04T12:34:56.123456789Z"), makeNullValue(), )), makeListValueAsValue(makeListValue( makeStringValue("100"), makeNullValue(), )), makeListValueAsValue(makeListValue( makeNumberValue(0.5), makeNullValue(), )), makeListValueAsValue(makeListValue( makeStringValue("2012-03-04"), makeNullValue(), )), }, rcols: arrayTypesKeys, fields: arrayTypesFields, expected: []*structpb.Value{ makeStringValue("100"), makeListValueAsValue(makeListValue( makeStringValue("xxx"), makeNullValue(), )), makeListValueAsValue(makeListValue( makeBoolValue(true), makeNullValue(), )), makeListValueAsValue(makeListValue( makeStringValue("eHh4"), makeNullValue(), )), makeListValueAsValue(makeListValue( makeStringValue("2012-03-04T12:34:56.123456789Z"), makeNullValue(), )), makeListValueAsValue(makeListValue( makeStringValue("100"), makeNullValue(), )), makeListValueAsValue(makeListValue( makeNumberValue(0.5), makeNullValue(), )), makeListValueAsValue(makeListValue( makeStringValue("2012-03-04"), makeNullValue(), )), }, }, // Spanner returns NullValue if the value is null "ArrayTypes_Null": { table: "ArrayTypes", wcols: arrayTypesKeys, values: []*structpb.Value{ makeStringValue("101"), makeNullValue(), makeNullValue(), makeNullValue(), makeNullValue(), makeNullValue(), makeNullValue(), makeNullValue(), }, rcols: arrayTypesKeys, fields: arrayTypesFields, expected: []*structpb.Value{ makeStringValue("101"), makeNullValue(), makeNullValue(), makeNullValue(), makeNullValue(), makeNullValue(), makeNullValue(), makeNullValue(), }, }, // Spanner returns empty list as array if the value is empty not null "ArrayTypes_Empty": { table: "ArrayTypes", wcols: arrayTypesKeys, values: []*structpb.Value{ makeStringValue("100"), makeListValueAsValue(&structpb.ListValue{Values: []*structpb.Value{}}), makeListValueAsValue(&structpb.ListValue{Values: []*structpb.Value{}}), makeListValueAsValue(&structpb.ListValue{Values: []*structpb.Value{}}), makeListValueAsValue(&structpb.ListValue{Values: []*structpb.Value{}}), makeListValueAsValue(&structpb.ListValue{Values: []*structpb.Value{}}), makeListValueAsValue(&structpb.ListValue{Values: []*structpb.Value{}}), makeListValueAsValue(&structpb.ListValue{Values: []*structpb.Value{}}), }, rcols: arrayTypesKeys, fields: arrayTypesFields, expected: []*structpb.Value{ makeStringValue("100"), makeListValueAsValue(&structpb.ListValue{Values: []*structpb.Value{}}), makeListValueAsValue(&structpb.ListValue{Values: []*structpb.Value{}}), makeListValueAsValue(&structpb.ListValue{Values: []*structpb.Value{}}), makeListValueAsValue(&structpb.ListValue{Values: []*structpb.Value{}}), makeListValueAsValue(&structpb.ListValue{Values: []*structpb.Value{}}), makeListValueAsValue(&structpb.ListValue{Values: []*structpb.Value{}}), makeListValueAsValue(&structpb.ListValue{Values: []*structpb.Value{}}), }, }, } for name, tc := range table { t.Run(name, func(t *testing.T) { ctx := context.Background() s := newTestServer() session, dbName := testCreateSession(t, s) db, ok := s.db[dbName] if !ok { t.Fatalf("database not found") } for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } _, err := s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_SingleUseTransaction{ SingleUseTransaction: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }, Mutations: []*spannerpb.Mutation{ { Operation: &spannerpb.Mutation_Insert{ Insert: &spannerpb.Mutation_Write{ Table: tc.table, Columns: tc.wcols, Values: []*structpb.ListValue{ { Values: tc.values, }, }, }, }}, }, }) if err != nil { t.Fatalf("commit failed: %v", err) } fake := &fakeExecuteStreamingSqlServer{} if err := s.StreamingRead(&spannerpb.ReadRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{}, Table: tc.table, Columns: tc.rcols, KeySet: &spannerpb.KeySet{All: true}, }, fake); err != nil { t.Fatalf("unexpected error: %v", err) } var results [][]*structpb.Value for _, set := range fake.sets { results = append(results, set.Values) } if len(results) != 1 { t.Errorf("results should be 1 record but got %v", len(results)) } if diff := cmp.Diff(tc.fields, fake.sets[0].Metadata.RowType.Fields, protocmp.Transform()); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } if diff := cmp.Diff(tc.expected, results[0], protocmp.Transform()); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) } } func TestExecuteStreamingSql_Error(t *testing.T) { ctx := context.Background() s := newTestServer() session, dbName := testCreateSession(t, s) // TODO: prepare initial data db, ok := s.db[dbName] if !ok { t.Fatalf("database not found") } for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } table := map[string]struct { sql string types map[string]*spannerpb.Type params *structpb.Struct code codes.Code msg *regexp.Regexp }{ "NoParameter": { sql: `SELECT @foo`, types: nil, params: nil, code: codes.InvalidArgument, msg: regexp.MustCompile(`No parameter found for binding: foo`), }, "NoType": { sql: `SELECT @foo`, types: nil, params: &structpb.Struct{ Fields: map[string]*structpb.Value{ "foo": &structpb.Value{ Kind: &structpb.Value_StringValue{StringValue: "x"}, }, }, }, code: codes.InvalidArgument, msg: regexp.MustCompile(`Invalid value for bind parameter foo: Expected INT64.`), }, "InvalidValueForBinding": { sql: `SELECT @foo`, params: &structpb.Struct{ Fields: map[string]*structpb.Value{ "foo": &structpb.Value{ Kind: &structpb.Value_StringValue{StringValue: "x"}, }, }, }, code: codes.InvalidArgument, msg: regexp.MustCompile(`Invalid value for bind parameter foo: Expected INT64.`), }, "EmptySQL": { sql: ``, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Invalid ExecuteStreamingSql request.`), }, "SQLSyntxError": { sql: `SELECT`, code: codes.InvalidArgument, msg: regexp.MustCompile(`^Syntax error: .+`), }, "BindStruct": { sql: `SELECT @bar`, types: map[string]*spannerpb.Type{ "bar": &spannerpb.Type{ Code: spannerpb.TypeCode_STRUCT, StructType: &spannerpb.StructType{ Fields: []*spannerpb.StructType_Field{ { Name: "xxx", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_STRING, }, }, { Name: "yyy", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, }, }, }, }, params: &structpb.Struct{ Fields: map[string]*structpb.Value{ "bar": &structpb.Value{ Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{ Fields: map[string]*structpb.Value{ "xxx": &structpb.Value{ Kind: &structpb.Value_StringValue{StringValue: "xxx"}, }, "yyy": &structpb.Value{ Kind: &structpb.Value_StringValue{StringValue: "100"}, }, }, }}, }, }, }, code: codes.InvalidArgument, msg: regexp.MustCompile(`Invalid value for bind parameter bar: Expected STRUCT`), }, "BindArrayStruct": { sql: `SELECT @bar`, params: &structpb.Struct{ Fields: map[string]*structpb.Value{ "bar": &structpb.Value{ Kind: &structpb.Value_ListValue{ ListValue: &structpb.ListValue{ Values: []*structpb.Value{ { Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{ Fields: map[string]*structpb.Value{ "xxx": &structpb.Value{ Kind: &structpb.Value_StringValue{StringValue: "xxx"}, }, "yyy": &structpb.Value{ Kind: &structpb.Value_StringValue{StringValue: "100"}, }, }, }}, }, }, }, }, }, }, }, types: map[string]*spannerpb.Type{ "bar": &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_STRUCT, StructType: &spannerpb.StructType{ Fields: []*spannerpb.StructType_Field{ { Name: "xxx", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_STRING, }, }, { Name: "yyy", Type: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, }, }, }, }, }, code: codes.InvalidArgument, // TODO: return correct error message // msg: regexp.MustCompile(`Invalid value for array element bar[0]: Expected STRUCT.`), msg: regexp.MustCompile(`^Invalid value for bind parameter bar\[0\]: Expected STRUCT.`), }, } for name, tc := range table { t.Run(name, func(t *testing.T) { fake := &fakeExecuteStreamingSqlServer{} err := s.ExecuteStreamingSql(&spannerpb.ExecuteSqlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{}, Sql: tc.sql, ParamTypes: tc.types, Params: tc.params, }, fake) st := status.Convert(err) if st.Code() != tc.code { t.Errorf("expect code to be %v but got %v", tc.code, st.Code()) } if !tc.msg.MatchString(st.Message()) { t.Errorf("unexpected error message: \n %q\n expected:\n %q", st.Message(), tc.msg) } }) } } func TestExecuteSql_Success(t *testing.T) { ctx := context.Background() s := newTestServer() preareDBAndSession := func(t *testing.T) *spannerpb.Session { session, dbName := testCreateSession(t, s) db, ok := s.db[dbName] if !ok { t.Fatalf("database not found") } for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } for _, query := range []string{ `INSERT INTO Simple VALUES(100, "xxx")`, `INSERT INTO Simple VALUES(200, "yyy")`, `INSERT INTO Simple VALUES(300, "zzz")`, } { if _, err := db.db.ExecContext(ctx, query); err != nil { t.Fatalf("Insert failed: %v", err) } } return session } begin := func(session *spannerpb.Session) *spannerpb.Transaction { tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } return tx } commit := func(session *spannerpb.Session, tx *spannerpb.Transaction) error { _, err := s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_TransactionId{ TransactionId: tx.Id, }, Mutations: []*spannerpb.Mutation{}, }) return err } table := map[string]struct { sql string types map[string]*spannerpb.Type params *structpb.Struct fields []*spannerpb.StructType_Field table string columns []string expected [][]*structpb.Value }{ "Update": { sql: `UPDATE Simple SET Value = "xyz" WHERE Id = 200`, table: "Simple", columns: []string{"Id", "Value"}, expected: [][]*structpb.Value{ { makeStringValue("100"), makeStringValue("xxx"), makeStringValue("200"), makeStringValue("xyz"), makeStringValue("300"), makeStringValue("zzz"), }, }, }, "Update_ParamInWhere": { sql: `UPDATE Simple SET Value = "xyz" WHERE Id = @id`, types: map[string]*spannerpb.Type{ "id": &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, params: &structpb.Struct{ Fields: map[string]*structpb.Value{ "id": &structpb.Value{ Kind: &structpb.Value_StringValue{StringValue: "200"}, }, }, }, table: "Simple", columns: []string{"Id", "Value"}, expected: [][]*structpb.Value{ { makeStringValue("100"), makeStringValue("xxx"), makeStringValue("200"), makeStringValue("xyz"), makeStringValue("300"), makeStringValue("zzz"), }, }, }, } for name, tc := range table { t.Run(name, func(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() session := preareDBAndSession(t) tx := begin(session) if _, err := s.ExecuteSql(ctx, &spannerpb.ExecuteSqlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx.Id, }, }, Sql: tc.sql, ParamTypes: tc.types, Params: tc.params, }); err != nil { t.Fatalf("unexpected error: %v", err) } if err := commit(session, tx); err != nil { t.Fatalf("commit error: %v", err) } fake := NewFakeExecuteStreamingSqlServer(ctx) if err := s.StreamingRead(&spannerpb.ReadRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_SingleUse{ SingleUse: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadOnly_{ ReadOnly: &spannerpb.TransactionOptions_ReadOnly{}, }, }, }, }, Table: tc.table, Columns: tc.columns, KeySet: &spannerpb.KeySet{All: true}, }, fake); err != nil { t.Fatalf("Read failed: %v", err) } var results [][]*structpb.Value for _, set := range fake.sets { results = append(results, set.Values) } if diff := cmp.Diff(tc.expected, results, protocmp.Transform()); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) } } func TestExecuteSql_Error(t *testing.T) { ctx := context.Background() s := newTestServer() begin := func(ctx context.Context, t *testing.T, session *spannerpb.Session) *spannerpb.Transaction { tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } return tx } t.Run("RollbackAfterRollback", func(t *testing.T) { session, _ := testCreateSession(t, s) tx := begin(ctx, t, session) _, err := s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_TransactionId{ TransactionId: tx.Id, }, Mutations: []*spannerpb.Mutation{}, }) if err != nil { t.Fatalf("unexpected error: %v", err) } _, err = s.ExecuteSql(ctx, &spannerpb.ExecuteSqlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx.Id, }, }, Sql: `UPDATE Simple SET Value = "x" WHERE Id = 1000`, }) expected := "Cannot start a read or query within a transaction after Commit() or Rollback() has been called." assertGRPCError(t, err, codes.FailedPrecondition, expected) }) t.Run("CommitAfterRollback", func(t *testing.T) { session, _ := testCreateSession(t, s) tx := begin(ctx, t, session) _, err := s.Rollback(ctx, &spannerpb.RollbackRequest{ Session: session.Name, TransactionId: tx.Id, }) if err != nil { t.Fatalf("unexpected error: %v", err) } _, err = s.ExecuteSql(ctx, &spannerpb.ExecuteSqlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx.Id, }, }, Sql: `UPDATE Simple SET Value = "x" WHERE Id = 1000`, }) expected := "Cannot start a read or query within a transaction after Commit() or Rollback() has been called." assertGRPCError(t, err, codes.FailedPrecondition, expected) }) t.Run("ReadOnlyTransaction", func(t *testing.T) { session, _ := testCreateSession(t, s) tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadOnly_{ ReadOnly: &spannerpb.TransactionOptions_ReadOnly{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } _, err = s.ExecuteSql(ctx, &spannerpb.ExecuteSqlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx.Id, }, }, Sql: `UPDATE Simple SET Value = "x" WHERE Id = 1000`, }) expected := "DML statements can only be performed in a read-write transaction." assertGRPCError(t, err, codes.FailedPrecondition, expected) }) t.Run("SingleUseTransaction", func(t *testing.T) { session, _ := testCreateSession(t, s) _, err := s.ExecuteSql(ctx, &spannerpb.ExecuteSqlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_SingleUse{ SingleUse: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }, }, Sql: `UPDATE Simple SET Value = "x" WHERE Id = 1000`, }) expected := "DML statements may not be performed in single-use transactions, to avoid replay." assertGRPCError(t, err, codes.InvalidArgument, expected) }) } func TestExecuteSql_Transaction(t *testing.T) { ctx := context.Background() s := newTestServer() preareDBAndSession := func(t *testing.T) *spannerpb.Session { session, dbName := testCreateSession(t, s) db, ok := s.db[dbName] if !ok { t.Fatalf("database not found") } for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } for _, query := range []string{ `INSERT INTO Simple VALUES(100, "xxx")`, } { if _, err := db.db.ExecContext(ctx, query); err != nil { t.Fatalf("Insert failed: %v", err) } } return session } begin := func(session *spannerpb.Session) *spannerpb.Transaction { tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } return tx } commit := func(session *spannerpb.Session, tx *spannerpb.Transaction) error { _, err := s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_TransactionId{ TransactionId: tx.Id, }, Mutations: []*spannerpb.Mutation{}, }) return err } readSimple := func(ctx context.Context, session *spannerpb.Session, tx *spannerpb.Transaction) ([]*structpb.Value, error) { fake := NewFakeExecuteStreamingSqlServer(ctx) err := s.StreamingRead(&spannerpb.ReadRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx.Id, }, }, Table: "Simple", Columns: []string{"Id", "Value"}, KeySet: &spannerpb.KeySet{All: true}, }, fake) if err != nil { return nil, err } var results []*structpb.Value for _, set := range fake.sets { results = append(results, set.Values...) } return results, nil } initialValueSet := []*structpb.Value{ makeStringValue("100"), makeStringValue("xxx"), } t.Run("ReadBeforeCommit_SameTransaction", func(t *testing.T) { session := preareDBAndSession(t) tx := begin(session) _, err := s.ExecuteSql(ctx, &spannerpb.ExecuteSqlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx.Id, }, }, Sql: `UPDATE Simple SET Value = "zzz" WHERE Id = 100`, Params: nil, ParamTypes: nil, }) assertStatusCode(t, err, codes.OK) expected := []*structpb.Value{ makeStringValue("100"), makeStringValue("zzz"), } results, err := readSimple(ctx, session, tx) assertStatusCode(t, err, codes.OK) if diff := cmp.Diff(results, expected, protocmp.Transform()); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } if err := commit(session, tx); err != nil { t.Fatalf("commit failed: %v", err) } }) t.Run("ReadBeforeCommit_AnotherTransaction", func(t *testing.T) { session := preareDBAndSession(t) tx1 := begin(session) tx2 := begin(session) defer commit(session, tx1) defer commit(session, tx2) _, err := s.ExecuteSql(ctx, &spannerpb.ExecuteSqlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx1.Id, }, }, Sql: `UPDATE Simple SET Value = "zzz" WHERE Id = 100`, Params: nil, ParamTypes: nil, }) assertStatusCode(t, err, codes.OK) expected := []*structpb.Value{ makeStringValue("100"), makeStringValue("zzz"), } // read in another transaction timeouts ctx2, cancel := context.WithTimeout(ctx, 100*time.Millisecond) defer cancel() _, err = readSimple(ctx2, session, tx2) assertStatusCode(t, err, codes.DeadlineExceeded) if err := commit(session, tx1); err != nil { t.Fatalf("commit failed: %v", err) } // read in another transaction succeeds after commit ctx3, cancel := context.WithTimeout(ctx, 100*time.Millisecond) defer cancel() results, err := readSimple(ctx3, session, tx2) assertStatusCode(t, err, codes.OK) if diff := cmp.Diff(results, expected, protocmp.Transform()); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) t.Run("ReadBeforeWrite_AnotherTransaction", func(t *testing.T) { session := preareDBAndSession(t) tx1 := begin(session) tx2 := begin(session) defer commit(session, tx1) defer commit(session, tx2) expected := []*structpb.Value{ makeStringValue("100"), makeStringValue("zzz"), } // read in transaction2 results, err := readSimple(ctx, session, tx2) assertStatusCode(t, err, codes.OK) if diff := cmp.Diff(results, initialValueSet, protocmp.Transform()); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } // write in transaction1 (aborting tx1) _, err = s.ExecuteSql(ctx, &spannerpb.ExecuteSqlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx1.Id, }, }, Sql: `UPDATE Simple SET Value = "zzz" WHERE Id = 100`, Params: nil, ParamTypes: nil, }) assertStatusCode(t, err, codes.OK) if err := commit(session, tx1); err != nil { t.Fatalf("commit failed: %v", err) } // read in transaction fails because of abort _, err = readSimple(ctx, session, tx2) assertStatusCode(t, err, codes.Aborted) // read in another transaction succeeds after commit tx3 := begin(session) defer commit(session, tx3) results, err = readSimple(ctx, session, tx3) assertStatusCode(t, err, codes.OK) if diff := cmp.Diff(results, expected, protocmp.Transform()); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) t.Run("WriteBeforeWrite_AnotherTransaction", func(t *testing.T) { session := preareDBAndSession(t) tx1 := begin(session) tx2 := begin(session) defer commit(session, tx1) defer commit(session, tx2) expected1 := []*structpb.Value{ makeStringValue("100"), makeStringValue("xxx"), makeStringValue("101"), makeStringValue("yyy"), } // write in transaction2 _, err := s.ExecuteSql(ctx, &spannerpb.ExecuteSqlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx2.Id, }, }, Sql: `INSERT INTO Simple (Id, Value) VALUES(101, "yyy")`, }) assertStatusCode(t, err, codes.OK) // write in transaction1 timeouts because tx2 holds write lock ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) defer cancel() _, err = s.ExecuteSql(ctx, &spannerpb.ExecuteSqlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx1.Id, }, }, Sql: `UPDATE Simple SET Value = "zzz" WHERE Id = 100`, Params: nil, ParamTypes: nil, }) assertStatusCode(t, err, codes.DeadlineExceeded) // commit in transaction2 if err := commit(session, tx2); err != nil { t.Fatalf("commit failed: %v", err) } // read in another transaction succeeds after commit tx3 := begin(session) defer commit(session, tx3) results, err := readSimple(ctx, session, tx3) assertStatusCode(t, err, codes.OK) if diff := cmp.Diff(results, expected1, protocmp.Transform()); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) } func TestExecuteBatchDml_Success(t *testing.T) { ctx := context.Background() s := newTestServer() preareDBAndSession := func(t *testing.T) *spannerpb.Session { session, dbName := testCreateSession(t, s) db, ok := s.db[dbName] if !ok { t.Fatalf("database not found") } for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } for _, query := range []string{ `INSERT INTO Simple VALUES(100, "xxx")`, `INSERT INTO Simple VALUES(200, "yyy")`, `INSERT INTO Simple VALUES(300, "zzz")`, } { if _, err := db.db.ExecContext(ctx, query); err != nil { t.Fatalf("Insert failed: %v", err) } } return session } begin := func(session *spannerpb.Session) *spannerpb.Transaction { tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } return tx } commit := func(session *spannerpb.Session, tx *spannerpb.Transaction) error { _, err := s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_TransactionId{ TransactionId: tx.Id, }, Mutations: []*spannerpb.Mutation{}, }) return err } read := func(session *spannerpb.Session, tx *spannerpb.Transaction) ([][]*structpb.Value, error) { fake := NewFakeExecuteStreamingSqlServer(ctx) if err := s.StreamingRead(&spannerpb.ReadRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_SingleUse{ SingleUse: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadOnly_{ ReadOnly: &spannerpb.TransactionOptions_ReadOnly{}, }, }, }, }, Table: "Simple", Columns: []string{"Id", "Value"}, KeySet: &spannerpb.KeySet{All: true}, }, fake); err != nil { return nil, err } var results [][]*structpb.Value for _, set := range fake.sets { results = append(results, set.Values) } return results, nil } table := map[string]struct { stmts []*spannerpb.ExecuteBatchDmlRequest_Statement expectedSetCount int expectedStatusCode int32 expectedStatusMessage *regexp.Regexp expected [][]*structpb.Value }{ "UpdateSingle": { stmts: []*spannerpb.ExecuteBatchDmlRequest_Statement{ { Sql: `UPDATE Simple SET Value = "xyz" WHERE Id = 200`, }, }, expectedSetCount: 1, expectedStatusCode: 0, expectedStatusMessage: regexp.MustCompile(`^$`), expected: [][]*structpb.Value{ { makeStringValue("100"), makeStringValue("xxx"), makeStringValue("200"), makeStringValue("xyz"), makeStringValue("300"), makeStringValue("zzz"), }, }, }, "UpdateMulti": { stmts: []*spannerpb.ExecuteBatchDmlRequest_Statement{ { Sql: `UPDATE Simple SET Value = "xyz" WHERE Id = 200`, }, { Sql: `UPDATE Simple SET Value = "z" WHERE Id = 300`, }, }, expectedSetCount: 2, expectedStatusCode: 0, expectedStatusMessage: regexp.MustCompile(`^$`), expected: [][]*structpb.Value{ { makeStringValue("100"), makeStringValue("xxx"), makeStringValue("200"), makeStringValue("xyz"), makeStringValue("300"), makeStringValue("z"), }, }, }, "PartialSuccess": { stmts: []*spannerpb.ExecuteBatchDmlRequest_Statement{ { Sql: `UPDATE Simple SET Value = "xyz" WHERE Id = 200`, }, { Sql: `UPD`, }, }, expectedSetCount: 1, expectedStatusCode: int32(codes.InvalidArgument), expectedStatusMessage: regexp.MustCompile(`^Statement 1: .* is not valid DML`), expected: [][]*structpb.Value{ { makeStringValue("100"), makeStringValue("xxx"), makeStringValue("200"), makeStringValue("xyz"), makeStringValue("300"), makeStringValue("zzz"), }, }, }, "PartialSuccessStopped": { stmts: []*spannerpb.ExecuteBatchDmlRequest_Statement{ { Sql: `UPD`, }, { Sql: `UPDATE Simple SET Value = "xyz" WHERE Id = 200`, }, }, expectedSetCount: 0, expectedStatusCode: int32(codes.InvalidArgument), expectedStatusMessage: regexp.MustCompile(`^Statement 0: .* is not valid DML`), expected: [][]*structpb.Value{ { makeStringValue("100"), makeStringValue("xxx"), makeStringValue("200"), makeStringValue("yyy"), makeStringValue("300"), makeStringValue("zzz"), }, }, }, } for name, tc := range table { t.Run(name, func(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() session := preareDBAndSession(t) tx := begin(session) result, err := s.ExecuteBatchDml(ctx, &spannerpb.ExecuteBatchDmlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx.Id, }, }, Statements: tc.stmts, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if count := len(result.ResultSets); count != tc.expectedSetCount { t.Errorf("expect the number of ResultSets to be %v, but got %v", tc.expectedSetCount, count) } if result.Status.GetCode() != tc.expectedStatusCode { t.Errorf("expect status code to be %v, but got %v", tc.expectedStatusCode, result.Status.GetCode()) } if !tc.expectedStatusMessage.MatchString(result.Status.GetMessage()) { t.Errorf("unexpected status message: \n %q\n expected:\n %q", result.Status.GetMessage(), tc.expectedStatusMessage) } if err := commit(session, tx); err != nil { t.Fatalf("commit error: %v", err) } results, err := read(session, tx) if err != nil { t.Fatalf("read error: %v", err) } if diff := cmp.Diff(tc.expected, results, protocmp.Transform()); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) } } func TestExecuteBatchDml_Error(t *testing.T) { ctx := context.Background() s := newTestServer() begin := func(ctx context.Context, t *testing.T, session *spannerpb.Session) *spannerpb.Transaction { tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } return tx } t.Run("NoStatements", func(t *testing.T) { session, _ := testCreateSession(t, s) tx := begin(ctx, t, session) _, err := s.ExecuteBatchDml(ctx, &spannerpb.ExecuteBatchDmlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx.Id, }, }, Statements: []*spannerpb.ExecuteBatchDmlRequest_Statement{}, }) expected := "No statements in batch DML request." assertGRPCError(t, err, codes.InvalidArgument, expected) }) t.Run("RollbackAfterRollback", func(t *testing.T) { session, _ := testCreateSession(t, s) tx := begin(ctx, t, session) _, err := s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_TransactionId{ TransactionId: tx.Id, }, Mutations: []*spannerpb.Mutation{}, }) if err != nil { t.Fatalf("unexpected error: %v", err) } _, err = s.ExecuteBatchDml(ctx, &spannerpb.ExecuteBatchDmlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx.Id, }, }, Statements: []*spannerpb.ExecuteBatchDmlRequest_Statement{ {Sql: `UPDATE Simple SET Value = "x" WHERE Id = 1000`}, }, }) expected := "Cannot start a read or query within a transaction after Commit() or Rollback() has been called." assertGRPCError(t, err, codes.FailedPrecondition, expected) }) t.Run("CommitAfterRollback", func(t *testing.T) { session, _ := testCreateSession(t, s) tx := begin(ctx, t, session) _, err := s.Rollback(ctx, &spannerpb.RollbackRequest{ Session: session.Name, TransactionId: tx.Id, }) if err != nil { t.Fatalf("unexpected error: %v", err) } _, err = s.ExecuteBatchDml(ctx, &spannerpb.ExecuteBatchDmlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx.Id, }, }, Statements: []*spannerpb.ExecuteBatchDmlRequest_Statement{ {Sql: `UPDATE Simple SET Value = "x" WHERE Id = 1000`}, }, }) expected := "Cannot start a read or query within a transaction after Commit() or Rollback() has been called." assertGRPCError(t, err, codes.FailedPrecondition, expected) }) t.Run("ReadOnlyTransaction", func(t *testing.T) { session, _ := testCreateSession(t, s) tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadOnly_{ ReadOnly: &spannerpb.TransactionOptions_ReadOnly{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } _, err = s.ExecuteBatchDml(ctx, &spannerpb.ExecuteBatchDmlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx.Id, }, }, Statements: []*spannerpb.ExecuteBatchDmlRequest_Statement{ {Sql: `UPDATE Simple SET Value = "x" WHERE Id = 1000`}, }, }) expected := "DML statements can only be performed in a read-write transaction." assertGRPCError(t, err, codes.FailedPrecondition, expected) }) t.Run("SingleUseTransaction", func(t *testing.T) { session, _ := testCreateSession(t, s) _, err := s.ExecuteBatchDml(ctx, &spannerpb.ExecuteBatchDmlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_SingleUse{ SingleUse: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }, }, Statements: []*spannerpb.ExecuteBatchDmlRequest_Statement{ {Sql: `UPDATE Simple SET Value = "x" WHERE Id = 1000`}, }, }) expected := "DML statements may not be performed in single-use transactions, to avoid replay." assertGRPCError(t, err, codes.InvalidArgument, expected) }) } func TestCommit(t *testing.T) { ctx := context.Background() s := newTestServer() t.Run("Success_MultiUseTransaction", func(t *testing.T) { session, _ := testCreateSession(t, s) tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } _, err = s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_TransactionId{ TransactionId: tx.Id, }, Mutations: []*spannerpb.Mutation{}, }) if err != nil { t.Fatalf("unexpected error: %v", err) } }) t.Run("Success_SingleUse", func(t *testing.T) { session, _ := testCreateSession(t, s) _, err := s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_SingleUseTransaction{ SingleUseTransaction: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }, Mutations: []*spannerpb.Mutation{}, }) if err != nil { t.Fatalf("unexpected error: %v", err) } }) t.Run("CommitAfterCommit", func(t *testing.T) { session, _ := testCreateSession(t, s) tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } _, err = s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_TransactionId{ TransactionId: tx.Id, }, Mutations: []*spannerpb.Mutation{}, }) if err != nil { t.Fatalf("unexpected error: %v", err) } _, err = s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_TransactionId{ TransactionId: tx.Id, }, Mutations: []*spannerpb.Mutation{}, }) if err != nil { t.Fatalf("unexpected error: %v", err) } }) t.Run("CommitAfterRollback", func(t *testing.T) { session, _ := testCreateSession(t, s) tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } _, err = s.Rollback(ctx, &spannerpb.RollbackRequest{ Session: session.Name, TransactionId: tx.Id, }) if err != nil { t.Fatalf("unexpected error: %v", err) } _, err = s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_TransactionId{ TransactionId: tx.Id, }, Mutations: []*spannerpb.Mutation{}, }) expected := "Cannot commit a transaction after Rollback() has been called." assertGRPCError(t, err, codes.FailedPrecondition, expected) }) t.Run("ReadOnlyTransaction", func(t *testing.T) { session, _ := testCreateSession(t, s) tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadOnly_{ ReadOnly: &spannerpb.TransactionOptions_ReadOnly{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } _, err = s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_TransactionId{ TransactionId: tx.Id, }, Mutations: []*spannerpb.Mutation{}, }) expected := "Cannot commit a read-only transaction." assertGRPCError(t, err, codes.FailedPrecondition, expected) }) t.Run("SingleReadOnlyTransaction", func(t *testing.T) { session, _ := testCreateSession(t, s) _, err := s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_SingleUseTransaction{ SingleUseTransaction: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadOnly_{ ReadOnly: &spannerpb.TransactionOptions_ReadOnly{}, }, }, }, Mutations: []*spannerpb.Mutation{}, }) expected := "Cannot commit a single-use read-only transaction." assertGRPCError(t, err, codes.FailedPrecondition, expected) }) t.Run("TransactionInDifferentSession", func(t *testing.T) { session, _ := testCreateSession(t, s) tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } session2, _ := testCreateSession(t, s) _, err = s.Commit(ctx, &spannerpb.CommitRequest{ Session: session2.Name, Transaction: &spannerpb.CommitRequest_TransactionId{ TransactionId: tx.Id, }, Mutations: []*spannerpb.Mutation{}, }) assertGRPCError(t, err, codes.InvalidArgument, "Transaction was started in a different session") }) t.Run("Invalidated_Commit", func(t *testing.T) { session, _ := testCreateSession(t, s) tx := testInvalidatedTransaction(t, s, session) _, err := s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_TransactionId{ TransactionId: tx.Id, }, Mutations: []*spannerpb.Mutation{}, }) expected := "This transaction has been invalidated by a later transaction in the same session." assertGRPCError(t, err, codes.FailedPrecondition, expected) }) } func TestCommitMutations(t *testing.T) { ctx := context.Background() s := newTestServer() session, dbName := testCreateSession(t, s) for _, schema := range allSchema { ddls := parseDDL(t, schema) for _, ddl := range ddls { if err := s.ApplyDDL(ctx, dbName, ddl); err != nil { t.Fatalf("ApplyDDL failed: %v", err) } } } singleTx := &spannerpb.CommitRequest_SingleUseTransaction{ SingleUseTransaction: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, } _, err := s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: singleTx, Mutations: []*spannerpb.Mutation{ { Operation: &spannerpb.Mutation_Insert{ Insert: &spannerpb.Mutation_Write{ Table: "Simple", Columns: []string{"Id", "Value"}, Values: []*structpb.ListValue{ { Values: []*structpb.Value{ makeStringValue("100"), makeStringValue("aaa"), }, }, { Values: []*structpb.Value{ makeStringValue("200"), makeStringValue("bbb"), }, }, { Values: []*structpb.Value{ makeStringValue("300"), makeStringValue("ccc"), }, }, { Values: []*structpb.Value{ makeStringValue("400"), makeStringValue("ddd"), }, }, }, }, }, }, }, }) if err != nil { t.Fatalf("Commit1 failed: %v", err) } singleTx2 := &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_SingleUse{ SingleUse: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }, } fake := NewFakeExecuteStreamingSqlServer(ctx) err = s.StreamingRead(&spannerpb.ReadRequest{ Session: session.Name, Transaction: singleTx2, Table: "Simple", Columns: []string{"Id"}, KeySet: &spannerpb.KeySet{All: true}, }, fake) if err != nil { t.Fatalf("Read failed: %v", err) } _, err = s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: singleTx, Mutations: []*spannerpb.Mutation{ { Operation: &spannerpb.Mutation_Insert{ Insert: &spannerpb.Mutation_Write{ Table: "Simple", Columns: []string{"Id", "Value"}, Values: []*structpb.ListValue{ { Values: []*structpb.Value{ makeStringValue("500"), makeStringValue("eee"), }, }, }, }, }, }, { Operation: &spannerpb.Mutation_Update{ Update: &spannerpb.Mutation_Write{ Table: "Simple", Columns: []string{"Id", "Value"}, Values: []*structpb.ListValue{ { Values: []*structpb.Value{ makeStringValue("100"), makeStringValue("aaa2"), }, }, }, }, }, }, { Operation: &spannerpb.Mutation_Replace{ Replace: &spannerpb.Mutation_Write{ Table: "Simple", Columns: []string{"Id", "Value"}, Values: []*structpb.ListValue{ { Values: []*structpb.Value{ makeStringValue("200"), makeStringValue("bbb2"), }, }, }, }, }, }, { Operation: &spannerpb.Mutation_InsertOrUpdate{ InsertOrUpdate: &spannerpb.Mutation_Write{ Table: "Simple", Columns: []string{"Id", "Value"}, Values: []*structpb.ListValue{ { Values: []*structpb.Value{ makeStringValue("300"), makeStringValue("ccc2"), }, }, }, }, }, }, { Operation: &spannerpb.Mutation_Delete_{ Delete: &spannerpb.Mutation_Delete{ Table: "Simple", KeySet: &spannerpb.KeySet{ Keys: []*structpb.ListValue{ { Values: []*structpb.Value{ makeStringValue("400"), }, }, }, }, }, }, }, }, }) if err != nil { t.Fatalf("Commit2 failed: %v", err) } fake2 := NewFakeExecuteStreamingSqlServer(ctx) err = s.StreamingRead(&spannerpb.ReadRequest{ Session: session.Name, Transaction: singleTx2, Table: "Simple", Columns: []string{"Id", "Value"}, KeySet: &spannerpb.KeySet{All: true}, }, fake2) if err != nil { t.Fatalf("Read failed: %v", err) } var results []*structpb.Value for _, set := range fake2.sets { results = append(results, set.Values...) } expected := []*structpb.Value{ makeStringValue("100"), makeStringValue("aaa2"), makeStringValue("200"), makeStringValue("bbb2"), makeStringValue("300"), makeStringValue("ccc2"), makeStringValue("500"), makeStringValue("eee"), } if diff := cmp.Diff(expected, results, protocmp.Transform()); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } } func TestCommitMutations_AtomicOperation(t *testing.T) { ctx := context.Background() s := newTestServer() preareDBAndSession := func(t *testing.T) *spannerpb.Session { session, dbName := testCreateSession(t, s) db, ok := s.db[dbName] if !ok { t.Fatalf("database not found") } for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } for _, query := range []string{ `INSERT INTO Simple VALUES(100, "xxx")`, } { if _, err := db.db.ExecContext(ctx, query); err != nil { t.Fatalf("Insert failed: %v", err) } } return session } session := preareDBAndSession(t) begin := func() *spannerpb.Transaction { tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } return tx } readSimple := func(t *testing.T) [][]*structpb.Value { fake := NewFakeExecuteStreamingSqlServer(ctx) err := s.StreamingRead(&spannerpb.ReadRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_SingleUse{ SingleUse: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }, }, Table: "Simple", Columns: []string{"Id", "Value"}, KeySet: &spannerpb.KeySet{All: true}, }, fake) if err != nil { t.Fatalf("StreamingRead error: %v", err) } var results [][]*structpb.Value for _, set := range fake.sets { results = append(results, set.Values) } return results } expected := [][]*structpb.Value{ { makeStringValue("100"), makeStringValue("xxx"), }, } t.Run("InsertConflict", func(t *testing.T) { tx := begin() _, err := s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_TransactionId{ TransactionId: tx.Id, }, Mutations: []*spannerpb.Mutation{ { Operation: &spannerpb.Mutation_Insert{ Insert: &spannerpb.Mutation_Write{ Table: "Simple", Columns: []string{"Id", "Value"}, Values: []*structpb.ListValue{ { Values: []*structpb.Value{ makeStringValue("300"), makeStringValue("ccc2"), }, }, }, }, }, }, { Operation: &spannerpb.Mutation_Insert{ Insert: &spannerpb.Mutation_Write{ Table: "Simple", Columns: []string{"Id", "Value"}, Values: []*structpb.ListValue{ { Values: []*structpb.Value{ makeStringValue("100"), // already exists makeStringValue("ccc2"), }, }, }, }, }, }, }, }) assertStatusCode(t, err, codes.AlreadyExists) results := readSimple(t) if diff := cmp.Diff(results, expected, protocmp.Transform()); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) } func TestRollback(t *testing.T) { ctx := context.Background() s := newTestServer() t.Run("Success", func(t *testing.T) { session, _ := testCreateSession(t, s) tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } _, err = s.Rollback(ctx, &spannerpb.RollbackRequest{ Session: session.Name, TransactionId: tx.Id, }) if err != nil { t.Fatalf("unexpected error: %v", err) } }) t.Run("RollbackAfterRollback", func(t *testing.T) { session, _ := testCreateSession(t, s) tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } _, err = s.Rollback(ctx, &spannerpb.RollbackRequest{ Session: session.Name, TransactionId: tx.Id, }) if err != nil { t.Fatalf("unexpected error: %v", err) } _, err = s.Rollback(ctx, &spannerpb.RollbackRequest{ Session: session.Name, TransactionId: tx.Id, }) if err != nil { t.Fatalf("unexpected error: %v", err) } }) t.Run("RollbackAfterCommit", func(t *testing.T) { session, _ := testCreateSession(t, s) tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } _, err = s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_TransactionId{ TransactionId: tx.Id, }, Mutations: []*spannerpb.Mutation{}, }) if err != nil { t.Fatalf("unexpected error: %v", err) } _, err = s.Rollback(ctx, &spannerpb.RollbackRequest{ Session: session.Name, TransactionId: tx.Id, }) expected := "Cannot rollback a transaction after Commit() has been called." assertGRPCError(t, err, codes.FailedPrecondition, expected) }) t.Run("TransactionInDifferentSession", func(t *testing.T) { session, _ := testCreateSession(t, s) tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } session2, _ := testCreateSession(t, s) _, err = s.Rollback(ctx, &spannerpb.RollbackRequest{ Session: session2.Name, TransactionId: tx.Id, }) assertGRPCError(t, err, codes.InvalidArgument, "Transaction was started in a different session") }) t.Run("InvalidatedTransaction", func(t *testing.T) { session, _ := testCreateSession(t, s) tx := testInvalidatedTransaction(t, s, session) _, err := s.Rollback(ctx, &spannerpb.RollbackRequest{ Session: session.Name, TransactionId: tx.Id, }) expected := "This transaction has been invalidated by a later transaction in the same session." assertGRPCError(t, err, codes.FailedPrecondition, expected) }) } func TestTransaction(t *testing.T) { ctx := context.Background() s := newTestServer() preareDBAndSession := func(t *testing.T) *spannerpb.Session { session, dbName := testCreateSession(t, s) db, ok := s.db[dbName] if !ok { t.Fatalf("database not found") } for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } for _, query := range []string{ `INSERT INTO Simple VALUES(100, "xxx")`, } { if _, err := db.db.ExecContext(ctx, query); err != nil { t.Fatalf("Insert failed: %v", err) } } return session } testcase := map[string]func(session string, tx *spannerpb.TransactionSelector, stream spannerpb.Spanner_StreamingReadServer) error{ "ExecuteStreamingSql": func(session string, tx *spannerpb.TransactionSelector, stream spannerpb.Spanner_StreamingReadServer) error { return s.ExecuteStreamingSql(&spannerpb.ExecuteSqlRequest{ Session: session, Transaction: tx, Sql: "SELECT 1", }, stream) }, "StreamingRead": func(session string, tx *spannerpb.TransactionSelector, stream spannerpb.Spanner_StreamingReadServer) error { return s.StreamingRead(&spannerpb.ReadRequest{ Session: session, Transaction: tx, Table: "Simple", Columns: []string{"Id"}, KeySet: &spannerpb.KeySet{All: true}, }, stream) }, "ExecuteSql": func(session string, tx *spannerpb.TransactionSelector, stream spannerpb.Spanner_StreamingReadServer) error { r, err := s.ExecuteSql(context.Background(), &spannerpb.ExecuteSqlRequest{ Session: session, Transaction: tx, Sql: `UPDATE Simple SET Value = "xxx" WHERE Id = 10000000`, }) if err == nil { // set data with stream to make test easir stream.Send(&spannerpb.PartialResultSet{ Metadata: r.Metadata, Stats: r.Stats, }) } return err }, "ExecuteBatchDml": func(session string, tx *spannerpb.TransactionSelector, stream spannerpb.Spanner_StreamingReadServer) error { r, err := s.ExecuteBatchDml(context.Background(), &spannerpb.ExecuteBatchDmlRequest{ Session: session, Transaction: tx, Statements: []*spannerpb.ExecuteBatchDmlRequest_Statement{ {Sql: `UPDATE Simple SET Value = "xxx" WHERE Id = 10000000`}, }, }) if err == nil { // set data with stream to make test easir for i := range r.ResultSets { stream.Send(&spannerpb.PartialResultSet{ Metadata: r.ResultSets[i].Metadata, Stats: r.ResultSets[i].Stats, }) } } return err }, } for name, fn := range testcase { t.Run(name, func(t *testing.T) { t.Run("UseReturnedTransaction", func(t *testing.T) { session := preareDBAndSession(t) fake := &fakeExecuteStreamingSqlServer{} begin := &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Begin{ Begin: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }, } if err := fn(session.Name, begin, fake); err != nil { t.Fatalf("unexpected error: %v", err) } newtx := fake.sets[0].GetMetadata().GetTransaction() if newtx == nil { t.Fatalf("transaction should not be nil") } fake2 := &fakeExecuteStreamingSqlServer{} txsel := &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: newtx.Id, }, } if err := fn(session.Name, txsel, fake2); err != nil { t.Fatalf("unexpected error: %v", err) } }) t.Run("AfterCommit", func(t *testing.T) { session, _ := testCreateSession(t, s) tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } _, err = s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_TransactionId{ TransactionId: tx.Id, }, Mutations: []*spannerpb.Mutation{}, }) if err != nil { t.Fatalf("unexpected error: %v", err) } fake := &fakeExecuteStreamingSqlServer{} txsel := &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx.Id, }, } err = fn(session.Name, txsel, fake) expected := "Cannot start a read or query within a transaction after Commit() or Rollback() has been called." assertGRPCError(t, err, codes.FailedPrecondition, expected) }) t.Run("InvalidatedTransaction", func(t *testing.T) { session, _ := testCreateSession(t, s) tx := testInvalidatedTransaction(t, s, session) fake := &fakeExecuteStreamingSqlServer{} txsel := &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx.Id, }, } err := fn(session.Name, txsel, fake) expected := "This transaction has been invalidated by a later transaction in the same session." assertGRPCError(t, err, codes.FailedPrecondition, expected) }) }) } } func TestUpdateDatabaseDdl(t *testing.T) { ctx := context.Background() s := newTestServer() _, dbName := testCreateSession(t, s) stmts := []string{`CREATE TABLE Test (Id INT64) PRIMARY KEY(Id)`} op, err := s.UpdateDatabaseDdl(ctx, &adminv1pb.UpdateDatabaseDdlRequest{ Database: dbName, Statements: stmts, }) if err != nil { t.Fatalf("unexpected error: %v", err) } op2, err := s.WaitOperation(ctx, &lropb.WaitOperationRequest{ Name: op.Name, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if !op2.Done { t.Errorf("should be success") } } func TestCreateDatabase(t *testing.T) { ctx := context.Background() s := newTestServer() projectID, instanceID := "fake", "fake" parent := fmt.Sprintf("projects/%s/instances/%s", projectID, instanceID) extraStmts := []string{` CREATE TABLE Ids ( Id INT64 NOT NULL, ) PRIMARY KEY(Id)`} alreadyExistsDatabase := "already-exists" s.createDatabase(fmt.Sprintf("%s/databases/%s", parent, alreadyExistsDatabase)) t.Run("Success", func(t *testing.T) { databaseID := strconv.Itoa(int(time.Now().Unix())) op, err := s.CreateDatabase(ctx, &adminv1pb.CreateDatabaseRequest{ Parent: parent, CreateStatement: fmt.Sprintf("CREATE DATABASE `%s`", databaseID), ExtraStatements: extraStmts, }) if err != nil { t.Fatalf("failed to create database: %s", err) } if _, ok := s.db[fmt.Sprintf("%s/databases/%s", parent, databaseID)]; !ok { t.Fatal("failed to get database from map") } if !op.GetDone() { t.Fatal("the operation has not finished yet") } var db adminv1pb.Database if err := anypb.UnmarshalTo(op.GetResponse(), &db, proto.UnmarshalOptions{DiscardUnknown: true}); err != nil { t.Fatalf("failed to unmarshal response: %s", err) } if got, expect := db.GetName(), fmt.Sprintf("%s/databases/%s", parent, databaseID); got != expect { t.Fatalf("expected %s but got %s", expect, db.GetName()) } }) t.Run("Failure", func(t *testing.T) { tests := map[string]struct { req *adminv1pb.CreateDatabaseRequest expectCode codes.Code }{ "invalid parent": { req: &adminv1pb.CreateDatabaseRequest{ Parent: "INVALID", CreateStatement: fmt.Sprintf("CREATE DATABASE `%d`", time.Now().Unix()), ExtraStatements: extraStmts, }, expectCode: codes.InvalidArgument, }, "invalid create statement": { req: &adminv1pb.CreateDatabaseRequest{ Parent: parent, CreateStatement: "INVALID", ExtraStatements: extraStmts, }, expectCode: codes.InvalidArgument, }, "create statement is not CREATE DATABASE": { req: &adminv1pb.CreateDatabaseRequest{ Parent: parent, CreateStatement: extraStmts[0], ExtraStatements: extraStmts, }, expectCode: codes.InvalidArgument, }, "invalid extra statements": { req: &adminv1pb.CreateDatabaseRequest{ Parent: parent, CreateStatement: fmt.Sprintf("CREATE DATABASE `%d`", time.Now().Unix()), ExtraStatements: []string{"INVALID"}, }, expectCode: codes.InvalidArgument, }, "already exists": { req: &adminv1pb.CreateDatabaseRequest{ Parent: parent, CreateStatement: fmt.Sprintf("CREATE DATABASE `%s`", alreadyExistsDatabase), ExtraStatements: extraStmts, }, expectCode: codes.AlreadyExists, }, } for name, test := range tests { test := test t.Run(name, func(t *testing.T) { _, err := s.CreateDatabase(ctx, test.req) if got, expect := status.Convert(err).Code(), test.expectCode; got != expect { t.Errorf("expect error code %s but got %s", expect, got) } }) } }) } func TestDropDatabase(t *testing.T) { ctx := context.Background() s := newTestServer() database := "projects/fake/instances/fake/databases/fake" t.Run("Success", func(t *testing.T) { _, err := s.createDatabase(database) if err != nil { t.Fatalf("failed to create database: %s", err) } if _, err := s.DropDatabase(ctx, &adminv1pb.DropDatabaseRequest{ Database: database, }); err != nil { t.Fatalf("failed to drop database: %s", err) } _, err = s.GetDatabase(ctx, &adminv1pb.GetDatabaseRequest{ Name: database, }) st := status.Convert(err) if st.Code() != codes.NotFound { t.Fatalf("failed to drop database: %s", err) } expectedRI := &errdetails.ResourceInfo{ ResourceType: "type.googleapis.com/google.spanner.admin.database.v1.Database", ResourceName: database, Description: "Database does not exist.", } assertResourceInfo(t, err, expectedRI) // DropDatabase returns no error even if the database is not exist if _, err := s.DropDatabase(ctx, &adminv1pb.DropDatabaseRequest{ Database: database, }); err != nil { t.Fatalf("failed to drop database: %s", err) } }) t.Run("Failure", func(t *testing.T) { tests := map[string]struct { req *adminv1pb.DropDatabaseRequest expectCode codes.Code }{ "invalid database": { req: &adminv1pb.DropDatabaseRequest{ Database: "INVALID", }, expectCode: codes.InvalidArgument, }, } for name, test := range tests { test := test t.Run(name, func(t *testing.T) { _, err := s.DropDatabase(ctx, test.req) if got, expect := status.Convert(err).Code(), test.expectCode; got != expect { t.Errorf("expect error code %s but got %s", expect, got) } }) } }) } func TestListDatabases(t *testing.T) { ctx := context.Background() s := newTestServer() databases := []string{ "projects/fake/instances/fake/databases/fake", "projects/fake/instances/fake/databases/fake2", "projects/fake/instances/fake/databases/fake3", } for _, name := range databases { _, err := s.createDatabase(name) if err != nil { t.Fatalf("failed to create database: %s", err) } } res, err := s.ListDatabases(ctx, &adminv1pb.ListDatabasesRequest{ Parent: "projects/fake/instances/fake", }) if err != nil { t.Fatalf("failed to list databases: %s", err) } sort.Slice(res.Databases, func(i, j int) bool { return res.Databases[i].Name < res.Databases[j].Name }) expected := []*adminv1pb.Database{ { Name: "projects/fake/instances/fake/databases/fake", State: adminv1pb.Database_READY, }, { Name: "projects/fake/instances/fake/databases/fake2", State: adminv1pb.Database_READY, }, { Name: "projects/fake/instances/fake/databases/fake3", State: adminv1pb.Database_READY, }, } if diff := cmp.Diff(expected, res.Databases, protocmp.Transform()); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } } ================================================ FILE: server/server_transaction_test.go ================================================ // Copyright 2019 Masahiro Sano // // 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 server import ( "context" "fmt" "strconv" "sync" "testing" "time" spannerpb "google.golang.org/genproto/googleapis/spanner/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" structpb "google.golang.org/protobuf/types/known/structpb" ) func TestReadAndWriteTransaction_Aborted(t *testing.T) { if testing.Short() { t.Skip("skip long test") } ctx := context.Background() s := newTestServer() preareDBAndSession := func(t *testing.T) *spannerpb.Session { session, dbName := testCreateSession(t, s) db, ok := s.db[dbName] if !ok { t.Fatalf("database not found") } for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } for _, query := range []string{ `INSERT INTO Simple VALUES(100, "xxx")`, } { if _, err := db.db.ExecContext(ctx, query); err != nil { t.Fatalf("Insert failed: %v", err) } } return session } session := preareDBAndSession(t) begin := func() *spannerpb.Transaction { tx, err := s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } return tx } rollback := func(tx *spannerpb.Transaction) { _, _ = s.Rollback(ctx, &spannerpb.RollbackRequest{ Session: session.Name, TransactionId: tx.Id, }) // ignore error } readSimple := func(tx *spannerpb.Transaction) error { fake := NewFakeExecuteStreamingSqlServer(ctx) err := s.StreamingRead(&spannerpb.ReadRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx.Id, }, }, Table: "Simple", Columns: []string{"Id"}, KeySet: &spannerpb.KeySet{All: true}, }, fake) return err } readFT := func(tx *spannerpb.Transaction) error { fake := NewFakeExecuteStreamingSqlServer(ctx) err := s.StreamingRead(&spannerpb.ReadRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx.Id, }, }, Table: "FullTypes", Columns: []string{"PKey"}, KeySet: &spannerpb.KeySet{All: true}, }, fake) return err } querySimple := func(tx *spannerpb.Transaction) error { fake := NewFakeExecuteStreamingSqlServer(ctx) err := s.ExecuteStreamingSql(&spannerpb.ExecuteSqlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx.Id, }, }, Sql: `SELECT Id FROM Simple`, }, fake) return err } queryFT := func(tx *spannerpb.Transaction) error { fake := NewFakeExecuteStreamingSqlServer(ctx) err := s.ExecuteStreamingSql(&spannerpb.ExecuteSqlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx.Id, }, }, Sql: `SELECT PKey FROM FullTypes`, }, fake) return err } commitSimple := func(tx *spannerpb.Transaction) error { _, err := s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_TransactionId{ TransactionId: tx.Id, }, Mutations: []*spannerpb.Mutation{ { Operation: &spannerpb.Mutation_InsertOrUpdate{ InsertOrUpdate: &spannerpb.Mutation_Write{ Table: "Simple", Columns: []string{"Id", "Value"}, Values: []*structpb.ListValue{ { Values: []*structpb.Value{ makeStringValue("300"), makeStringValue("ccc2"), }, }, }, }, }, }, }, }) return err } commitFT := func(tx *spannerpb.Transaction) error { _, err := s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_TransactionId{ TransactionId: tx.Id, }, Mutations: []*spannerpb.Mutation{ { Operation: &spannerpb.Mutation_InsertOrUpdate{ InsertOrUpdate: &spannerpb.Mutation_Write{ Table: "FullTypes", Columns: fullTypesKeys, Values: []*structpb.ListValue{ { Values: []*structpb.Value{ makeStringValue("xxx"), // PKey STRING(32) NOT NULL, makeStringValue("xxx"), // FTString STRING(32) NOT NULL, makeNullValue(), // FTStringNull STRING(32), makeBoolValue(true), // FTBool BOOL NOT NULL, makeNullValue(), // FTBoolNull BOOL, makeStringValue("eHh4"), // FTBytes BYTES(32) NOT NULL, makeNullValue(), // FTBytesNull BYTES(32), makeStringValue("2012-03-04T12:34:56.123456789Z"), // FTTimestamp TIMESTAMP NOT NULL, makeNullValue(), // FTTimestampNull TIMESTAMP, makeStringValue("100"), // FTInt INT64 NOT NULL, makeNullValue(), // FTIntNull INT64, makeNumberValue(0.5), // FTFloat FLOAT64 NOT NULL, makeNullValue(), // FTFloatNull FLOAT64, makeStringValue("2012-03-04"), // FTDate DATE NOT NULL, makeNullValue(), // FTDateNull DATE, }, }, }, }, }, }, }, }) return err } readPattern := []struct { name string readSimple func(tx *spannerpb.Transaction) error readFT func(tx *spannerpb.Transaction) error }{ {"Read", readSimple, readFT}, {"Query", querySimple, queryFT}, } t.Run("ReadLockAborted", func(t *testing.T) { for _, tc := range readPattern { t.Run(tc.name, func(t *testing.T) { tx1 := begin() tx2 := begin() defer rollback(tx1) defer rollback(tx2) assertStatusCode(t, tc.readSimple(tx1), codes.OK) assertStatusCode(t, commitSimple(tx2), codes.OK) assertStatusCode(t, tc.readSimple(tx1), codes.Aborted) }) } }) t.Run("ReadLock_DifferentTable", func(t *testing.T) { for _, tc := range readPattern { t.Run(tc.name, func(t *testing.T) { tx1 := begin() tx2 := begin() tx3 := begin() defer rollback(tx1) defer rollback(tx2) defer rollback(tx3) assertStatusCode(t, tc.readSimple(tx1), codes.OK) assertStatusCode(t, tc.readFT(tx2), codes.OK) assertStatusCode(t, commitSimple(tx3), codes.OK) assertStatusCode(t, tc.readSimple(tx1), codes.Aborted) assertStatusCode(t, tc.readFT(tx2), codes.OK) }) } }) t.Run("WriteTest", func(t *testing.T) { for _, tc := range readPattern { t.Run(tc.name, func(t *testing.T) { tx1 := begin() tx2 := begin() defer rollback(tx1) defer rollback(tx2) assertStatusCode(t, tc.readSimple(tx1), codes.OK) assertStatusCode(t, tc.readFT(tx2), codes.OK) assertStatusCode(t, commitSimple(tx1), codes.OK) assertStatusCode(t, commitFT(tx2), codes.OK) }) } }) t.Run("ReadAbortedWhileReading", func(t *testing.T) { for _, tc := range readPattern { t.Run(tc.name, func(t *testing.T) { for i := 0; i < 100; i++ { tx1 := begin() tx2 := begin() done := make(chan struct{}) errCh := make(chan error, 2) assertStatusCode(t, tc.readSimple(tx1), codes.OK) // read lock first go func() { for { time.Sleep(100 * time.Microsecond) // try to happen aborted while reading err := tc.readSimple(tx1) code := status.Code(err) if code == codes.OK { continue } if code == codes.Aborted { close(done) return } errCh <- err close(done) return } }() time.Sleep(time.Duration(i/10+1) * time.Millisecond) assertStatusCode(t, commitSimple(tx2), codes.OK) <-done select { case err := <-errCh: if err != nil { t.Fatalf("error: %v", err) } default: } } }) } }) } func TestReadAndWriteTransaction_AtomicUpdate(t *testing.T) { if testing.Short() { t.Skip("skip long test") } ctx, cancel := context.WithCancel(context.Background()) defer cancel() s := newTestServer() preareDB := func(t *testing.T) string { _, dbName := testCreateSession(t, s) db, ok := s.db[dbName] if !ok { t.Fatalf("database not found") } for _, s := range allSchema { ddls := parseDDL(t, s) for _, ddl := range ddls { db.ApplyDDL(ctx, ddl) } } return dbName } createSession := func(t *testing.T, dbName string) *spannerpb.Session { session, err := s.CreateSession(context.Background(), &spannerpb.CreateSessionRequest{ Database: dbName, }) if err != nil { t.Fatalf("failed to create session: %v", err) } return session } begin := func(session *spannerpb.Session) (*spannerpb.Transaction, error) { return s.BeginTransaction(ctx, &spannerpb.BeginTransactionRequest{ Session: session.Name, Options: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }) } read := func(session *spannerpb.Session, tx *spannerpb.Transaction, pKey string, sql bool) (int, error) { var txSel *spannerpb.TransactionSelector if tx == nil { txSel = &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_SingleUse{ SingleUse: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }, } } else { txSel = &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx.Id, }, } } fake := NewFakeExecuteStreamingSqlServer(ctx) if sql { err := s.ExecuteStreamingSql(&spannerpb.ExecuteSqlRequest{ Session: session.Name, Transaction: txSel, Sql: `SELECT Value FROM Simple WHERE Id = @key`, ParamTypes: map[string]*spannerpb.Type{ "key": &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, Params: &structpb.Struct{ Fields: map[string]*structpb.Value{ "key": makeStringValue(pKey), }, }, }, fake) if err != nil { return 0, err } } else { err := s.StreamingRead(&spannerpb.ReadRequest{ Session: session.Name, Transaction: txSel, Table: "Simple", Columns: []string{"Value"}, KeySet: &spannerpb.KeySet{ Keys: []*structpb.ListValue{ { Values: []*structpb.Value{ makeStringValue(pKey), }, }, }, }, }, fake) if err != nil { return 0, err } } var results [][]*structpb.Value for _, set := range fake.sets { results = append(results, set.Values) } if len(results) != 1 { return 0, fmt.Errorf("results should be 1 record but got %v", len(results)) } v := results[0][0].Kind.(*structpb.Value_StringValue).StringValue n, _ := strconv.Atoi(v) return n, nil } commit := func(session *spannerpb.Session, tx *spannerpb.Transaction, pKey string, sql bool, next int) error { var mus []*spannerpb.Mutation if sql { _, err := s.ExecuteSql(ctx, &spannerpb.ExecuteSqlRequest{ Session: session.Name, Transaction: &spannerpb.TransactionSelector{ Selector: &spannerpb.TransactionSelector_Id{ Id: tx.Id, }, }, Sql: fmt.Sprintf(`UPDATE Simple Set Value = "%d" WHERE Id = %s`, next, pKey), }) if err != nil { return err } } else { mus = []*spannerpb.Mutation{ { Operation: &spannerpb.Mutation_Update{ Update: &spannerpb.Mutation_Write{ Table: "Simple", Columns: []string{"Id", "Value"}, Values: []*structpb.ListValue{ { Values: []*structpb.Value{ makeStringValue(pKey), makeStringValue(fmt.Sprint(next)), }, }, }, }, }, }, } } _, err := s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_TransactionId{ TransactionId: tx.Id, }, Mutations: mus, }) return err } dbname := preareDB(t) session := createSession(t, dbname) initialValue := 100 table := []struct { pKey string concurrency int // should be under 32 tries int }{ {pKey: "1000", concurrency: 5, tries: 3}, {pKey: "1001", concurrency: 10, tries: 10}, {pKey: "1002", concurrency: 20, tries: 10}, {pKey: "1003", concurrency: 50, tries: 5}, // {pKey: "1004", concurrency: 100, tries: 30}, } for _, tc := range table { pKey := fmt.Sprint(tc.pKey) concurrency := tc.concurrency tries := tc.tries t.Run(fmt.Sprintf("KEY%s_Con%d_Try%d", pKey, concurrency, tries), func(t *testing.T) { _, err := s.Commit(ctx, &spannerpb.CommitRequest{ Session: session.Name, Transaction: &spannerpb.CommitRequest_SingleUseTransaction{ SingleUseTransaction: &spannerpb.TransactionOptions{ Mode: &spannerpb.TransactionOptions_ReadWrite_{ ReadWrite: &spannerpb.TransactionOptions_ReadWrite{}, }, }, }, Mutations: []*spannerpb.Mutation{ { Operation: &spannerpb.Mutation_Insert{ Insert: &spannerpb.Mutation_Write{ Table: "Simple", Columns: []string{"Id", "Value"}, Values: []*structpb.ListValue{ { Values: []*structpb.Value{ makeStringValue(pKey), makeStringValue(fmt.Sprint(initialValue)), }, }, }, }, }, }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } errCh := make(chan error, concurrency*tries) wg := &sync.WaitGroup{} readerDone := make(chan struct{}) for i := 0; i < concurrency; i++ { wg.Add(1) go func(me int) { defer wg.Done() // create own session session := createSession(t, dbname) err := func() error { try := 0 for { select { case <-ctx.Done(): return ctx.Err() default: } // begin transaction tx, err := begin(session) if err != nil { return fmt.Errorf("begin: %v", err) } // read the current value useSql := (me % 2) == 0 n, err := read(session, tx, pKey, useSql) if err != nil { if code := status.Code(err); code == codes.Aborted { continue } return fmt.Errorf("[%s] read: %v", string(tx.Id), err) } next := n + 1 // write +1 value if err := commit(session, tx, pKey, useSql, next); err != nil { if code := status.Code(err); code == codes.Aborted { continue } return fmt.Errorf("[%s] commit: %v", string(tx.Id), err) } try++ if try == tries { return nil } } }() if err != nil { errCh <- err cancel() } }(i) } go func() { // create own session session := createSession(t, dbname) readLoop: for { select { case <-readerDone: break readLoop default: } if _, err := read(session, nil, pKey, false); err != nil { if code := status.Code(err); code == codes.Aborted { continue } errCh <- err cancel() break } } }() wg.Wait() close(readerDone) var errs []error L: for { select { case err := <-errCh: if err != nil { errs = append(errs, err) } cancel() default: break L } } if len(errs) != 0 { for _, err := range errs { t.Errorf("error %v", err) } t.FailNow() } n, err := read(session, nil, pKey, false) if err != nil { t.Fatalf("read error %v", err) } expected := initialValue + tries*concurrency if n != expected { t.Errorf("expect n to be %v, but got %v", expected, n) } }) } } ================================================ FILE: server/session.go ================================================ // Copyright 2019 Masahiro Sano // // 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 server import ( "fmt" "strings" "sync" "time" uuidpkg "github.com/google/uuid" spannerpb "google.golang.org/genproto/googleapis/spanner/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" ) func validateSessionName(sessionName string) bool { parts := strings.Split(sessionName, "/") if len(parts) != 8 { return false } if parts[0] != "projects" { return false } if parts[2] != "instances" { return false } if parts[4] != "databases" { return false } if parts[6] != "sessions" { return false } if parts[1] == "" || parts[3] == "" || parts[5] == "" || parts[7] == "" { return false } return true } type session struct { id string dbName string database *database name string createdAt time.Time lastUse time.Time mu sync.Mutex transactions map[string]*transaction activeRWTx []*transaction } func (s *session) Name() string { return s.name } func (s *session) Proto() *spannerpb.Session { ctime := timestamppb.New(s.createdAt) last := timestamppb.New(s.lastUse) return &spannerpb.Session{ Name: s.name, CreateTime: ctime, ApproximateLastUseTime: last, } } func (s *session) createTransaction(txMode transactionMode, single bool) (*transaction, error) { s.mu.Lock() defer s.mu.Unlock() for i := 0; i < 3; i++ { tx := newTransaction(s, txMode, single) if _, ok := s.transactions[tx.Name()]; ok { continue } s.transactions[tx.Name()] = tx if err := s.database.BeginTransaction(tx); err != nil { return nil, err // TODO } // read write transactions are kept only 32 in a session // it seems no limitation for read only transactions if txMode == txReadWrite { s.activeRWTx = append(s.activeRWTx, tx) if len(s.activeRWTx) > 32 { oldtx := s.activeRWTx[0] s.activeRWTx = s.activeRWTx[1:] oldtx.Done(TransactionInvalidated) } } return tx, nil } return nil, fmt.Errorf("failed to create transaction") } func (s *session) GetTransaction(id []byte) (*transaction, bool) { s.mu.Lock() tx, ok := s.transactions[string(id)] s.mu.Unlock() return tx, ok } // GetTransactionBySelector returns a transaction by selector from session. // The second return value means a new transaction is created or not. // When the selector specifies Begin, true is returned. Otherwise false even if SingleUse option is specified. func (s *session) GetTransactionBySelector(txsel *spannerpb.TransactionSelector) (*transaction, bool, error) { switch sel := txsel.GetSelector().(type) { case nil: // From documents: If none is provided, the default is a // single use transaction with strong concurrency. tx, err := s.createTransaction(txReadWrite, true) return tx, false, err case *spannerpb.TransactionSelector_SingleUse: tx, err := s.createTransaction(txReadWrite, true) return tx, false, err case *spannerpb.TransactionSelector_Id: tx, ok := s.GetTransaction(sel.Id) if !ok { return nil, false, status.Errorf(codes.InvalidArgument, "Transaction was started in a different session") } return tx, false, nil case *spannerpb.TransactionSelector_Begin: tx, err := s.BeginTransaction(sel.Begin) return tx, true, err default: return nil, false, fmt.Errorf("unknown transaction selector: %v", sel) } } // GetTransactionForCommit returns a transaction by selector from session. // The argument is expected to be spannerpb.isCommitRequest_Transaction. It is not exported so interface{} // is used instead. func (s *session) GetTransactionForCommit(txsel interface{}) (*transaction, error) { switch v := txsel.(type) { case *spannerpb.CommitRequest_TransactionId: tx, ok := s.GetTransaction(v.TransactionId) if !ok { return nil, status.Errorf(codes.InvalidArgument, "Transaction was started in a different session") } return tx, nil case *spannerpb.CommitRequest_SingleUseTransaction: return s.beginTransaction(v.SingleUseTransaction, true) default: return nil, status.Errorf(codes.Unknown, "unknown transaction: %v", v) } } // BeginTransaction creates a new transaction for a session. func (s *session) BeginTransaction(opt *spannerpb.TransactionOptions) (*transaction, error) { return s.beginTransaction(opt, false) } func (s *session) beginTransaction(opt *spannerpb.TransactionOptions, single bool) (*transaction, error) { var txMode transactionMode switch v := opt.GetMode().(type) { case *spannerpb.TransactionOptions_ReadWrite_: txMode = txReadWrite case *spannerpb.TransactionOptions_ReadOnly_: txMode = txReadOnly case *spannerpb.TransactionOptions_PartitionedDml_: txMode = txPartitionedDML case nil: // TransactionOptions is required return nil, status.Errorf(codes.InvalidArgument, "Invalid BeginTransaction request") default: return nil, status.Errorf(codes.Unknown, "unknown transaction mode: %v", v) } tx, err := s.createTransaction(txMode, single) if err != nil { return nil, err } return tx, nil } func newSession(db *database, dbName string) *session { id := uuidpkg.New().String() return &session{ id: id, database: db, dbName: dbName, name: fmt.Sprintf("%s/sessions/%s", dbName, id), createdAt: time.Now(), transactions: make(map[string]*transaction), activeRWTx: make([]*transaction, 0, 8), } } ================================================ FILE: server/spanner_error.go ================================================ // Copyright 2020 Masahiro Sano // // 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 server import ( "fmt" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func newSpannerDatabaseNotFoundError(name string) error { detail := &errdetails.ResourceInfo{ ResourceType: "type.googleapis.com/google.spanner.admin.database.v1.Database", ResourceName: name, Description: "Database does not exist.", } st, err := status.Newf(codes.NotFound, "Database not found: %s", name).WithDetails(detail) if err != nil { return status.Errorf(codes.Internal, "failed to build status error") } return st.Err() } func newSpannerSessionNotFoundError(name string) error { detail := &errdetails.ResourceInfo{ ResourceType: "type.googleapis.com/google.spanner.v1.Session", ResourceName: name, Description: "Session does not exist.", } st, err := status.Newf(codes.NotFound, "Session not found: %s", name).WithDetails(detail) if err != nil { return status.Errorf(codes.Internal, "failed to build status error") } return st.Err() } func newSpannerTableNotFoundError(name string) error { detail := &errdetails.ResourceInfo{ ResourceType: "spanner.googleapis.com/Table", ResourceName: name, Description: "Table not found", } st, err := status.Newf(codes.NotFound, "Table not found: %s", name).WithDetails(detail) if err != nil { return status.Errorf(codes.Internal, "failed to build status error") } return st.Err() } func newSpannerColumnNotFoundError(table, name string) error { detail := &errdetails.ResourceInfo{ ResourceType: "spanner.googleapis.com/Column", ResourceName: name, // no description in real spanner } st, err := status.Newf(codes.NotFound, "Column not found in table %s: %s", table, name).WithDetails(detail) if err != nil { return status.Errorf(codes.Internal, "failed to build status error") } return st.Err() } func newSpannerIndexnNotFoundError(table, name string) error { detail := &errdetails.ResourceInfo{ ResourceType: "spanner.googleapis.com/Index", ResourceName: name, Description: fmt.Sprintf("Index not found on table %s", table), } st, err := status.Newf(codes.NotFound, "Index not found on table %s: %s", table, name).WithDetails(detail) if err != nil { return status.Errorf(codes.Internal, "failed to build status error") } return st.Err() } ================================================ FILE: server/table.go ================================================ // Copyright 2019 Masahiro Sano // // 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 server import ( "fmt" "strconv" "github.com/cloudspannerecosystem/memefish/ast" ) type Table struct { ast *ast.CreateTable Name string columns []*Column columnsMap map[string]*Column primaryKey *TableIndex index []*TableIndex } func newTable() *Table { return &Table{ columnsMap: make(map[string]*Column), } } func (t *Table) TableIndex(idx string) (*TableIndex, bool) { if idx == "" { return t.primaryKey, true } for _, index := range t.index { if index.Name() == idx { return index, true } } return nil, false } func (t *Table) TableView() *TableView { return createTableViewFromTable(t, "") } func (t *Table) TableViewWithAlias(alias string) *TableView { return createTableViewFromTable(t, alias) } // NonNullableAndNonGeneratedColumnsExist checks non nullable columns exist in the spciefied columns. // It returns true and the columns if non nullable and non generated columns exist. func (t *Table) NonNullableAndNonGeneratedColumnsExist(columns []string) (bool, []string) { usedColumns := make(map[string]struct{}, len(columns)) for _, name := range columns { usedColumns[name] = struct{}{} } var noExistNonNullableColumns []string for _, c := range t.columns { if c.nullable { continue } if c.ast != nil && c.ast.DefaultSemantics != nil { if _, ok := c.ast.DefaultSemantics.(*ast.GeneratedColumnExpr); ok { continue } } n := c.Name() if _, ok := usedColumns[n]; !ok { noExistNonNullableColumns = append(noExistNonNullableColumns, n) } } if len(noExistNonNullableColumns) > 0 { return true, noExistNonNullableColumns } return false, nil } func createTableFromAST(stmt *ast.CreateTable) (*Table, error) { t := newTable() t.Name = stmt.Name.Idents[0].Name t.ast = stmt for _, col := range stmt.Columns { t.addColumn(col) } t.reorderColumnPosition() if err := t.setPrimaryKeys(stmt.PrimaryKeys); err != nil { return nil, err } return t, nil } func (t *Table) addColumn(col *ast.ColumnDef) { column := newColumn(col) t.columns = append(t.columns, column) t.columnsMap[column.Name()] = column } func (t *Table) reorderColumnPosition() { for i := range t.columns { t.columns[i].setPosition(i + 1) } } func (t *Table) setPrimaryKeys(pkey []*ast.IndexKey) error { index, err := createPrimaryKey(t, pkey) if err != nil { return err } t.primaryKey = index for i, col := range index.IndexColumns() { col.markPrimaryKey(i + 1) } return nil } func (t *Table) createIndex(stmt *ast.CreateIndex) (*TableIndex, error) { idx, err := createTableIndexFromAST(t, stmt) if err != nil { return nil, err } t.index = append(t.index, idx) return idx, nil } func (t *Table) getColumn(name string) (*Column, error) { c, ok := t.columnsMap[name] if !ok { return nil, newSpannerColumnNotFoundError(t.Name, name) } return c, nil } func (t *Table) getColumnsByName(names []string) ([]*Column, error) { columns := make([]*Column, len(names)) for i, name := range names { c, err := t.getColumn(name) if err != nil { return nil, err } columns[i] = c } return columns, nil } type Column struct { ast *ast.ColumnDef pos int alias string valueType ValueType dbDataType dbDataType nullable bool isArray bool isSized bool isMax bool size int64 allowCommitTimestamp bool isPrimaryKey bool primaryKeyPos int } func (c *Column) Name() string { return c.ast.Name.Name } func (c *Column) Alias() string { if c.alias != "" { return c.alias } return c.ast.Name.Name } func (c *Column) setPosition(pos int) { c.pos = pos } func (c *Column) markPrimaryKey(pos int) { c.isPrimaryKey = true c.primaryKeyPos = pos } type columnType struct { dataType ast.ScalarTypeName isArray bool isSized bool isMax bool size int64 } type dbDataType string const ( DBDTInteger dbDataType = "INTEGER" DBDTReal dbDataType = "REAL" DBDTText dbDataType = "TEXT" DBDTBlob dbDataType = "BLOB" DBDTJson dbDataType = "JSON" ) func (ct columnType) SqliteDataType() string { switch ct.dataType { case ast.BoolTypeName: return "INTEGER" case ast.Int64TypeName: return "INTEGER" case ast.Float64TypeName: return "REAL" case ast.StringTypeName: return "TEXT" case ast.BytesTypeName: return "BLOB" case ast.DateTypeName: return "TEXT" case ast.TimestampTypeName: return "TEXT" } panic(fmt.Sprintf("unknown data type: %s", ct.dataType)) } func newColumn(def *ast.ColumnDef) *Column { ct := toColumnType(def.Type) vt := toValueType(def.Type) var dbdt dbDataType switch vt.Code { default: panic(fmt.Sprintf("unknown value type %#v", vt)) case TCBool: dbdt = DBDTInteger case TCInt64: dbdt = DBDTInteger case TCFloat64: dbdt = DBDTReal case TCString: dbdt = DBDTText case TCBytes: dbdt = DBDTBlob case TCDate: dbdt = DBDTText case TCTimestamp: dbdt = DBDTText case TCArray: dbdt = DBDTJson case TCJson: dbdt = DBDTJson } var allowCommitTimestamp bool if def.Options != nil { if val, err := def.Options.BoolField("allow_commit_timestamp"); err == nil && val != nil { allowCommitTimestamp = *val } } return &Column{ ast: def, valueType: vt, dbDataType: dbdt, nullable: !def.NotNull, isArray: ct.isArray, isSized: ct.isSized, isMax: ct.isMax, size: ct.size, allowCommitTimestamp: allowCommitTimestamp, } } func astTypeToTypeCode(astTypeName ast.ScalarTypeName) TypeCode { switch astTypeName { case ast.BoolTypeName: return TCBool case ast.Int64TypeName: return TCInt64 case ast.Float64TypeName: return TCFloat64 case ast.StringTypeName: return TCString case ast.BytesTypeName: return TCBytes case ast.DateTypeName: return TCDate case ast.TimestampTypeName: return TCTimestamp case ast.ScalarTypeName("JSON"): return TCJson default: panic("unknown type") } } func toValueType(t ast.SchemaType) ValueType { switch v := t.(type) { case *ast.ScalarSchemaType: return ValueType{Code: astTypeToTypeCode(v.Name)} case *ast.SizedSchemaType: return ValueType{Code: astTypeToTypeCode(v.Name)} case *ast.ArraySchemaType: arrType := toValueType(v.Item) return ValueType{ Code: TCArray, ArrayType: &arrType, } default: panic(fmt.Sprintf("unknow type %v", t)) } } func schemaTypetoTypString(t ast.SchemaType) string { switch v := t.(type) { case *ast.ScalarSchemaType: return astTypeToTypeCode(v.Name).String() case *ast.SizedSchemaType: typ := astTypeToTypeCode(v.Name).String() size := "MAX" if !v.Max { intLit := v.Size.(*ast.IntLiteral) size = intLit.Value // TODO: respect base? } return fmt.Sprintf("%s(%s)", typ, size) case *ast.ArraySchemaType: arrType := schemaTypetoTypString(v.Item) return fmt.Sprintf("ARRAY<%s>", arrType) default: panic(fmt.Sprintf("unknow type %v", t)) } } func toColumnType(t ast.SchemaType) columnType { switch v := t.(type) { case *ast.ScalarSchemaType: return columnType{ dataType: v.Name, } case *ast.SizedSchemaType: if v.Max { return columnType{ dataType: v.Name, isSized: true, isMax: true, } } intLit, ok := v.Size.(*ast.IntLiteral) if !ok { panic(fmt.Sprintf("expected IntLiteral but %v", v.Size)) } n, err := strconv.ParseInt(intLit.Value, intLit.Base, 64) if err != nil { panic(fmt.Sprintf("cannot parse IntLiteral: %v", intLit)) } return columnType{ dataType: v.Name, isSized: true, isMax: false, size: n, } case *ast.ArraySchemaType: ct := toColumnType(v.Item) ct.isArray = true return ct default: panic(fmt.Sprintf("unknow type %v", t)) } } type TableIndex struct { ast *ast.CreateIndex astIndexKeys []*ast.IndexKey name string table *Table unique bool nullFiltered bool columnsRef []*Column columnNames []string columnDirctions []string storedColumns map[string]struct{} } func createTableIndexFromAST(table *Table, stmt *ast.CreateIndex) (*TableIndex, error) { return createTableIndex(table, stmt.Keys, stmt) } func createPrimaryKey(table *Table, pkeys []*ast.IndexKey) (*TableIndex, error) { return createTableIndex(table, pkeys, nil) } func createTableIndex(table *Table, keys []*ast.IndexKey, secondaryIdx *ast.CreateIndex) (*TableIndex, error) { columns := make([]*Column, len(keys)) columnNames := make([]string, len(keys)) columnDirctions := make([]string, len(keys)) for i, key := range keys { col, ok := table.columnsMap[key.Name.Name] if !ok { return nil, fmt.Errorf("primary key not found: %s", key.Name.Name) } columns[i] = col columnNames[i] = col.Name() dir := string(key.Dir) if dir == "" { // work around dir = "ASC" } columnDirctions[i] = dir } name := "PRIMARY_KEY" unique := true nullFiltered := false storedColumns := make(map[string]struct{}) if secondaryIdx != nil { name = secondaryIdx.Name.Idents[0].Name unique = secondaryIdx.Unique nullFiltered = secondaryIdx.NullFiltered // columns for Index Keys for _, c := range columns { storedColumns[c.Name()] = struct{}{} } // secondary index also has primary key columns by default for _, name := range table.primaryKey.IndexColumnNames() { storedColumns[name] = struct{}{} } // storing columns if secondaryIdx.Storing != nil { for _, c := range secondaryIdx.Storing.Columns { storedColumns[c.Name] = struct{}{} } } } else { // Primry Keys have all columns for _, c := range table.columns { storedColumns[c.Name()] = struct{}{} } } return &TableIndex{ ast: secondaryIdx, astIndexKeys: keys, name: name, table: table, unique: unique, nullFiltered: nullFiltered, columnsRef: columns, columnNames: columnNames, storedColumns: storedColumns, columnDirctions: columnDirctions, }, nil } func (i *TableIndex) Name() string { return i.name } func (i *TableIndex) IndexColumns() []*Column { return i.columnsRef } func (i *TableIndex) IndexColumnNames() []string { return i.columnNames } func (i *TableIndex) IndexColumnDirections() []string { return i.columnDirctions } func (i *TableIndex) HasColumn(c string) bool { _, ok := i.storedColumns[c] return ok } type TableView struct { ResultItems []ResultItem ResultItemsMap map[string]ResultItem ambiguous map[string]struct{} } func (v *TableView) AllItems() []ResultItem { return v.ResultItems } func (v *TableView) Get(id string) (ResultItem, bool, bool) { if _, ok := v.ambiguous[id]; ok { return ResultItem{}, true, false } item, ok := v.ResultItemsMap[id] if !ok { return ResultItem{}, false, true } return item, false, false } func (v *TableView) ToStruct() *StructType { names := make([]string, len(v.ResultItems)) vts := make([]*ValueType, len(v.ResultItems)) for i := range v.ResultItems { names[i] = v.ResultItems[i].Name vts[i] = &v.ResultItems[i].ValueType } return &StructType{ FieldNames: names, FieldTypes: vts, IsTable: true, } } func createTableViewFromItems(items1 []ResultItem, items2 []ResultItem) *TableView { newItems := make([]ResultItem, 0, len(items1)+len(items2)) newItemsMap := make(map[string]ResultItem, len(items1)+len(items2)) ambiguous := make(map[string]struct{}) for _, items := range [][]ResultItem{items1, items2} { for _, item := range items { newItems = append(newItems, item) if item.Name != "" { _, ok := newItemsMap[item.Name] if ok { ambiguous[item.Name] = struct{}{} } else { newItemsMap[item.Name] = item } } } } return &TableView{ ResultItems: newItems, ResultItemsMap: newItemsMap, ambiguous: ambiguous, } } func createTableViewFromTable(table *Table, alias string) *TableView { items := make([]ResultItem, 0, len(table.columns)) itemsMap := make(map[string]ResultItem, len(table.columns)) for _, column := range table.columns { item := createResultItemFromColumn(column) // if alias specified, add the alias to Expr if alias != "" { item.Expr.Raw = fmt.Sprintf("%s.%s", QuoteString(alias), item.Expr.Raw) } items = append(items, item) itemsMap[column.Name()] = item } return &TableView{ ResultItems: items, ResultItemsMap: itemsMap, } } type ResultItem struct { Name string ValueType ValueType Expr Expr } func createResultItemFromColumn(column *Column) ResultItem { return ResultItem{ Name: column.Name(), ValueType: column.valueType, Expr: Expr{ Raw: QuoteString(column.Name()), ValueType: column.valueType, }, } } ================================================ FILE: server/transaction.go ================================================ // Copyright 2019 Masahiro Sano // // 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 server import ( "bytes" "context" "database/sql" "fmt" "sync" "sync/atomic" "time" uuidpkg "github.com/google/uuid" spannerpb "google.golang.org/genproto/googleapis/spanner/v1" ) type databaseReader interface { QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) } type databaseWriter interface { ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) Commit() error Rollback() error } var ( ErrNotStartedTransaction = fmt.Errorf("transaction is not started") ErrNotAvailableTransaction = fmt.Errorf("transaction is not available") ErrInvalidatedTransaction = fmt.Errorf("transaction is invalidated") ) type transactionMode int const ( txReadOnly transactionMode = 1 txReadWrite transactionMode = 2 txPartitionedDML transactionMode = 3 ) type TransactionStatus int const ( TransactionActive TransactionStatus = 1 TransactionInvalidated TransactionStatus = 2 TransactionCommited TransactionStatus = 3 TransactionRollbacked TransactionStatus = 4 TransactionAborted TransactionStatus = 5 ) func (s TransactionStatus) String() string { switch s { case TransactionActive: return "ACTIVE" case TransactionInvalidated: return "INVALIDATED" case TransactionCommited: return "COMMITED" case TransactionRollbacked: return "ROLLBACKED" case TransactionAborted: return "ABORTED" default: return "UNKNOWN" } } type transaction struct { id []byte name string session *session single bool mode transactionMode status int32 // for atomic operation createdAt time.Time ctx context.Context cancel func() mu sync.RWMutex tx *sql.Tx close func(*transaction, *sql.Tx) // database.endTransaction() } func (tx *transaction) Context() context.Context { return tx.ctx } func (tx *transaction) Equals(t *transaction) bool { if tx == nil || t == nil { return false } if bytes.Equal(t.ID(), tx.ID()) { return true } return false } func (tx *transaction) ID() []byte { return tx.id } func (tx *transaction) Name() string { if tx == nil { return "" } return tx.name } func (tx *transaction) Proto() *spannerpb.Transaction { return &spannerpb.Transaction{ Id: tx.id, } } func (tx *transaction) Status() TransactionStatus { return TransactionStatus(atomic.LoadInt32(&tx.status)) } func (tx *transaction) ReadWrite() bool { return tx.mode == txReadWrite } func (tx *transaction) Available() bool { return tx.Status() == TransactionActive } func (tx *transaction) Invalidated() bool { st := tx.Status() return st == TransactionInvalidated || st == TransactionAborted } func (tx *transaction) SingleUse() bool { return tx.single } func (tx *transaction) Done(status TransactionStatus) { tx.mu.Lock() defer tx.mu.Unlock() // skip if the transaction is done already if tx.Status() != TransactionActive { return } Debugf("[%s] transaction.Done %s\n", tx.Name(), status) atomic.StoreInt32(&tx.status, int32(status)) if tx.close != nil { tx.close(tx, tx.tx) tx.close = nil } // Cancling transaction context is very unstable. // Call cancel after explicitly stop the transaction. tx.cancel() } func (tx *transaction) SetTransaction(dbtx *sql.Tx, closer func(*transaction, *sql.Tx)) error { tx.mu.Lock() defer tx.mu.Unlock() if tx.tx != nil { return fmt.Errorf("transaction already started") } tx.tx = dbtx tx.close = closer return nil } func (tx *transaction) WriteTransaction(fn func(databaseWriter) error) error { if IsDebug() { defer DebugStartEnd("[%s] transaction.WriteTransaction", tx.Name())() } tx.mu.Lock() defer tx.mu.Unlock() if !tx.Available() { return ErrNotAvailableTransaction } if tx.tx == nil { return ErrNotStartedTransaction } return fn(tx.tx) } func (tx *transaction) ReadTransaction(fn func(context.Context, databaseReader) error) error { if IsDebug() { defer DebugStartEnd("[%s] transaction.ReadTransaction", tx.Name())() } tx.mu.Lock() defer tx.mu.Unlock() if !tx.Available() { return ErrNotAvailableTransaction } if tx.tx == nil { return ErrNotStartedTransaction } return fn(tx.ctx, tx.tx) } func newTransaction(s *session, mode transactionMode, single bool) *transaction { id := uuidpkg.New().String() ctx, cancel := context.WithCancel(context.Background()) return &transaction{ id: []byte(id), name: id, session: s, single: single, mode: mode, status: int32(TransactionActive), createdAt: time.Now(), ctx: ctx, cancel: cancel, } } ================================================ FILE: server/value.go ================================================ // Copyright 2019 Masahiro Sano // // 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 server import ( "database/sql" "database/sql/driver" "encoding/base64" "encoding/json" "fmt" "reflect" "strconv" "strings" "time" spannerpb "google.golang.org/genproto/googleapis/spanner/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" structpb "google.golang.org/protobuf/types/known/structpb" ) var NullExpr = Expr{} var NullValue = Value{} type Expr struct { Raw string ValueType ValueType Args []interface{} } type RowIterator interface { ResultSet() []ResultItem Do(func([]interface{}) error) error } type Value struct { Data interface{} Type ValueType } type ValueType struct { Code TypeCode ArrayType *ValueType StructType *StructType } func (t ValueType) IsArray() bool { return t.Code == TCArray } func (t ValueType) IsStruct() bool { return t.Code == TCStruct } func (t ValueType) String() string { switch t.Code { case TCBool, TCInt64, TCFloat64, TCTimestamp, TCDate, TCString, TCBytes: return t.Code.String() case TCArray: return fmt.Sprintf("ARRAY<%s>", t.ArrayType.String()) case TCStruct: n := len(t.StructType.FieldTypes) ss := make([]string, n) for i := 0; i < n; i++ { name := t.StructType.FieldNames[i] vt := t.StructType.FieldTypes[i] if name == "" { ss[i] = vt.String() } else { ss[i] = name + " " + vt.String() } } return fmt.Sprintf("STRUCT<%s>", strings.Join(ss, ", ")) } return "(unknown type)" } func compareValueType(a, b ValueType) bool { if a.Code != b.Code { return false } if a.Code == TCStruct && b.Code == TCStruct { aStr := a.StructType bStr := b.StructType if len(aStr.FieldTypes) != len(bStr.FieldTypes) { return false } for i := 0; i < len(aStr.FieldTypes); i++ { b := compareValueType(*aStr.FieldTypes[i], *bStr.FieldTypes[i]) if !b { return false } } return true } if a.Code == TCArray && b.Code == TCArray { return compareValueType(*a.ArrayType, *b.ArrayType) } return true } func compatibleValueType(a, b ValueType) (ValueType, bool) { if a.Code == TCInt64 && b.Code == TCFloat64 { return b, true } if b.Code == TCInt64 && a.Code == TCFloat64 { return a, true } return a, a == b } func decideArrayElementsValueType(vts ...ValueType) (ValueType, error) { vt := ValueType{Code: TCInt64} if len(vts) > 0 { vt = vts[0] } used := map[string]struct{}{} for i := range vts { used[vts[i].String()] = struct{}{} } for i := range vts { var ok bool // TODO: if ValueType is changed, types of all values also need changing // vt, ok = compatibleValueType(vt, vts[i]) ok = compareValueType(vt, vts[i]) if !ok { var typ string first := true for n := range used { if !first { typ += ", " } typ += n first = false } return ValueType{}, fmt.Errorf("Array elements of types {%s} do not have a common supertype", typ) } } return vt, nil } type StructType struct { FieldNames []string FieldTypes []*ValueType // Table can be struct but it behaves differently. // So a struct created from table should be marked. IsTable bool } func (s *StructType) AllItems() []ResultItem { n := len(s.FieldTypes) items := make([]ResultItem, n) for i := 0; i < n; i++ { name := s.FieldNames[i] vt := s.FieldTypes[i] items[i] = ResultItem{ Name: name, ValueType: *vt, Expr: Expr{ Raw: name, ValueType: *vt, }, } } return items } type TypeCode int32 func (c TypeCode) String() string { switch c { case TCBool: return "BOOL" case TCInt64: return "INT64" case TCFloat64: return "FLOAT64" case TCTimestamp: return "TIMESTAMP" case TCDate: return "DATE" case TCString: return "STRING" case TCBytes: return "BYTES" case TCArray: return "ARRAY" case TCStruct: return "STRUCT" default: return "(unknown)" } } const ( TCBool TypeCode = iota + 1 TCInt64 TCFloat64 TCTimestamp TCDate TCString TCBytes TCArray TCStruct TCJson ) type ArrayValue interface { Elements() interface{} } type ArrayValueEncoder struct { Values interface{} Invalid bool } func (a *ArrayValueEncoder) Value() (driver.Value, error) { if a.Invalid { return nil, fmt.Errorf("cannot use invalid value") } b, err := json.Marshal(a.Values) if err != nil { return nil, fmt.Errorf("json.Marshal failed in %T: %v", a, err) } return driver.Value(string(b)), nil } func (a *ArrayValueEncoder) Elements() interface{} { return a.Values } type BoolDecoder struct { Bool *bool } func (b *BoolDecoder) UnmarshalJSON(data []byte) error { if len(data) == 1 { var v bool if data[0] == '0' { v = false b.Bool = &v return nil } else if data[0] == '1' { v = true b.Bool = &v return nil } } if err := json.Unmarshal(data, &b.Bool); err != nil { return fmt.Errorf("json.Unmarshal failed for bool: %v", err) } return nil } type ArrayValueDecoder struct { Values interface{} Type ValueType Invalid bool `json:"-"` } func (a *ArrayValueDecoder) Value() interface{} { return a.Values } func (a *ArrayValueDecoder) Scan(src interface{}) error { if src == nil { a.Invalid = true return nil } switch v := src.(type) { case string: return a.UnmarshalJSON([]byte(v)) case []uint8: res := make([]int64, len(v)) for i, _ := range v { res[i] = int64(v[i]) } a.Values = res default: return fmt.Errorf("unexpected type %T for %T", src, a) } return nil } func (a *ArrayValueDecoder) UnmarshalJSON(b []byte) error { if a.Type.ArrayType.Code == TCStruct { var arr []json.RawMessage if err := json.Unmarshal(b, &arr); err != nil { return fmt.Errorf("json.Unmarshal failed in %T: %v", a, err) } var svs []*StructValue for _, b2 := range arr { vv, err := a.decodeStruct(b2, a.Type.ArrayType.StructType) if err != nil { return err } svs = append(svs, vv) } a.Values = svs } else { v, err := a.decodeValue(b, a.Type) if err != nil { return err } a.Values = v } return nil } func (a *ArrayValueDecoder) decodeStruct(b []byte, typ *StructType) (*StructValue, error) { var vv struct { Keys []string `json:"keys"` Values []json.RawMessage `json:"values"` } if err := json.Unmarshal(b, &vv); err != nil { return nil, fmt.Errorf("json.Unmarshal failed in %T: %v", a, err) } var values []interface{} for i, value := range vv.Values { typ := typ.FieldTypes[i] vvv, err := a.decodeValue(value, *typ) if err != nil { return nil, err } values = append(values, vvv) } return &StructValue{ Keys: vv.Keys, Values: values, }, nil } func (a *ArrayValueDecoder) decodeValue(b []byte, typ ValueType) (interface{}, error) { var rv reflect.Value switch typ.Code { case TCBool: rv = reflect.New(reflect.TypeOf(BoolDecoder{})) case TCInt64: rv = reflect.New(reflect.TypeOf(int64(0))) case TCFloat64: rv = reflect.New(reflect.TypeOf(float64(0))) case TCTimestamp, TCDate, TCString: rv = reflect.New(reflect.TypeOf(string(""))) case TCBytes: rv = reflect.New(reflect.TypeOf([]byte{})) case TCArray: switch typ.ArrayType.Code { case TCBool: rv = reflect.New(reflect.TypeOf([]*BoolDecoder{})) case TCInt64: rv = reflect.New(reflect.TypeOf([]*int64{})) case TCFloat64: rv = reflect.New(reflect.TypeOf([]*float64{})) case TCTimestamp, TCDate, TCString: rv = reflect.New(reflect.TypeOf([]*string{})) case TCBytes: rv = reflect.New(reflect.TypeOf([][]byte{})) case TCStruct: v := reflect.New(reflect.TypeOf(ArrayValueDecoder{})) reflect.Indirect(v).FieldByName("Type").Set(reflect.ValueOf(typ)) rv = v default: return nil, fmt.Errorf("unknownn supported type for Array: %v", typ.ArrayType.Code) } case TCStruct: return nil, fmt.Errorf("unknown supported type: %v", typ.Code) } rvv := rv.Interface() if err := json.Unmarshal([]byte(b), rvv); err != nil { return nil, fmt.Errorf("json.Unmarshalll failed for %T in %T: %v", rvv, a, err) } var value interface{} switch vv := rv.Interface().(type) { case *BoolDecoder: value = *vv.Bool case *[]*BoolDecoder: if vv == nil { value = nil } else { vv := *vv vs := make([]*bool, len(vv)) for i := 0; i < len(vv); i++ { if vv[i] == nil { vs[i] = nil } else { vs[i] = vv[i].Bool } } value = vs } case *ArrayValueDecoder: value = vv.Value() default: value = reflect.Indirect(rv).Interface() } return value, nil } type StructValue struct { Keys []string `json:"keys"` Values []interface{} `json:"values"` } type rows struct { rows *sql.Rows resultItems []ResultItem transaction *transaction lastErr error } func (r *rows) ResultSet() []ResultItem { return r.resultItems } func (it *rows) Do(fn func([]interface{}) error) error { var lastErr error var rows []interface{} for { row, ok := it.next() if !ok { break } rows = append(rows, row) if err := fn(row); err != nil { lastErr = err break } } if it.lastErr != nil { if lastErr == nil { lastErr = it.lastErr } } if err := it.rows.Err(); err != nil { if lastErr == nil { lastErr = err } } if err := it.rows.Close(); err != nil { if lastErr == nil { lastErr = err } } // convert sqlite runtime error as InvalidArgument error if it is SqliteArgumentRuntimeError. if lastErr != nil { msg := lastErr.Error() if strings.HasPrefix(msg, SqliteArgumentRuntimeErrorPrefix) { msg = strings.TrimPrefix(msg, SqliteArgumentRuntimeErrorPrefix) return status.Errorf(codes.InvalidArgument, "%s", msg) } if strings.HasPrefix(msg, SqliteOutOfRangeRuntimeErrorPrefix) { msg = strings.TrimPrefix(msg, SqliteOutOfRangeRuntimeErrorPrefix) return status.Errorf(codes.OutOfRange, "%s", msg) } } // database/sql has a possible bug that cannot read any data without error. // It may happen context is canceled in bad timing. // Here checks the transaction is available or not and if it's not available return aborted error. if !it.transaction.Available() { lastErr = status.Errorf(codes.Aborted, "transaction aborted") } return lastErr } func (it *rows) next() ([]interface{}, bool) { ok := it.rows.Next() if !ok { return nil, false } values := make([]reflect.Value, len(it.resultItems)) ptrs := make([]interface{}, len(it.resultItems)) for i, item := range it.resultItems { switch item.ValueType.Code { case TCBool: values[i] = reflect.New(reflect.TypeOf(sql.NullBool{})) case TCInt64: values[i] = reflect.New(reflect.TypeOf(sql.NullInt64{})) case TCFloat64: values[i] = reflect.New(reflect.TypeOf(sql.NullFloat64{})) case TCTimestamp, TCDate, TCString, TCJson: values[i] = reflect.New(reflect.TypeOf(sql.NullString{})) case TCBytes: values[i] = reflect.New(reflect.TypeOf(&[]byte{})) case TCArray: v := reflect.New(reflect.TypeOf(ArrayValueDecoder{})) reflect.Indirect(v).FieldByName("Type").Set(reflect.ValueOf(item.ValueType)) values[i] = v case TCStruct: it.lastErr = fmt.Errorf("unknown supported type: %v", item.ValueType.Code) return nil, false } ptrs[i] = values[i].Interface() } if err := it.rows.Scan(ptrs...); err != nil { it.lastErr = err return nil, false } data := make([]interface{}, len(it.resultItems)) for i := range values { v := reflect.Indirect(values[i]).Interface() switch vv := v.(type) { case sql.NullBool: if !vv.Valid { data[i] = nil } else { data[i] = vv.Bool } case sql.NullString: if !vv.Valid { data[i] = nil } else { data[i] = vv.String } case sql.NullInt64: if !vv.Valid { data[i] = nil } else { data[i] = vv.Int64 } case sql.NullFloat64: if !vv.Valid { data[i] = nil } else { data[i] = vv.Float64 } case *[]byte: if vv == nil { data[i] = nil } else { data[i] = *vv } case ArrayValueDecoder: if vv.Invalid { data[i] = nil } else { data[i] = vv.Value() } default: data[i] = v } } return data, true } func convertToDatabaseValues(lv *structpb.ListValue, columns []*Column) ([]interface{}, error) { values := make([]interface{}, 0, len(columns)) for i, v := range lv.Values { column := columns[i] vv, err := spannerValue2DatabaseValue(v, *column) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "%v", err) } values = append(values, vv) } return values, nil } func spannerValue2DatabaseValue(v *structpb.Value, col Column) (interface{}, error) { // special handling of commit_stamp // It needs to be checked if the column allows to use commit_timestamp if col.valueType.Code == TCTimestamp { if vv, ok := v.Kind.(*structpb.Value_StringValue); ok { s := vv.StringValue if s == "spanner.commit_timestamp()" { if !col.allowCommitTimestamp { msg := "Cannot write commit timestamp because the allow_commit_timestamp column option is not set to true for column %s, or for all corresponding shared key columns in this table's interleaved table hierarchy." return nil, fmt.Errorf(msg, col.Name) // TODO: return FailedPrecondition } now := time.Now().UTC() vv.StringValue = now.Format(time.RFC3339Nano) } } } vv, err := makeDataFromSpannerValue(col.Name(), v, col.valueType) if err != nil { return nil, err } // sqlite doesn not support nil with type like []string(nil) // explicitly convert those values to nil to store as null value rv := reflect.ValueOf(vv) if rv.Kind() == reflect.Slice && rv.IsNil() { return nil, nil } return vv, nil } func encodeBase64(b []byte) string { return base64.StdEncoding.EncodeToString(b) } func decodeBase64(s string) ([]byte, error) { b, err := base64.StdEncoding.DecodeString(s) if err != nil { // seems Spanner tries to use both padding and no-padding return base64.RawStdEncoding.DecodeString(s) } return b, nil } func makeSpannerTypeFromValueType(typ ValueType) *spannerpb.Type { var code spannerpb.TypeCode switch typ.Code { case TCBool: code = spannerpb.TypeCode_BOOL case TCInt64: code = spannerpb.TypeCode_INT64 case TCFloat64: code = spannerpb.TypeCode_FLOAT64 case TCTimestamp: code = spannerpb.TypeCode_TIMESTAMP case TCDate: code = spannerpb.TypeCode_DATE case TCString: code = spannerpb.TypeCode_STRING case TCBytes: code = spannerpb.TypeCode_BYTES case TCArray: code = spannerpb.TypeCode_ARRAY case TCStruct: code = spannerpb.TypeCode_STRUCT case TCJson: code = spannerpb.TypeCode_JSON } st := &spannerpb.Type{Code: code} if code == spannerpb.TypeCode_ARRAY { st = &spannerpb.Type{ Code: code, ArrayElementType: makeSpannerTypeFromValueType(*typ.ArrayType), } } if code == spannerpb.TypeCode_STRUCT { n := len(typ.StructType.FieldTypes) fields := make([]*spannerpb.StructType_Field, n) for i := 0; i < n; i++ { fields[i] = &spannerpb.StructType_Field{ Name: typ.StructType.FieldNames[i], Type: makeSpannerTypeFromValueType(*typ.StructType.FieldTypes[i]), } } st = &spannerpb.Type{ Code: code, StructType: &spannerpb.StructType{ Fields: fields, }, } } return st } func makeValueTypeFromSpannerType(typ *spannerpb.Type) (ValueType, error) { switch typ.Code { case spannerpb.TypeCode_BOOL: return ValueType{ Code: TCBool, }, nil case spannerpb.TypeCode_INT64: return ValueType{ Code: TCInt64, }, nil case spannerpb.TypeCode_FLOAT64: return ValueType{ Code: TCFloat64, }, nil case spannerpb.TypeCode_TIMESTAMP: return ValueType{ Code: TCTimestamp, }, nil case spannerpb.TypeCode_DATE: return ValueType{ Code: TCDate, }, nil case spannerpb.TypeCode_STRING: return ValueType{ Code: TCString, }, nil case spannerpb.TypeCode_BYTES: return ValueType{ Code: TCBytes, }, nil case spannerpb.TypeCode_ARRAY: var array *ValueType if typ.ArrayElementType != nil { vt, err := makeValueTypeFromSpannerType(typ.ArrayElementType) if err != nil { return ValueType{}, err } array = &vt } return ValueType{ Code: TCArray, ArrayType: array, }, nil case spannerpb.TypeCode_STRUCT: fields := typ.GetStructType().GetFields() names := make([]string, len(fields)) types := make([]*ValueType, len(fields)) for i, field := range fields { vt, err := makeValueTypeFromSpannerType(field.Type) if err != nil { return ValueType{}, err } names[i] = field.Name types[i] = &vt } return ValueType{ Code: TCStruct, StructType: &StructType{ FieldNames: names, FieldTypes: types, }, }, nil case spannerpb.TypeCode_JSON: return ValueType{ Code: TCJson, }, nil } return ValueType{}, fmt.Errorf("unknown code for spanner.Type: %v", typ.Code) } func spannerValueFromValue(x interface{}) (*structpb.Value, error) { switch x := x.(type) { case nil: return &structpb.Value{Kind: &structpb.Value_NullValue{}}, nil case bool: return &structpb.Value{Kind: &structpb.Value_BoolValue{x}}, nil case int64: // The Spanner int64 is actually a decimal string. s := strconv.FormatInt(x, 10) return &structpb.Value{Kind: &structpb.Value_StringValue{s}}, nil case float64: return &structpb.Value{Kind: &structpb.Value_NumberValue{x}}, nil case string: return &structpb.Value{Kind: &structpb.Value_StringValue{x}}, nil case []byte: if x == nil { return &structpb.Value{Kind: &structpb.Value_NullValue{}}, nil } else { return &structpb.Value{Kind: &structpb.Value_StringValue{encodeBase64(x)}}, nil } case StructValue: n := len(x.Values) fields := make(map[string]*structpb.Value) for i := 0; i < n; i++ { elem := x.Values[i] v, err := spannerValueFromValue(elem) if err != nil { return nil, err } fields[x.Keys[i]] = v } return &structpb.Value{Kind: &structpb.Value_StructValue{ StructValue: &structpb.Struct{ Fields: fields, }, }}, nil case []*bool, []*int64, []*float64, []*string, []*StructValue, [][]byte: rv := reflect.ValueOf(x) n := rv.Len() vs := make([]*structpb.Value, n) for i := 0; i < n; i++ { var elem interface{} rvv := rv.Index(i) if !rvv.IsNil() { elem = reflect.Indirect(rv.Index(i)).Interface() } v, err := spannerValueFromValue(elem) if err != nil { return nil, err } vs[i] = v } return &structpb.Value{Kind: &structpb.Value_ListValue{ &structpb.ListValue{Values: vs}, }}, nil default: return nil, fmt.Errorf("unknown database value type %T", x) } } func makeDataFromSpannerValue(key string, v *structpb.Value, typ ValueType) (interface{}, error) { if typ.StructType != nil { return nil, newBindingErrorf(key, typ, "Struct type is not supported yet") } if typ.Code == TCArray { if typ.ArrayType == nil { return nil, status.Error(codes.InvalidArgument, "The array_element_type field is required for ARRAYs") } if _, ok := v.Kind.(*structpb.Value_NullValue); ok { switch typ.ArrayType.Code { case TCBool: return []bool(nil), nil case TCInt64: return []int64(nil), nil case TCFloat64: return []float64(nil), nil case TCTimestamp, TCDate, TCString: return []string(nil), nil case TCBytes: return [][]byte(nil), nil case TCArray, TCStruct: // this should not be error actually but no reason to support. return nil, newBindingErrorf(key, typ, "nested Array or Struct for Array is not supported yet") default: return nil, fmt.Errorf("unexpected type %d for Null value as Array", typ.ArrayType.Code) } } vv, ok := v.Kind.(*structpb.Value_ListValue) if !ok { return nil, newBindingErrorf(key, typ, "unexpected value %T and type %s as Array", v.Kind, typ) } n := len(vv.ListValue.Values) switch typ.ArrayType.Code { case TCBool: ret := make([]*bool, n) for i, vv := range vv.ListValue.Values { elemKey := fmt.Sprintf("%s[%d]", key, i) vvv, err := makeDataFromSpannerValue(elemKey, vv, *typ.ArrayType) if err != nil { return nil, err } if vvv == nil { ret[i] = nil } else { vvvv, ok := vvv.(bool) if !ok { panic(fmt.Sprintf("unexpected value type: %T", vvv)) } ret[i] = &vvvv } } return &ArrayValueEncoder{Values: ret}, nil case TCInt64: ret := make([]*int64, n) for i, vv := range vv.ListValue.Values { elemKey := fmt.Sprintf("%s[%d]", key, i) vvv, err := makeDataFromSpannerValue(elemKey, vv, *typ.ArrayType) if err != nil { return nil, err } if vvv == nil { ret[i] = nil } else { vvvv, ok := vvv.(int64) if !ok { panic(fmt.Sprintf("unexpected value type: %T", vvv)) } ret[i] = &vvvv } } return &ArrayValueEncoder{Values: ret}, nil case TCFloat64: ret := make([]*float64, n) for i, vv := range vv.ListValue.Values { elemKey := fmt.Sprintf("%s[%d]", key, i) vvv, err := makeDataFromSpannerValue(elemKey, vv, *typ.ArrayType) if err != nil { return nil, err } if vvv == nil { ret[i] = nil } else { vvvv, ok := vvv.(float64) if !ok { panic(fmt.Sprintf("unexpected value type: %T", vvv)) } ret[i] = &vvvv } } return &ArrayValueEncoder{Values: ret}, nil case TCTimestamp, TCDate, TCString: ret := make([]*string, n) for i, vv := range vv.ListValue.Values { elemKey := fmt.Sprintf("%s[%d]", key, i) vvv, err := makeDataFromSpannerValue(elemKey, vv, *typ.ArrayType) if err != nil { return nil, err } if vvv == nil { ret[i] = nil } else { vvvv, ok := vvv.(string) if !ok { panic(fmt.Sprintf("unexpected value type: %T", vvv)) } ret[i] = &vvvv } } return &ArrayValueEncoder{Values: ret}, nil case TCBytes: ret := make([][]byte, n) for i, vv := range vv.ListValue.Values { elemKey := fmt.Sprintf("%s[%d]", key, i) vvv, err := makeDataFromSpannerValue(elemKey, vv, *typ.ArrayType) if err != nil { return nil, err } if vvv == nil { ret[i] = nil } else { vvvv, ok := vvv.([]byte) if !ok { panic(fmt.Sprintf("unexpected value type: %T", vvv)) } ret[i] = vvvv } } return &ArrayValueEncoder{Values: ret}, nil case TCArray, TCStruct: // just visit elements for appropriate error handling for i, vv := range vv.ListValue.Values { elemKey := fmt.Sprintf("%s[%d]", key, i) _, err := makeDataFromSpannerValue(elemKey, vv, *typ.ArrayType) if err != nil { return nil, err } } // must be unreachable panic("array of array or array of struct is not supported") default: return nil, fmt.Errorf("unknown TypeCode for ArrayElement %v", typ.Code) } } if _, ok := v.Kind.(*structpb.Value_NullValue); ok { return nil, nil } switch typ.Code { case TCBool: switch vv := v.Kind.(type) { case *structpb.Value_BoolValue: return vv.BoolValue, nil } case TCInt64: switch vv := v.Kind.(type) { case *structpb.Value_StringValue: // base is always 10 n, err := strconv.ParseInt(vv.StringValue, 10, 64) if err != nil { return nil, newBindingErrorf(key, typ, "unexpected format %q as int64: %v", vv.StringValue, err) } return n, nil } case TCFloat64: switch vv := v.Kind.(type) { case *structpb.Value_NumberValue: return vv.NumberValue, nil } case TCTimestamp: switch vv := v.Kind.(type) { case *structpb.Value_StringValue: s := vv.StringValue if _, err := time.Parse(time.RFC3339Nano, s); err != nil { return nil, newBindingErrorf(key, typ, "unexpected format %q as timestamp: %v", s, err) } return s, nil } case TCDate: switch vv := v.Kind.(type) { case *structpb.Value_StringValue: s := vv.StringValue if _, err := time.Parse("2006-01-02", s); err != nil { return nil, newBindingErrorf(key, typ, "unexpected format for %q as date: %v", s, err) } return s, nil } case TCString, TCJson: switch vv := v.Kind.(type) { case *structpb.Value_StringValue: return vv.StringValue, nil } case TCBytes: switch vv := v.Kind.(type) { case *structpb.Value_StringValue: b, err := decodeBase64(vv.StringValue) if err != nil { return nil, newBindingErrorf(key, typ, "decoding base64 failed: %v", err) } return b, nil } default: return nil, fmt.Errorf("unknown Type %s", typ) } return nil, newBindingErrorf(key, typ, "unexpected value %T and type %s", v.Kind, typ) } func makeValueFromSpannerValue(key string, v *structpb.Value, typ *spannerpb.Type) (Value, error) { vt, err := makeValueTypeFromSpannerType(typ) if err != nil { return Value{}, err } data, err := makeDataFromSpannerValue(key, v, vt) if err != nil { return Value{}, err } return Value{ Data: data, Type: vt, }, nil } type bindingError struct { key string msg string expected ValueType } func (e *bindingError) Error() string { return e.msg } func (e *bindingError) GRPCStatus() *status.Status { return status.Newf(codes.InvalidArgument, "Invalid value for bind parameter %s: Expected %s.", e.key, e.expected) } func newBindingErrorf(key string, expected ValueType, format string, a ...interface{}) error { return &bindingError{ key: key, expected: expected, msg: fmt.Sprintf(format, a...), } } ================================================ FILE: server/value_test.go ================================================ // Copyright 2019 Masahiro Sano // // 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 server import ( "context" "fmt" "regexp" "testing" "time" "database/sql" "github.com/cloudspannerecosystem/memefish/ast" cmp "github.com/google/go-cmp/cmp" uuidpkg "github.com/google/uuid" spannerpb "google.golang.org/genproto/googleapis/spanner/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" structpb "google.golang.org/protobuf/types/known/structpb" ) func TestCompareValueType(t *testing.T) { table := []struct { a ValueType b ValueType result bool }{ { a: ValueType{Code: TCInt64}, b: ValueType{Code: TCInt64}, result: true, }, { a: ValueType{Code: TCString}, b: ValueType{Code: TCString}, result: true, }, { a: ValueType{Code: TCInt64}, b: ValueType{Code: TCString}, result: false, }, { a: ValueType{ Code: TCArray, ArrayType: &ValueType{Code: TCInt64}, }, b: ValueType{ Code: TCArray, ArrayType: &ValueType{Code: TCInt64}, }, result: true, }, { a: ValueType{ Code: TCArray, ArrayType: &ValueType{Code: TCString}, }, b: ValueType{ Code: TCArray, ArrayType: &ValueType{Code: TCString}, }, result: true, }, { a: ValueType{ Code: TCArray, ArrayType: &ValueType{Code: TCString}, }, b: ValueType{ Code: TCArray, ArrayType: &ValueType{Code: TCInt64}, }, result: false, }, { a: ValueType{Code: TCInt64}, b: ValueType{ Code: TCArray, ArrayType: &ValueType{Code: TCInt64}, }, result: false, }, { a: ValueType{ Code: TCStruct, StructType: &StructType{ FieldNames: []string{"", "", ""}, FieldTypes: []*ValueType{ &ValueType{Code: TCInt64}, &ValueType{Code: TCString}, }, IsTable: false, }, }, b: ValueType{ Code: TCStruct, StructType: &StructType{ FieldNames: []string{"", "", ""}, FieldTypes: []*ValueType{ &ValueType{Code: TCInt64}, &ValueType{Code: TCString}, }, IsTable: false, }, }, result: true, }, { a: ValueType{ Code: TCStruct, StructType: &StructType{ FieldNames: []string{"", "", ""}, FieldTypes: []*ValueType{ &ValueType{Code: TCInt64}, &ValueType{Code: TCString}, }, IsTable: false, }, }, b: ValueType{ Code: TCStruct, StructType: &StructType{ FieldNames: []string{"a", "b", "c"}, FieldTypes: []*ValueType{ &ValueType{Code: TCInt64}, &ValueType{Code: TCString}, }, IsTable: true, }, }, result: true, }, { a: ValueType{ Code: TCStruct, StructType: &StructType{ FieldNames: []string{"", "", ""}, FieldTypes: []*ValueType{ &ValueType{Code: TCInt64}, &ValueType{Code: TCString}, }, IsTable: false, }, }, b: ValueType{ Code: TCStruct, StructType: &StructType{ FieldNames: []string{"", "", ""}, FieldTypes: []*ValueType{ &ValueType{Code: TCString}, &ValueType{Code: TCInt64}, }, IsTable: false, }, }, result: false, }, { a: ValueType{ Code: TCStruct, StructType: &StructType{ FieldNames: []string{"", "", ""}, FieldTypes: []*ValueType{ &ValueType{Code: TCInt64}, &ValueType{ Code: TCArray, ArrayType: &ValueType{Code: TCInt64}, }, }, IsTable: false, }, }, b: ValueType{ Code: TCStruct, StructType: &StructType{ FieldNames: []string{"", "", ""}, FieldTypes: []*ValueType{ &ValueType{Code: TCInt64}, &ValueType{ Code: TCArray, ArrayType: &ValueType{Code: TCInt64}, }, }, IsTable: false, }, }, result: true, }, } for _, tc := range table { r := compareValueType(tc.a, tc.b) if r != tc.result { t.Errorf("expect result %v, but got %v", tc.result, r) } } } func TestDatabaseEncDec(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() table := map[string]struct { value *structpb.Value typ ValueType expected interface{} }{ "Bool": { value: makeBoolValue(true), typ: ValueType{Code: TCBool}, expected: true, }, "Int": { value: makeStringValue("100"), typ: ValueType{Code: TCInt64}, expected: int64(100), }, "Float": { value: makeNumberValue(0.5), typ: ValueType{Code: TCFloat64}, expected: float64(0.5), }, "Bytes": { value: makeStringValue("eHh4"), // xxx typ: ValueType{Code: TCBytes}, expected: []byte("xxx"), }, "Timestamp": { value: makeStringValue("2012-03-04T00:00:00.123456789Z"), typ: ValueType{Code: TCTimestamp}, expected: "2012-03-04T00:00:00.123456789Z", }, "ArrayBool": { value: makeListValueAsValue(makeListValue( makeBoolValue(true), makeBoolValue(false), )), typ: ValueType{ Code: TCArray, ArrayType: &ValueType{Code: TCBool}, }, expected: makeTestArray(TCBool, true, false), }, "ArrayString": { value: makeListValueAsValue(makeListValue( makeStringValue("xxx"), makeStringValue("yyy"), )), typ: ValueType{ Code: TCArray, ArrayType: &ValueType{Code: TCString}, }, expected: makeTestArray(TCString, "xxx", "yyy"), }, "ArrayInt": { value: makeListValueAsValue(makeListValue( makeStringValue("100"), makeStringValue("200"), )), typ: ValueType{ Code: TCArray, ArrayType: &ValueType{Code: TCInt64}, }, expected: makeTestArray(TCInt64, 100, 200), }, "ArrayFloat": { value: makeListValueAsValue(makeListValue( makeNumberValue(0.1), makeNumberValue(0.2), )), typ: ValueType{ Code: TCArray, ArrayType: &ValueType{Code: TCFloat64}, }, expected: makeTestArray(TCFloat64, float64(0.1), float64(0.2)), }, "ArrayBytes": { value: makeListValueAsValue(makeListValue( makeStringValue("eHl6"), // xyz makeStringValue("eHh4"), // xxx )), typ: ValueType{ Code: TCArray, ArrayType: &ValueType{Code: TCBytes}, }, expected: makeTestArray(TCBytes, []byte("xyz"), []byte("xxx")), }, "JSON": { value: makeStringValue(`{"a": 1, "b": 2}`), typ: ValueType{ Code: TCJson, }, expected: `{"a": 1, "b": 2}`, }, } uuid := uuidpkg.New().String() db, err := sql.Open("sqlite3_spanner", fmt.Sprintf("file:%s.db?cache=shared&mode=memory", uuid)) if err != nil { t.Fatal(err) } defer db.Close() if _, err := db.ExecContext(ctx, "CREATE TABLE test (js JSON)"); err != nil { t.Fatal(err) } for name, tc := range table { t.Run(name, func(t *testing.T) { defer func() { if _, err := db.ExecContext(ctx, "DELETE FROM test"); err != nil { t.Fatalf("delete failed: %v", err) } }() column := Column{ ast: &ast.ColumnDef{Name: &ast.Ident{Name: "Id"}}, valueType: tc.typ, dbDataType: DBDTJson, } v, err := spannerValue2DatabaseValue(tc.value, column) if err != nil { t.Fatalf("spannerValue2DatabaseValue failed: %v", err) } if _, err := db.ExecContext(ctx, "INSERT INTO test VALUES(?)", v); err != nil { t.Fatalf("insert failed: %v", err) } r, err := db.QueryContext(ctx, "SELECT js FROM test") if err != nil { t.Fatalf("select failed: %v", err) } defer r.Close() item := createResultItemFromColumn(&column) iter := rows{rows: r, resultItems: []ResultItem{item}, transaction: &transaction{status: 1}} var rows [][]interface{} err = iter.Do(func(row []interface{}) error { rows = append(rows, row) return nil }) if err != nil { t.Fatalf("unexpected error in iteration: %v", err) } if len(rows) != 1 { t.Errorf("there should be only 1 row") } if diff := cmp.Diff(tc.expected, rows[0][0]); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) } } func TestMakeValueFromSpannerValue(t *testing.T) { table := map[string]struct { value *structpb.Value typ *spannerpb.Type expected Value }{ "Null": { value: makeNullValue(), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, expected: Value{ Data: nil, Type: ValueType{ Code: TCInt64, }, }, }, "Int": { value: makeStringValue("100"), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, expected: Value{ Data: int64(100), Type: ValueType{ Code: TCInt64, }, }, }, "String": { value: makeStringValue("xx"), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_STRING, }, expected: Value{ Data: "xx", Type: ValueType{ Code: TCString, }, }, }, "Bool": { value: makeBoolValue(true), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_BOOL, }, expected: Value{ Data: true, Type: ValueType{ Code: TCBool, }, }, }, "Number": { value: makeNumberValue(0.123), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_FLOAT64, }, expected: Value{ Data: 0.123, Type: ValueType{ Code: TCFloat64, }, }, }, "Timestamp": { value: makeStringValue("2012-03-04T00:00:00.123456789Z"), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_TIMESTAMP, }, expected: Value{ Data: "2012-03-04T00:00:00.123456789Z", Type: ValueType{ Code: TCTimestamp, }, }, }, "Date": { value: makeStringValue("2012-03-04"), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_DATE, }, expected: Value{ Data: "2012-03-04", Type: ValueType{ Code: TCDate, }, }, }, "Bytes": { value: makeStringValue("eHh4eHg="), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_BYTES, }, expected: Value{ Data: []byte("xxxxx"), Type: ValueType{ Code: TCBytes, }, }, }, "ListInt": { value: makeListValueAsValue(makeListValue( makeStringValue("100"), makeStringValue("101"), )), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, expected: Value{ Data: makeTestWrappedArray(TCInt64, 100, 101), Type: ValueType{ Code: TCArray, ArrayType: &ValueType{ Code: TCInt64, }, }, }, }, "ListString": { value: makeListValueAsValue(makeListValue( makeStringValue("xxx"), makeStringValue("yyy"), )), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_STRING, }, }, expected: Value{ Data: makeTestWrappedArray(TCString, "xxx", "yyy"), Type: ValueType{ Code: TCArray, ArrayType: &ValueType{ Code: TCString, }, }, }, }, "ListStringNull": { value: makeNullValue(), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_STRING, }, }, expected: Value{ Data: []string(nil), Type: ValueType{ Code: TCArray, ArrayType: &ValueType{ Code: TCString, }, }, }, }, "ListBool": { value: makeListValueAsValue(makeListValue( makeBoolValue(true), makeBoolValue(false), )), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_BOOL, }, }, expected: Value{ Data: makeTestWrappedArray(TCBool, true, false), Type: ValueType{ Code: TCArray, ArrayType: &ValueType{ Code: TCBool, }, }, }, }, "ListBoolNull": { value: makeNullValue(), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_BOOL, }, }, expected: Value{ Data: []bool(nil), Type: ValueType{ Code: TCArray, ArrayType: &ValueType{ Code: TCBool, }, }, }, }, "ListNumber": { value: makeListValueAsValue(makeListValue( makeNumberValue(0.123), makeNumberValue(1.123), )), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_FLOAT64, }, }, expected: Value{ Data: makeTestWrappedArray(TCFloat64, 0.123, 1.123), Type: ValueType{ Code: TCArray, ArrayType: &ValueType{ Code: TCFloat64, }, }, }, }, "ListNumberNull": { value: makeNullValue(), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_FLOAT64, }, }, expected: Value{ Data: []float64(nil), Type: ValueType{ Code: TCArray, ArrayType: &ValueType{ Code: TCFloat64, }, }, }, }, "ListTimestamp": { value: makeListValueAsValue(makeListValue( makeStringValue("2012-03-04T00:00:00.123456789Z"), makeStringValue("2012-03-04T00:00:00.000000000Z"), )), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_TIMESTAMP, }, }, expected: Value{ Data: makeTestWrappedArray(TCString, "2012-03-04T00:00:00.123456789Z", "2012-03-04T00:00:00.000000000Z", ), Type: ValueType{ Code: TCArray, ArrayType: &ValueType{ Code: TCTimestamp, }, }, }, }, "ListTimestampNull": { value: makeNullValue(), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_TIMESTAMP, }, }, expected: Value{ Data: []string(nil), Type: ValueType{ Code: TCArray, ArrayType: &ValueType{ Code: TCTimestamp, }, }, }, }, "ListDate": { value: makeListValueAsValue(makeListValue( makeStringValue("2012-03-04"), makeStringValue("2012-03-05"), )), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_DATE, }, }, expected: Value{ Data: makeTestWrappedArray(TCString, "2012-03-04", "2012-03-05"), Type: ValueType{ Code: TCArray, ArrayType: &ValueType{ Code: TCDate, }, }, }, }, "ListDateNull": { value: makeNullValue(), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_DATE, }, }, expected: Value{ Data: []string(nil), Type: ValueType{ Code: TCArray, ArrayType: &ValueType{ Code: TCDate, }, }, }, }, "ListBytes": { value: makeListValueAsValue(makeListValue( makeStringValue("eHh4eHg="), makeStringValue("eXl5eXk="), )), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_BYTES, }, }, expected: Value{ Data: makeTestWrappedArray(TCBytes, []byte("xxxxx"), []byte("yyyyy")), Type: ValueType{ Code: TCArray, ArrayType: &ValueType{ Code: TCBytes, }, }, }, }, "ListBytesNull": { value: makeNullValue(), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_BYTES, }, }, expected: Value{ Data: [][]byte(nil), Type: ValueType{ Code: TCArray, ArrayType: &ValueType{ Code: TCBytes, }, }, }, }, "JSON": { value: makeStringValue(`{"a": 1, "b": 2}`), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_JSON, }, expected: Value{ Data: `{"a": 1, "b": 2}`, Type: ValueType{ Code: TCJson, }, }, }, } for name, tc := range table { t.Run(name, func(t *testing.T) { res, err := makeValueFromSpannerValue("foo", tc.value, tc.typ) if err != nil { t.Fatalf("unexpected error: %v", err) } if diff := cmp.Diff(tc.expected, res); diff != "" { t.Errorf("(-got, +want)\n%s", diff) } }) } } func TestMakeValueFromSpannerValue_Error(t *testing.T) { table := map[string]struct { value *structpb.Value typ *spannerpb.Type msg *regexp.Regexp }{ "Int_InvalidValue": { value: makeStringValue("xxx"), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, msg: regexp.MustCompile(`^Invalid value for bind parameter foo: Expected INT64`), }, "Int_InvalidType": { value: makeBoolValue(true), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, msg: regexp.MustCompile(`^Invalid value for bind parameter foo: Expected INT64`), }, "Timestamp_InvalidValue": { value: makeStringValue("xxx"), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_TIMESTAMP, }, msg: regexp.MustCompile(`^Invalid value for bind parameter foo: Expected TIMESTAMP`), }, "Timestamp_InvalidType": { value: makeBoolValue(true), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_TIMESTAMP, }, msg: regexp.MustCompile(`^Invalid value for bind parameter foo: Expected TIMESTAMP`), }, "Date_InvalidValue": { value: makeStringValue("xxx"), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_DATE, }, msg: regexp.MustCompile(`^Invalid value for bind parameter foo: Expected DATE`), }, "Date_InvalidType": { value: makeBoolValue(true), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_DATE, }, msg: regexp.MustCompile(`^Invalid value for bind parameter foo: Expected DATE`), }, "Bytes_InvalidValue": { value: makeStringValue("x"), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_BYTES, }, msg: regexp.MustCompile(`^Invalid value for bind parameter foo: Expected BYTES`), }, "Bytes_InvalidType": { value: makeBoolValue(true), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_BYTES, }, msg: regexp.MustCompile(`^Invalid value for bind parameter foo: Expected BYTES`), }, "Array_InvalidType": { value: makeStringValue("xxx"), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_STRING, }, }, msg: regexp.MustCompile(`^Invalid value for bind parameter foo: Expected ARRAY`), }, "Array_InvalidElementValue": { value: makeListValueAsValue(makeListValue( makeStringValue("100"), makeStringValue("xxx"), )), typ: &spannerpb.Type{ Code: spannerpb.TypeCode_ARRAY, ArrayElementType: &spannerpb.Type{ Code: spannerpb.TypeCode_INT64, }, }, msg: regexp.MustCompile(`^Invalid value for bind parameter foo\[1\]: Expected INT64`), }, } for name, tc := range table { t.Run(name, func(t *testing.T) { _, err := makeValueFromSpannerValue("foo", tc.value, tc.typ) if err == nil { t.Fatalf("unexpected success") } st := status.Convert(err) if st.Code() != codes.InvalidArgument { t.Errorf("expect code to be %v, but got %v", codes.InvalidArgument, st.Code()) } if !tc.msg.MatchString(st.Message()) { t.Errorf("unexpected error message: \n %q\n expected:\n %q", st.Message(), tc.msg) } }) } }