From 72d18c4ed2f0dbbe1c2dd9dea8fa3259a0b759df Mon Sep 17 00:00:00 2001 From: Yuki Ito Date: Tue, 3 Sep 2019 12:15:23 +0900 Subject: [PATCH] initialize --- .github/ISSUE_TEMPLATE/bug-report.md | 26 ++ .github/ISSUE_TEMPLATE/feature-request.md | 11 + .github/PULL_REQUEST_TEMPLATE.md | 4 + .gitignore | 1 + .goreleaser.yml | 8 + BUILD.bazel | 32 ++ CONTRIBUTING.md | 6 + LICENSE | 9 + Makefile | 27 ++ README.md | 130 ++++++ WORKSPACE | 54 +++ _examples/migrations/000001.sql | 1 + _examples/schema.sql | 4 + bazel/BUILD.bzl | 0 bazel/deps.bzl | 225 ++++++++++ cmd/BUILD.bazel | 33 ++ cmd/apply.go | 86 ++++ cmd/cmd.go | 49 ++ cmd/create.go | 42 ++ cmd/drop.go | 33 ++ cmd/errors.go | 20 + cmd/export_test.go | 3 + cmd/load.go | 42 ++ cmd/migrate.go | 241 ++++++++++ cmd/migrate_test.go | 53 +++ cmd/reset.go | 38 ++ cmd/root.go | 70 +++ cmd/testdata/migrations/000001_test.sql | 0 cmd/testdata/migrations/000002_test_2.up.sql | 0 go.mod | 14 + go.sum | 128 ++++++ main.go | 50 +++ pkg/spanner/BUILD.bazel | 36 ++ pkg/spanner/client.go | 380 ++++++++++++++++ pkg/spanner/client_test.go | 418 ++++++++++++++++++ pkg/spanner/config.go | 19 + pkg/spanner/errors.go | 35 ++ pkg/spanner/migration.go | 150 +++++++ pkg/spanner/migration_test.go | 46 ++ pkg/spanner/testdata/ddl.sql | 1 + pkg/spanner/testdata/dml.sql | 1 + .../testdata/migrations/000002_test.sql | 1 + pkg/spanner/testdata/migrations/000003.sql | 1 + pkg/spanner/testdata/migrations/000004.sql | 1 + pkg/spanner/testdata/schema.sql | 9 + 45 files changed, 2538 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/feature-request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .gitignore create mode 100644 .goreleaser.yml create mode 100644 BUILD.bazel create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 WORKSPACE create mode 100644 _examples/migrations/000001.sql create mode 100644 _examples/schema.sql create mode 100644 bazel/BUILD.bzl create mode 100644 bazel/deps.bzl create mode 100644 cmd/BUILD.bazel create mode 100644 cmd/apply.go create mode 100644 cmd/cmd.go create mode 100644 cmd/create.go create mode 100644 cmd/drop.go create mode 100644 cmd/errors.go create mode 100644 cmd/export_test.go create mode 100644 cmd/load.go create mode 100644 cmd/migrate.go create mode 100644 cmd/migrate_test.go create mode 100644 cmd/reset.go create mode 100644 cmd/root.go create mode 100644 cmd/testdata/migrations/000001_test.sql create mode 100644 cmd/testdata/migrations/000002_test_2.up.sql create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 pkg/spanner/BUILD.bazel create mode 100644 pkg/spanner/client.go create mode 100644 pkg/spanner/client_test.go create mode 100644 pkg/spanner/config.go create mode 100644 pkg/spanner/errors.go create mode 100644 pkg/spanner/migration.go create mode 100644 pkg/spanner/migration_test.go create mode 100644 pkg/spanner/testdata/ddl.sql create mode 100644 pkg/spanner/testdata/dml.sql create mode 100644 pkg/spanner/testdata/migrations/000002_test.sql create mode 100644 pkg/spanner/testdata/migrations/000003.sql create mode 100644 pkg/spanner/testdata/migrations/000004.sql create mode 100644 pkg/spanner/testdata/schema.sql diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..7aa67b5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,26 @@ +--- +name: Bug Report +about: Report errors and problems +labels: bug +--- + +## Expected Behavior + + +## Current Behavior + + +## Steps to Reproduce + + +1. +2. +3. +4. + +## Context (Environment) + +* go-emv-code version: + + + diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000..d948992 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,11 @@ +--- +name: Feature Request +about: Requests for new features and improvements +labels: enhancement +--- + +## WHAT + + +## WHY + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..d0b1c0e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,4 @@ +Please read the CLA carefully before submitting your contribution to Mercari. +Under any circumstances, by submitting your contribution, you are deemed to accept and agree to be bound by the terms and conditions of the CLA. + +https://www.mercari.com/cla/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6ef824 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/bazel-* diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..b323f9b --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,8 @@ +builds: + - binary: wrench + goos: + - windows + - darwin + - linux + goarch: + - amd64 diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 0000000..ad950f4 --- /dev/null +++ b/BUILD.bazel @@ -0,0 +1,32 @@ +version = "1.0.0" + +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") +load("@bazel_gazelle//:def.bzl", "gazelle") +load("@io_bazel_rules_docker//go:image.bzl", "go_image") + +# gazelle:prefix github.com/mercari/wrench +gazelle(name = "gazelle") + +go_library( + name = "go_default_library", + srcs = ["main.go"], + importpath = "github.com/mercari/wrench", + visibility = ["//visibility:private"], + x_defs = {"github.com/mercari/wrench/cmd.Version": version}, + deps = [ + "//cmd:go_default_library", + "//pkg/spanner:go_default_library", + "@org_golang_x_xerrors//:go_default_library", + ], +) + +go_binary( + name = "wrench", + embed = [":go_default_library"], + visibility = ["//visibility:public"], +) + +go_image( + name = "image", + embed = [":go_default_library"], +) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ca92f26 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# Contributing + +Please read the CLA carefully before submitting your contribution to Mercari. +Under any circumstances, by submitting your contribution, you are deemed to accept and agree to be bound by the terms and conditions of the CLA. + +https://www.mercari.com/cla/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2cbd6aa --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +Copyright 2019 Mercari, Inc. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e665b60 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +.PHONY: test +test: + bazel test \ + --test_env SPANNER_PROJECT_ID=$$SPANNER_PROJECT_ID \ + --test_env SPANNER_INSTANCE_ID=$$SPANNER_INSTANCE_ID \ + --test_env SPANNER_DATABASE_ID=$$SPANNER_DATABASE_ID \ + --test_timeout 600 \ + --test_output streamed \ + --features race \ + //... + +.PHONY: dep +dep: + go mod tidy + bazel run //:gazelle + bazel run //:gazelle -- \ + update-repos \ + -from_file go.mod \ + -to_macro bazel/deps.bzl%wrench_deps + +.PHONY: build +build: + bazel build //:wrench + +.PHONY: image +image: + bazel build --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 //:image diff --git a/README.md b/README.md new file mode 100644 index 0000000..aaf7efa --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# wrench + +`wrench` is a schema management tool for [Cloud Spanner](https://cloud.google.com/spanner/). + +```sh +$ cat ./_examples/schema.sql +CREATE TABLE Singers ( + SingerID STRING(36) NOT NULL, + FirstName STRING(1024), +) PRIMARY KEY(SingerID); + +# create database with ./_examples/schema.sql +$ wrench create --directory ./_examples + +# create migration file +$ wrench migrate create --directory ./_examples +_examples/migrations/000001.sql is created + +# edit _examples/migrations/000001.sql +$ cat ./_examples/migrations/000001.sql +ALTER TABLE Singers ADD COLUMN LastName STRING(1024); + +# execute migration +$ wrench migrate up --directory ./_examples + +# load ddl from database to file ./_examples/schema.sql +$ wrench load --directory ./_examples + +# finally, we have successfully migrated database! +$ cat ./_examples/schema.sql +CREATE TABLE SchemaMigrations ( + Version INT64 NOT NULL, + Dirty BOOL NOT NULL, +) PRIMARY KEY(Version); + +CREATE TABLE Singers ( + SingerID STRING(36) NOT NULL, + FirstName STRING(1024), + LastName STRING(1024), +) PRIMARY KEY(SingerID); +``` + +## Installation + +Get binary from releases page. + +## Usage + +### Prerequisite + +```sh +export SPANNER_PROJECT_ID=your-project-id +export SPANNER_INSTANCE_ID=your-instance-id +export SPANNER_DATABASE_ID=your-database-id +``` + +You can also specify project id, instance id and database id by passing them as command arguments. + +### Create database + +```sh +$ wrench create --directory ./_examples +``` + +This creates the database with `./_examples/schema.sql`. + +### Drop database + +```sh +$ wrench drop +``` + +This just drops the database. + +### Reset database + +```sh +wrench reset --directory ./_examples +``` + +This drops the database and then re-creates with `./_examples/schema.sql`. Equivalent to `drop` and then `create`. + +### Load schema from database to file + +```sh +$ wrench load --directory ./_examples +``` + +This loads schema DDL from database and writes it to `./_examples/schema.sql`. + +### Create migration file + +```sh +$ wrench migrate create --directory ./_examples +``` + +This creates a next migration file like `_examples/migrations/000001.sql`. You will wirte your own migration DDL to this file. + +### Execute migrations + +```sh +$ wrench migrate up --directory ./_examples +``` + +This executes migrations. This also creates `SchemaMigrations` table into your database to manage schema version if it does not exist. + +### Apply single DDL/DML + +```sh +$ wrench apply --ddl ./_examples/ddl.sql +``` + +This applies single DDL or DML. + +Use `wrench [command] --help` for more information about a command. + + +## Contribution + +Please read the CLA carefully before submitting your contribution to Mercari. +Under any circumstances, by submitting your contribution, you are deemed to accept and agree to be bound by the terms and conditions of the CLA. + +[https://www.mercari.com/cla/](https://www.mercari.com/cla/) + + +## License + +Copyright 2019 Mercari, Inc. + +Licensed under the MIT License. diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 0000000..5d10ff3 --- /dev/null +++ b/WORKSPACE @@ -0,0 +1,54 @@ +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") + +http_archive( + name = "io_bazel_rules_go", + urls = ["https://github.com/bazelbuild/rules_go/releases/download/0.19.3/rules_go-0.19.3.tar.gz"], + sha256 = "313f2c7a23fecc33023563f082f381a32b9b7254f727a7dd2d6380ccc6dfe09b", +) + +load("@io_bazel_rules_go//go:deps.bzl", "go_rules_dependencies", "go_register_toolchains") +go_rules_dependencies() +go_register_toolchains() + +http_archive( + name = "bazel_gazelle", + urls = ["https://github.com/bazelbuild/bazel-gazelle/releases/download/0.18.1/bazel-gazelle-0.18.1.tar.gz"], + sha256 = "be9296bfd64882e3c08e3283c58fcb461fa6dd3c171764fcc4cf322f60615a9b", +) + +load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies") +gazelle_dependencies() + +git_repository( + name = "com_google_protobuf", + commit = "09745575a923640154bcf307fba8aedff47f240a", + remote = "https://github.com/protocolbuffers/protobuf", + shallow_since = "1558721209 -0700", +) + +load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") +protobuf_deps() + +load("//:bazel/deps.bzl", "wrench_deps") +wrench_deps() + +http_archive( + name = "io_bazel_rules_docker", + urls = ["https://github.com/bazelbuild/rules_docker/releases/download/v0.9.0/rules_docker-v0.9.0.tar.gz"], + sha256 = "e513c0ac6534810eb7a14bf025a0f159726753f97f74ab7863c650d26e01d677", + strip_prefix = "rules_docker-0.9.0", +) + +load( + "@io_bazel_rules_docker//repositories:repositories.bzl", + container_repositories = "repositories", +) +container_repositories() + +load( + "@io_bazel_rules_docker//go:image.bzl", + _go_image_repos = "repositories", +) + +_go_image_repos() diff --git a/_examples/migrations/000001.sql b/_examples/migrations/000001.sql new file mode 100644 index 0000000..d8d2c82 --- /dev/null +++ b/_examples/migrations/000001.sql @@ -0,0 +1 @@ +ALTER TABLE Singers ADD COLUMN LastName STRING(1024); diff --git a/_examples/schema.sql b/_examples/schema.sql new file mode 100644 index 0000000..1d495f7 --- /dev/null +++ b/_examples/schema.sql @@ -0,0 +1,4 @@ +CREATE TABLE Singers ( + SingerID STRING(36) NOT NULL, + FirstName STRING(1024), +) PRIMARY KEY(SingerID); diff --git a/bazel/BUILD.bzl b/bazel/BUILD.bzl new file mode 100644 index 0000000..e69de29 diff --git a/bazel/deps.bzl b/bazel/deps.bzl new file mode 100644 index 0000000..fb6e2b1 --- /dev/null +++ b/bazel/deps.bzl @@ -0,0 +1,225 @@ +load("@bazel_gazelle//:deps.bzl", "go_repository") + +def wrench_deps(): + go_repository( + name = "co_honnef_go_tools", + importpath = "honnef.co/go/tools", + sum = "h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=", + version = "v0.0.0-20190523083050-ea95bdfd59fc", + ) + go_repository( + name = "com_github_burntsushi_toml", + importpath = "github.com/BurntSushi/toml", + sum = "h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=", + version = "v0.3.1", + ) + go_repository( + name = "com_github_burntsushi_xgb", + importpath = "github.com/BurntSushi/xgb", + sum = "h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=", + version = "v0.0.0-20160522181843-27f122750802", + ) + go_repository( + name = "com_github_client9_misspell", + importpath = "github.com/client9/misspell", + sum = "h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=", + version = "v0.3.4", + ) + go_repository( + name = "com_github_golang_glog", + importpath = "github.com/golang/glog", + sum = "h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=", + version = "v0.0.0-20160126235308-23def4e6c14b", + ) + go_repository( + name = "com_github_golang_mock", + importpath = "github.com/golang/mock", + sum = "h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=", + version = "v1.3.1", + ) + go_repository( + name = "com_github_golang_protobuf", + importpath = "github.com/golang/protobuf", + sum = "h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=", + version = "v1.3.2", + ) + go_repository( + name = "com_github_google_btree", + importpath = "github.com/google/btree", + sum = "h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=", + version = "v1.0.0", + ) + go_repository( + name = "com_github_google_go_cmp", + importpath = "github.com/google/go-cmp", + sum = "h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=", + version = "v0.3.0", + ) + go_repository( + name = "com_github_google_martian", + importpath = "github.com/google/martian", + sum = "h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=", + version = "v2.1.0+incompatible", + ) + go_repository( + name = "com_github_google_pprof", + importpath = "github.com/google/pprof", + sum = "h1:Jnx61latede7zDD3DiiP4gmNz33uK0U5HDUaF0a/HVQ=", + version = "v0.0.0-20190515194954-54271f7e092f", + ) + go_repository( + name = "com_github_googleapis_gax_go_v2", + importpath = "github.com/googleapis/gax-go/v2", + sum = "h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=", + version = "v2.0.5", + ) + go_repository( + name = "com_github_hashicorp_golang_lru", + importpath = "github.com/hashicorp/golang-lru", + sum = "h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=", + version = "v0.5.1", + ) + go_repository( + name = "com_github_inconshreveable_mousetrap", + importpath = "github.com/inconshreveable/mousetrap", + sum = "h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=", + version = "v1.0.0", + ) + go_repository( + name = "com_github_jstemmer_go_junit_report", + importpath = "github.com/jstemmer/go-junit-report", + sum = "h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc=", + version = "v0.0.0-20190106144839-af01ea7f8024", + ) + go_repository( + name = "com_github_spf13_cobra", + importpath = "github.com/spf13/cobra", + sum = "h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=", + version = "v0.0.3", + ) + go_repository( + name = "com_github_spf13_pflag", + importpath = "github.com/spf13/pflag", + sum = "h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=", + version = "v1.0.3", + ) + go_repository( + name = "com_google_cloud_go", + importpath = "cloud.google.com/go", + sum = "h1:NFvqUTDnSNYPX5oReekmB+D+90jrJIcVImxQ3qrBVgM=", + version = "v0.41.0", + ) + go_repository( + name = "io_opencensus_go", + importpath = "go.opencensus.io", + sum = "h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=", + version = "v0.22.0", + ) + go_repository( + name = "io_rsc_binaryregexp", + importpath = "rsc.io/binaryregexp", + sum = "h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=", + version = "v0.2.0", + ) + go_repository( + name = "org_golang_google_api", + importpath = "google.golang.org/api", + sum = "h1:9sdfJOzWlkqPltHAuzT2Cp+yrBeY1KRVYgms8soxMwM=", + version = "v0.7.0", + ) + go_repository( + name = "org_golang_google_appengine", + importpath = "google.golang.org/appengine", + sum = "h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=", + version = "v1.6.1", + ) + go_repository( + name = "org_golang_google_genproto", + importpath = "google.golang.org/genproto", + sum = "h1:Ygq9/SRJX9+dU0WCIICM8RkWvDw03lvB77hrhJnpxfU=", + version = "v0.0.0-20190716160619-c506a9f90610", + ) + go_repository( + name = "org_golang_google_grpc", + importpath = "google.golang.org/grpc", + sum = "h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw=", + version = "v1.22.0", + ) + go_repository( + name = "org_golang_x_crypto", + importpath = "golang.org/x/crypto", + sum = "h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU=", + version = "v0.0.0-20190605123033-f99c8df09eb5", + ) + go_repository( + name = "org_golang_x_exp", + importpath = "golang.org/x/exp", + sum = "h1:OeRHuibLsmZkFj773W4LcfAGsSxJgfPONhr8cmO+eLA=", + version = "v0.0.0-20190510132918-efd6b22b2522", + ) + go_repository( + name = "org_golang_x_image", + importpath = "golang.org/x/image", + sum = "h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0=", + version = "v0.0.0-20190227222117-0694c2d4d067", + ) + go_repository( + name = "org_golang_x_lint", + importpath = "golang.org/x/lint", + sum = "h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI=", + version = "v0.0.0-20190409202823-959b441ac422", + ) + go_repository( + name = "org_golang_x_mobile", + importpath = "golang.org/x/mobile", + sum = "h1:Tus/Y4w3V77xDsGwKUC8a/QrV7jScpU557J77lFffNs=", + version = "v0.0.0-20190312151609-d3739f865fa6", + ) + go_repository( + name = "org_golang_x_net", + importpath = "golang.org/x/net", + sum = "h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=", + version = "v0.0.0-20190628185345-da137c7871d7", + ) + go_repository( + name = "org_golang_x_oauth2", + importpath = "golang.org/x/oauth2", + sum = "h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=", + version = "v0.0.0-20190604053449-0f29369cfe45", + ) + go_repository( + name = "org_golang_x_sync", + importpath = "golang.org/x/sync", + sum = "h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=", + version = "v0.0.0-20190423024810-112230192c58", + ) + go_repository( + name = "org_golang_x_sys", + importpath = "golang.org/x/sys", + sum = "h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI=", + version = "v0.0.0-20190712062909-fae7ac547cb7", + ) + go_repository( + name = "org_golang_x_text", + importpath = "golang.org/x/text", + sum = "h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=", + version = "v0.3.2", + ) + go_repository( + name = "org_golang_x_time", + importpath = "golang.org/x/time", + sum = "h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=", + version = "v0.0.0-20190308202827-9d24e82272b4", + ) + go_repository( + name = "org_golang_x_tools", + importpath = "golang.org/x/tools", + sum = "h1:uIfBkD8gLczr4XDgYpt/qJYds2YJwZRNw4zs7wSnNhk=", + version = "v0.0.0-20190624190245-7f2218787638", + ) + go_repository( + name = "org_golang_x_xerrors", + importpath = "golang.org/x/xerrors", + sum = "h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=", + version = "v0.0.0-20190717185122-a985d3407aa7", + ) diff --git a/cmd/BUILD.bazel b/cmd/BUILD.bazel new file mode 100644 index 0000000..9589323 --- /dev/null +++ b/cmd/BUILD.bazel @@ -0,0 +1,33 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "apply.go", + "cmd.go", + "create.go", + "drop.go", + "errors.go", + "load.go", + "migrate.go", + "reset.go", + "root.go", + ], + importpath = "github.com/mercari/wrench/cmd", + visibility = ["//visibility:public"], + deps = [ + "//pkg/spanner:go_default_library", + "@com_github_spf13_cobra//:go_default_library", + "@org_golang_x_xerrors//:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = [ + "export_test.go", + "migrate_test.go", + ], + data = glob(["testdata/**"]), + embed = [":go_default_library"], +) diff --git a/cmd/apply.go b/cmd/apply.go new file mode 100644 index 0000000..fa902f1 --- /dev/null +++ b/cmd/apply.go @@ -0,0 +1,86 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + "io/ioutil" + + "github.com/spf13/cobra" +) + +var ( + ddlFile string + dmlFile string + partitioned bool +) + +var applyCmd = &cobra.Command{ + Use: "apply", + Short: "Apply DDL file to database", + RunE: apply, +} + +func apply(c *cobra.Command, _ []string) error { + ctx := context.Background() + + client, err := newSpannerClient(ctx, c) + if err != nil { + return err + } + defer client.Close() + + if ddlFile != "" { + if dmlFile != "" { + return errors.New("Cannot specify DDL and DML at same time.") + } + + ddl, err := ioutil.ReadFile(ddlFile) + if err != nil { + return &Error{ + err: err, + cmd: c, + } + } + + err = client.ApplyDDLFile(ctx, ddl) + if err != nil { + return &Error{ + err: err, + cmd: c, + } + } + + return nil + } + + if dmlFile == "" { + return errors.New("Must specify DDL or DML.") + } + + // apply dml + dml, err := ioutil.ReadFile(dmlFile) + if err != nil { + return &Error{ + err: err, + cmd: c, + } + } + + numAffectedRows, err := client.ApplyDMLFile(ctx, dml, partitioned) + if err != nil { + return &Error{ + err: err, + cmd: c, + } + } + fmt.Printf("%d rows affected.\n", numAffectedRows) + + return nil +} + +func init() { + applyCmd.PersistentFlags().StringVar(&ddlFile, flagDDLFile, "", "DDL file to be applied") + applyCmd.PersistentFlags().StringVar(&dmlFile, flagDMLFile, "", "DML file to be applied") + applyCmd.PersistentFlags().BoolVar(&partitioned, flagPartitioned, false, "Whether given DML should be executed as a Partitioned-DML or not") +} diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 0000000..42fb888 --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,49 @@ +package cmd + +import ( + "context" + "path/filepath" + + "github.com/mercari/wrench/pkg/spanner" + "github.com/spf13/cobra" +) + +const ( + flagNameProject = "project" + flagNameInstance = "instance" + flagNameDatabase = "database" + flagNameDirectory = "directory" + flagCredentialsFile = "credentials_file" + flagNameSchemaFile = "schema_file" + flagDDLFile = "ddl" + flagDMLFile = "dml" + flagPartitioned = "partitioned" + defaultSchemaFileName = "schema.sql" +) + +func newSpannerClient(ctx context.Context, c *cobra.Command) (*spanner.Client, error) { + config := &spanner.Config{ + Project: c.Flag(flagNameProject).Value.String(), + Instance: c.Flag(flagNameInstance).Value.String(), + Database: c.Flag(flagNameDatabase).Value.String(), + CredentialsFile: c.Flag(flagCredentialsFile).Value.String(), + } + + client, err := spanner.NewClient(ctx, config) + if err != nil { + return nil, &Error{ + err: err, + cmd: c, + } + } + + return client, nil +} + +func schemaFilePath(c *cobra.Command) string { + filename := c.Flag(flagNameSchemaFile).Value.String() + if filename == "" { + filename = defaultSchemaFileName + } + return filepath.Join(c.Flag(flagNameDirectory).Value.String(), filename) +} diff --git a/cmd/create.go b/cmd/create.go new file mode 100644 index 0000000..c03b03a --- /dev/null +++ b/cmd/create.go @@ -0,0 +1,42 @@ +package cmd + +import ( + "context" + "io/ioutil" + + "github.com/spf13/cobra" +) + +var createCmd = &cobra.Command{ + Use: "create", + Short: "Create database with tables described in schema file", + RunE: create, +} + +func create(c *cobra.Command, _ []string) error { + ctx := context.Background() + + client, err := newSpannerClient(ctx, c) + if err != nil { + return err + } + defer client.Close() + + ddl, err := ioutil.ReadFile(schemaFilePath(c)) + if err != nil { + return &Error{ + err: err, + cmd: c, + } + } + + err = client.CreateDatabase(ctx, ddl) + if err != nil { + return &Error{ + err: err, + cmd: c, + } + } + + return nil +} diff --git a/cmd/drop.go b/cmd/drop.go new file mode 100644 index 0000000..e9adf01 --- /dev/null +++ b/cmd/drop.go @@ -0,0 +1,33 @@ +package cmd + +import ( + "context" + + "github.com/spf13/cobra" +) + +var dropCmd = &cobra.Command{ + Use: "drop", + Short: "Drop database", + RunE: drop, +} + +func drop(c *cobra.Command, _ []string) error { + ctx := context.Background() + + client, err := newSpannerClient(ctx, c) + if err != nil { + return err + } + defer client.Close() + + err = client.DropDatabase(ctx) + if err != nil { + return &Error{ + err: err, + cmd: c, + } + } + + return nil +} diff --git a/cmd/errors.go b/cmd/errors.go new file mode 100644 index 0000000..c48084d --- /dev/null +++ b/cmd/errors.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +type Error struct { + err error + cmd *cobra.Command +} + +func (e *Error) Error() string { + return fmt.Sprintf("Error command: %s, version: %s", e.cmd.Name(), Version) +} + +func (e *Error) Unwrap() error { + return e.err +} diff --git a/cmd/export_test.go b/cmd/export_test.go new file mode 100644 index 0000000..0c7595a --- /dev/null +++ b/cmd/export_test.go @@ -0,0 +1,3 @@ +package cmd + +var CreateMigrationFile = createMigrationFile diff --git a/cmd/load.go b/cmd/load.go new file mode 100644 index 0000000..e05a8b0 --- /dev/null +++ b/cmd/load.go @@ -0,0 +1,42 @@ +package cmd + +import ( + "context" + "io/ioutil" + + "github.com/spf13/cobra" +) + +var loadCmd = &cobra.Command{ + Use: "load", + Short: "Load schema from server to file", + RunE: load, +} + +func load(c *cobra.Command, args []string) error { + ctx := context.Background() + + client, err := newSpannerClient(ctx, c) + if err != nil { + return err + } + defer client.Close() + + ddl, err := client.LoadDDL(ctx) + if err != nil { + return &Error{ + err: err, + cmd: c, + } + } + + err = ioutil.WriteFile(schemaFilePath(c), ddl, 0664) + if err != nil { + return &Error{ + err: err, + cmd: c, + } + } + + return nil +} diff --git a/cmd/migrate.go b/cmd/migrate.go new file mode 100644 index 0000000..8717869 --- /dev/null +++ b/cmd/migrate.go @@ -0,0 +1,241 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/mercari/wrench/pkg/spanner" + "github.com/spf13/cobra" + "golang.org/x/xerrors" +) + +const ( + migrationsDirName = "migrations" + migrationTableName = "SchemaMigrations" +) + +// migrateCmd represents the migrate command +var migrateCmd = &cobra.Command{ + Use: "migrate", + Short: "Migrate database", +} + +func init() { + migrateCreateCmd := &cobra.Command{ + Use: "create NAME", + Short: "Create a set of sequential up migrations in directory", + RunE: migrateCreate, + } + migrateUpCmd := &cobra.Command{ + Use: "up [N]", + Short: "Apply all or N up migrations", + RunE: migrateUp, + } + migrateVersionCmd := &cobra.Command{ + Use: "version", + Short: "Print current migration version", + RunE: migrateVersion, + } + migrateSetCmd := &cobra.Command{ + Use: "set V", + Short: "Set version V but don't run migration (ignores dirty state)", + RunE: migrateSet, + } + + migrateCmd.AddCommand( + migrateCreateCmd, + migrateUpCmd, + migrateVersionCmd, + migrateSetCmd, + ) + + migrateCmd.PersistentFlags().String(flagNameDirectory, "", "Directory that migration files placed (required)") +} + +func migrateCreate(c *cobra.Command, args []string) error { + name := "" + + if len(args) > 0 { + name = args[0] + } + + dir := filepath.Join(c.Flag(flagNameDirectory).Value.String(), migrationsDirName) + + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.Mkdir(dir, os.ModePerm); err != nil { + return &Error{ + cmd: c, + err: err, + } + } + } + + filename, err := createMigrationFile(dir, name, 6) + if err != nil { + return &Error{ + cmd: c, + err: err, + } + } + + fmt.Printf("%s is created\n", filename) + + return nil +} + +func migrateUp(c *cobra.Command, args []string) error { + ctx := context.Background() + + limit := -1 + if len(args) > 0 { + n, err := strconv.Atoi(args[0]) + if err != nil { + return &Error{ + cmd: c, + err: err, + } + } + limit = n + } + + client, err := newSpannerClient(ctx, c) + if err != nil { + return err + } + defer client.Close() + + if err = client.EnsureMigrationTable(ctx, migrationTableName); err != nil { + return &Error{ + cmd: c, + err: err, + } + } + + dir := filepath.Join(c.Flag(flagNameDirectory).Value.String(), migrationsDirName) + migrations, err := spanner.LoadMigrations(dir) + if err != nil { + return &Error{ + cmd: c, + err: err, + } + } + + return client.ExecuteMigrations(ctx, migrations, limit, migrationTableName) +} + +func migrateVersion(c *cobra.Command, args []string) error { + ctx := context.Background() + + client, err := newSpannerClient(ctx, c) + if err != nil { + return err + } + defer client.Close() + + if err = client.EnsureMigrationTable(ctx, migrationTableName); err != nil { + return &Error{ + cmd: c, + err: err, + } + } + + v, _, err := client.GetSchemaMigrationVersion(ctx, migrationTableName) + if err != nil { + se := &spanner.Error{} + if xerrors.As(err, &se) && se.Code == spanner.ErrorCodeNoMigration { + fmt.Println("No migrations.") + return nil + } else { + return &Error{ + cmd: c, + err: err, + } + } + } + + fmt.Println(v) + + return nil +} + +func migrateSet(c *cobra.Command, args []string) error { + ctx := context.Background() + + if len(args) == 0 { + return &Error{ + cmd: c, + err: errors.New("Parameters are not passed."), + } + } + version, err := strconv.Atoi(args[0]) + if err != nil { + return &Error{ + cmd: c, + err: err, + } + } + + client, err := newSpannerClient(ctx, c) + if err != nil { + return err + } + defer client.Close() + + if err = client.EnsureMigrationTable(ctx, migrationTableName); err != nil { + return &Error{ + cmd: c, + err: err, + } + } + + if err := client.SetSchemaMigrationVersion(ctx, uint(version), false, migrationTableName); err != nil { + return &Error{ + cmd: c, + err: err, + } + } + + return nil +} + +func createMigrationFile(dir string, name string, digits int) (string, error) { + if name != "" && !spanner.MigrationNameRegex.MatchString(name) { + return "", errors.New("Invalid migration file name.") + } + + ms, err := spanner.LoadMigrations(dir) + if err != nil { + return "", err + } + + var v uint = 1 + if len(ms) > 0 { + v = ms[len(ms)-1].Version + 1 + } + vStr := fmt.Sprint(v) + + padding := digits - len(vStr) + if padding > 0 { + vStr = strings.Repeat("0", padding) + vStr + } + + var filename string + if name == "" { + filename = filepath.Join(dir, fmt.Sprintf("%s.sql", vStr)) + } else { + filename = filepath.Join(dir, fmt.Sprintf("%s_%s.sql", vStr, name)) + } + + fp, err := os.Create(filename) + if err != nil { + return "", err + } + fp.Close() + + return filename, nil +} diff --git a/cmd/migrate_test.go b/cmd/migrate_test.go new file mode 100644 index 0000000..8caa665 --- /dev/null +++ b/cmd/migrate_test.go @@ -0,0 +1,53 @@ +package cmd_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/mercari/wrench/cmd" +) + +func TestCreateMigrationFile(t *testing.T) { + testdatadir := filepath.Join("testdata", "migrations") + + testcases := []struct { + filename string + digits int + wantFilename string + }{ + { + filename: "foo", + digits: 6, + wantFilename: filepath.Join(testdatadir, "000003_foo.sql"), + }, + { + filename: "bar", + digits: 0, + wantFilename: filepath.Join(testdatadir, "3_bar.sql"), + }, + } + + for _, tc := range testcases { + t.Run(tc.filename, func(t *testing.T) { + filename, err := cmd.CreateMigrationFile(testdatadir, tc.filename, tc.digits) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = os.Remove(filename) + }() + + if tc.wantFilename != filename { + t.Errorf("filename want %v, but got %v", tc.wantFilename, filename) + } + }) + } + + t.Run("invalid name", func(t *testing.T) { + _, err := cmd.CreateMigrationFile(testdatadir, "あああ", 6) + if err.Error() != "Invalid migration file name." { + t.Errorf("err want `invalid name`, but got `%v`", err) + } + }) +} diff --git a/cmd/reset.go b/cmd/reset.go new file mode 100644 index 0000000..109abc7 --- /dev/null +++ b/cmd/reset.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "golang.org/x/xerrors" +) + +var resetCmd = &cobra.Command{ + Use: "reset", + Short: "Equivalent to drop and then create", + RunE: reset, +} + +func reset(c *cobra.Command, args []string) error { + if err := drop(c, args); err != nil { + return errorReset(c, err) + } + + if err := create(c, args); err != nil { + return errorReset(c, err) + } + + return nil +} + +func errorReset(c *cobra.Command, err error) error { + if ue := xerrors.Unwrap(err); ue != nil { + return &Error{ + cmd: c, + err: ue, + } + } + + return &Error{ + cmd: c, + err: err, + } +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..45dfbd3 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,70 @@ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +var ( + Version = "unknown" + versionTemplate = `{{.Version}} +` +) + +var ( + project string + instance string + database string + directory string + schemaFile string + credentialsFile string +) + +var rootCmd = &cobra.Command{ + Use: "wrench", +} + +func Execute() error { + return rootCmd.Execute() +} + +func init() { + cobra.EnableCommandSorting = false + + rootCmd.SilenceUsage = true + rootCmd.SilenceErrors = true + + rootCmd.AddCommand(createCmd) + rootCmd.AddCommand(dropCmd) + rootCmd.AddCommand(resetCmd) + rootCmd.AddCommand(loadCmd) + rootCmd.AddCommand(applyCmd) + rootCmd.AddCommand(migrateCmd) + + rootCmd.PersistentFlags().StringVar(&project, flagNameProject, spannerProjectID(), "GCP project id (optional. if not set, will use $SPANNER_PROJECT_ID or $GOOGLE_CLOUD_PROJECT value)") + rootCmd.PersistentFlags().StringVar(&instance, flagNameInstance, spannerInstanceID(), "Cloud Spanner instance name (optional. if not set, will use $SPANNER_INSTANCE_ID value)") + rootCmd.PersistentFlags().StringVar(&database, flagNameDatabase, spannerDatabaseID(), "Cloud Spanner database name (optional. if not set, will use $SPANNER_DATABASE_ID value)") + rootCmd.PersistentFlags().StringVar(&directory, flagNameDirectory, "", "Directory that schema file placed (required)") + rootCmd.PersistentFlags().StringVar(&schemaFile, flagNameSchemaFile, "", "Name of schema file (optional. if not set, will use default 'schema.sql' file name)") + rootCmd.PersistentFlags().StringVar(&credentialsFile, flagCredentialsFile, "", "Specify Credentials File") + + rootCmd.Version = Version + rootCmd.SetVersionTemplate(versionTemplate) +} + +func spannerProjectID() string { + projectID := os.Getenv("SPANNER_PROJECT_ID") + if projectID != "" { + return projectID + } + return os.Getenv("GOOGLE_CLOUD_PROJECT") +} + +func spannerInstanceID() string { + return os.Getenv("SPANNER_INSTANCE_ID") +} + +func spannerDatabaseID() string { + return os.Getenv("SPANNER_DATABASE_ID") +} diff --git a/cmd/testdata/migrations/000001_test.sql b/cmd/testdata/migrations/000001_test.sql new file mode 100644 index 0000000..e69de29 diff --git a/cmd/testdata/migrations/000002_test_2.up.sql b/cmd/testdata/migrations/000002_test_2.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..12195ab --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/mercari/wrench + +require ( + cloud.google.com/go v0.41.0 + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/spf13/cobra v0.0.3 + github.com/spf13/pflag v1.0.3 // indirect + golang.org/x/net v0.0.0-20190628185345-da137c7871d7 // indirect + golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 // indirect + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 + google.golang.org/api v0.7.0 + google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610 + google.golang.org/grpc v1.22.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0b024d1 --- /dev/null +++ b/go.sum @@ -0,0 +1,128 @@ +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.41.0 h1:NFvqUTDnSNYPX5oReekmB+D+90jrJIcVImxQ3qrBVgM= +cloud.google.com/go v0.41.0/go.mod h1:OauMR7DV8fzvZIl2qg6rkaIhD/vmgk4iwEw/h6ercmg= +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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +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/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 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +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/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +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/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +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/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +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 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +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 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +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/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-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +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/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-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-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0 h1:9sdfJOzWlkqPltHAuzT2Cp+yrBeY1KRVYgms8soxMwM= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +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 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +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-20190626174449-989357319d63/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610 h1:Ygq9/SRJX9+dU0WCIICM8RkWvDw03lvB77hrhJnpxfU= +google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +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.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw= +google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +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= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/main.go b/main.go new file mode 100644 index 0000000..b957b4b --- /dev/null +++ b/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + "os" + + "github.com/mercari/wrench/cmd" + "github.com/mercari/wrench/pkg/spanner" + "golang.org/x/xerrors" +) + +func main() { + execute() +} + +func execute() { + handleError(cmd.Execute()) +} + +func handleError(err error) { + if err != nil { + fmt.Fprint(os.Stderr, (fmt.Sprintf("%s\n\t%s\n", err.Error(), errorDetails(err)))) + os.Exit(1) + } +} + +func errorDetails(err error) string { + se := &spanner.Error{} + if xerrors.As(err, &se) { + switch se.Code { + case spanner.ErrorCodeCreateClient: + return fmt.Sprintf("Failed to connect to Cloud Spanner, %s", se.Error()) + case spanner.ErrorCodeExecuteMigrations, spanner.ErrorCodeMigrationVersionDirty: + return fmt.Sprintf("Failed to execute migration, %s", se.Error()) + default: + return fmt.Sprintf("Failed to execute the operation to Cloud Spanner, %s", se.Error()) + } + } + + pe := &os.PathError{} + if xerrors.As(err, &pe) { + return fmt.Sprintf("Invalid file path, %s", pe.Error()) + } + + if err := xerrors.Unwrap(err); err != nil { + return fmt.Sprintf("%s", err.Error()) + } + + return "Unknown error..." +} diff --git a/pkg/spanner/BUILD.bazel b/pkg/spanner/BUILD.bazel new file mode 100644 index 0000000..9daafc7 --- /dev/null +++ b/pkg/spanner/BUILD.bazel @@ -0,0 +1,36 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "client.go", + "config.go", + "errors.go", + "migration.go", + ], + importpath = "github.com/mercari/wrench/pkg/spanner", + visibility = ["//visibility:public"], + deps = [ + "@com_google_cloud_go//spanner:go_default_library", + "@com_google_cloud_go//spanner/admin/database/apiv1:go_default_library", + "@go_googleapis//google/spanner/admin/database/v1:database_go_proto", + "@org_golang_google_api//iterator:go_default_library", + "@org_golang_google_api//option:go_default_library", + "@org_golang_google_grpc//status:go_default_library", + "@org_golang_x_xerrors//:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = [ + "client_test.go", + "migration_test.go", + ], + data = glob(["testdata/**"]), + embed = [":go_default_library"], + deps = [ + "@com_google_cloud_go//spanner:go_default_library", + "@org_golang_google_api//iterator:go_default_library", + ], +) diff --git a/pkg/spanner/client.go b/pkg/spanner/client.go new file mode 100644 index 0000000..51759d1 --- /dev/null +++ b/pkg/spanner/client.go @@ -0,0 +1,380 @@ +package spanner + +import ( + "context" + "errors" + "fmt" + "sort" + + "cloud.google.com/go/spanner" + admin "cloud.google.com/go/spanner/admin/database/apiv1" + "golang.org/x/xerrors" + "google.golang.org/api/iterator" + "google.golang.org/api/option" + databasepb "google.golang.org/genproto/googleapis/spanner/admin/database/v1" +) + +const ( + ddlStatementsSeparator = ";" +) + +type Client struct { + config *Config + spannerClient *spanner.Client + spannerAdminClient *admin.DatabaseAdminClient +} + +func NewClient(ctx context.Context, config *Config) (*Client, error) { + opts := make([]option.ClientOption, 0) + if config.CredentialsFile != "" { + opts = append(opts, option.WithCredentialsFile(config.CredentialsFile)) + } + + spannerClient, err := spanner.NewClient(ctx, config.URL(), opts...) + if err != nil { + return nil, &Error{ + Code: ErrorCodeCreateClient, + err: err, + } + } + + spannerAdminClient, err := admin.NewDatabaseAdminClient(ctx, opts...) + if err != nil { + spannerClient.Close() + return nil, &Error{ + Code: ErrorCodeCreateClient, + err: err, + } + } + + return &Client{ + config: config, + spannerClient: spannerClient, + spannerAdminClient: spannerAdminClient, + }, nil +} + +func (c *Client) CreateDatabase(ctx context.Context, ddl []byte) error { + statements := toStatements(ddl) + + createReq := &databasepb.CreateDatabaseRequest{ + Parent: fmt.Sprintf("projects/%s/instances/%s", c.config.Project, c.config.Instance), + CreateStatement: fmt.Sprintf("CREATE DATABASE `%s`", c.config.Database), + ExtraStatements: statements, + } + + op, err := c.spannerAdminClient.CreateDatabase(ctx, createReq) + if err != nil { + return &Error{ + Code: ErrorCodeCreateDatabase, + err: err, + } + } + + _, err = op.Wait(ctx) + if err != nil { + return &Error{ + Code: ErrorCodeWaitOperation, + err: err, + } + } + + return nil +} + +func (c *Client) DropDatabase(ctx context.Context) error { + req := &databasepb.DropDatabaseRequest{Database: c.config.URL()} + + if err := c.spannerAdminClient.DropDatabase(ctx, req); err != nil { + return &Error{ + Code: ErrorCodeDropDatabase, + err: err, + } + } + + return nil +} + +func (c *Client) LoadDDL(ctx context.Context) ([]byte, error) { + req := &databasepb.GetDatabaseDdlRequest{Database: c.config.URL()} + + res, err := c.spannerAdminClient.GetDatabaseDdl(ctx, req) + if err != nil { + return nil, &Error{ + Code: ErrorCodeLoadSchema, + err: err, + } + } + + var schema []byte + last := len(res.Statements) - 1 + for index, statement := range res.Statements { + if index != last { + statement += ddlStatementsSeparator + "\n\n" + } else { + statement += ddlStatementsSeparator + "\n" + } + + schema = append(schema[:], []byte(statement)[:]...) + } + + return schema, nil +} + +func (c *Client) ApplyDDLFile(ctx context.Context, ddl []byte) error { + return c.ApplyDDL(ctx, toStatements(ddl)) +} + +func (c *Client) ApplyDDL(ctx context.Context, statements []string) error { + req := &databasepb.UpdateDatabaseDdlRequest{ + Database: c.config.URL(), + Statements: statements, + } + + op, err := c.spannerAdminClient.UpdateDatabaseDdl(ctx, req) + if err != nil { + return &Error{ + Code: ErrorCodeUpdateDDL, + err: err, + } + } + + err = op.Wait(ctx) + if err != nil { + return &Error{ + Code: ErrorCodeWaitOperation, + err: err, + } + } + + return nil +} + +func (c *Client) ApplyDMLFile(ctx context.Context, ddl []byte, partitioned bool) (int64, error) { + statements := toStatements(ddl) + + if partitioned { + return c.ApplyPartitionedDML(ctx, statements) + } else { + return c.ApplyDML(ctx, statements) + } +} + +func (c *Client) ApplyDML(ctx context.Context, statements []string) (int64, error) { + numAffectedRows := int64(0) + _, err := c.spannerClient.ReadWriteTransaction(ctx, func(ctx context.Context, tx *spanner.ReadWriteTransaction) error { + for _, s := range statements { + num, err := tx.Update(ctx, spanner.Statement{ + SQL: s, + }) + if err != nil { + return err + } + numAffectedRows += num + } + return nil + }) + if err != nil { + return 0, &Error{ + Code: ErrorCodeUpdateDML, + err: err, + } + } + + return numAffectedRows, nil +} + +func (c *Client) ApplyPartitionedDML(ctx context.Context, statements []string) (int64, error) { + numAffectedRows := int64(0) + + for _, s := range statements { + num, err := c.spannerClient.PartitionedUpdate(ctx, spanner.Statement{ + SQL: s, + }) + if err != nil { + return numAffectedRows, &Error{ + Code: ErrorCodeUpdatePartitionedDML, + err: err, + } + } + + numAffectedRows += num + } + + return numAffectedRows, nil +} + +func (c *Client) ExecuteMigrations(ctx context.Context, migrations Migrations, limit int, tableName string) error { + sort.Sort(migrations) + + version, dirty, err := c.GetSchemaMigrationVersion(ctx, tableName) + if err != nil { + se := &Error{} + if !xerrors.As(err, &se) || se.Code != ErrorCodeNoMigration { + return &Error{ + Code: ErrorCodeExecuteMigrations, + err: err, + } + } + } + + if dirty { + return &Error{ + Code: ErrorCodeMigrationVersionDirty, + err: fmt.Errorf("Database version: %d is dirty, please fix it.", version), + } + } + + var count int + for _, m := range migrations { + if limit == 0 { + break + } + + if m.Version <= version { + continue + } + + if err := c.SetSchemaMigrationVersion(ctx, m.Version, true, tableName); err != nil { + return &Error{ + Code: ErrorCodeExecuteMigrations, + err: err, + } + } + + switch m.kind { + case statementKindDDL: + if err := c.ApplyDDL(ctx, m.Statements); err != nil { + return &Error{ + Code: ErrorCodeExecuteMigrations, + err: err, + } + } + case statementKindDML: + if _, err := c.ApplyPartitionedDML(ctx, m.Statements); err != nil { + return &Error{ + Code: ErrorCodeExecuteMigrations, + err: err, + } + } + default: + return &Error{ + Code: ErrorCodeExecuteMigrations, + err: fmt.Errorf("Unknown query type, version: %d", m.Version), + } + } + + if m.Name != "" { + fmt.Printf("%d/up %s\n", m.Version, m.Name) + } else { + fmt.Printf("%d/up\n", m.Version) + } + + if err := c.SetSchemaMigrationVersion(ctx, m.Version, false, tableName); err != nil { + return &Error{ + Code: ErrorCodeExecuteMigrations, + err: err, + } + } + + count++ + if limit > 0 && count == limit { + break + } + } + + if count == 0 { + fmt.Println("no change") + } + + return nil +} + +func (c *Client) GetSchemaMigrationVersion(ctx context.Context, tableName string) (uint, bool, error) { + stmt := spanner.Statement{ + SQL: `SELECT Version, Dirty FROM ` + tableName + ` LIMIT 1`, + } + iter := c.spannerClient.Single().Query(ctx, stmt) + defer iter.Stop() + + for { + row, err := iter.Next() + if err != nil { + if err == iterator.Done { + break + } + return 0, false, &Error{ + Code: ErrorCodeGetMigrationVersion, + err: err, + } + } + + var ( + v int64 + dirty bool + ) + if err := row.Columns(&v, &dirty); err != nil { + return 0, false, &Error{ + Code: ErrorCodeGetMigrationVersion, + err: err, + } + } + + return uint(v), dirty, nil + } + + return 0, false, &Error{ + Code: ErrorCodeNoMigration, + err: errors.New("No migration."), + } +} + +func (c *Client) SetSchemaMigrationVersion(ctx context.Context, version uint, dirty bool, tableName string) error { + _, err := c.spannerClient.ReadWriteTransaction(ctx, func(ctx context.Context, tx *spanner.ReadWriteTransaction) error { + m := []*spanner.Mutation{ + spanner.Delete(tableName, spanner.AllKeys()), + spanner.Insert( + tableName, + []string{"Version", "Dirty"}, + []interface{}{int64(version), dirty}, + )} + return tx.BufferWrite(m) + }) + if err != nil { + return &Error{ + Code: ErrorCodeSetMigrationVersion, + err: err, + } + } + + return nil +} + +func (c *Client) EnsureMigrationTable(ctx context.Context, tableName string) error { + iter := c.spannerClient.Single().Read(ctx, tableName, spanner.AllKeys(), []string{"Version"}) + err := iter.Do(func(r *spanner.Row) error { + return nil + }) + if err == nil { + return nil + } + + stmt := fmt.Sprintf(`CREATE TABLE %s ( + Version INT64 NOT NULL, + Dirty BOOL NOT NULL + ) PRIMARY KEY(Version)`, tableName) + + return c.ApplyDDL(ctx, []string{stmt}) +} + +func (c *Client) Close() error { + c.spannerClient.Close() + if err := c.spannerAdminClient.Close(); err != nil { + return &Error{ + err: err, + Code: ErrorCodeCloseClient, + } + } + + return nil +} diff --git a/pkg/spanner/client_test.go b/pkg/spanner/client_test.go new file mode 100644 index 0000000..f9205bc --- /dev/null +++ b/pkg/spanner/client_test.go @@ -0,0 +1,418 @@ +package spanner + +import ( + "context" + "io/ioutil" + "testing" + + "os" + + "cloud.google.com/go/spanner" + "google.golang.org/api/iterator" +) + +const ( + singerTable = "Singers" + migrationTable = "SchemaMigrations" +) + +type ( + table struct { + TableName string `spanner:"table_name"` + } + + column struct { + ColumnName string `spanner:"column_name"` + SpannerType string `spanner:"spanner_type"` + IsNullable string `spanner:"is_nullable"` + } + + singer struct { + SingerID string + FirstName string + } + + migration struct { + Version int64 + Dirty bool + } +) + +const ( + envSpannerProjectID = "SPANNER_PROJECT_ID" + envSpannerInstanceID = "SPANNER_INSTANCE_ID" + envSpannerDatabaseID = "SPANNER_DATABASE_ID" +) + +func TestLoadDDL(t *testing.T) { + ctx := context.Background() + + client, done := testClientWithDatabase(t, ctx) + defer done() + + gotDDL, err := client.LoadDDL(ctx) + if err != nil { + t.Fatalf("failed to load ddl: %v", err) + } + + wantDDL, err := ioutil.ReadFile("testdata/schema.sql") + if err != nil { + t.Fatalf("failed to read ddl file: %v", err) + } + + if want, got := string(wantDDL), string(gotDDL); want != got { + t.Errorf("want: \n%s\n but got: \n%s", want, got) + } +} + +func TestApplyDDLFile(t *testing.T) { + ctx := context.Background() + + ddl, err := ioutil.ReadFile("testdata/ddl.sql") + if err != nil { + t.Fatalf("failed to read ddl file: %v", err) + } + + client, done := testClientWithDatabase(t, ctx) + defer done() + + if err := client.ApplyDDLFile(ctx, ddl); err != nil { + t.Fatalf("failed to apply ddl file: %v", err) + } + + ri := client.spannerClient.Single().Query(ctx, spanner.Statement{ + SQL: "SELECT column_name, spanner_type FROM information_schema.columns WHERE table_catalog = '' AND table_name = @table AND column_name = @column", + Params: map[string]interface{}{ + "table": singerTable, + "column": "Foo", + }, + }) + defer ri.Stop() + + row, err := ri.Next() + if err == iterator.Done { + t.Fatalf("failed to get table information: %v", err) + } + + c := &column{} + if err := row.ToStruct(c); err != nil { + t.Fatalf("failed to convert row to struct: %v", err) + } + + if want, got := "Foo", c.ColumnName; want != got { + t.Errorf("want %s, but got %s", want, got) + } + + if want, got := "STRING(MAX)", c.SpannerType; want != got { + t.Errorf("want %s, but got %s", want, got) + } +} + +func TestApplyDMLFile(t *testing.T) { + ctx := context.Background() + + client, done := testClientWithDatabase(t, ctx) + defer done() + + tests := map[string]struct { + partitioned bool + }{ + "normal DML": {partitioned: false}, + "partitioned DML": {partitioned: true}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + key := "1" + + _, err := client.spannerClient.Apply( + ctx, + []*spanner.Mutation{ + spanner.InsertOrUpdate(singerTable, []string{"SingerID", "FirstName"}, []interface{}{key, "Foo"}), + }, + ) + if err != nil { + t.Fatalf("failed to apply mutation: %v", err) + } + + dml, err := ioutil.ReadFile("testdata/dml.sql") + if err != nil { + t.Fatalf("failed to read dml file: %v", err) + } + + n, err := client.ApplyDMLFile(ctx, dml, test.partitioned) + if err != nil { + t.Fatalf("failed to apply dml file: %v", err) + } + + if want, got := int64(1), n; want != got { + t.Fatalf("want %d, but got %d", want, got) + } + + row, err := client.spannerClient.Single().ReadRow(ctx, singerTable, spanner.Key{key}, []string{"FirstName"}) + if err != nil { + t.Fatalf("failed to read row: %v", err) + } + + s := &singer{} + if err := row.ToStruct(s); err != nil { + t.Fatalf("failed to convert row to struct: %v", err) + } + + if want, got := "Bar", s.FirstName; want != got { + t.Errorf("want %s, but got %s", want, got) + } + }) + } +} + +func TestExecuteMigrations(t *testing.T) { + ctx := context.Background() + + client, done := testClientWithDatabase(t, ctx) + defer done() + + // to ensure partitioned-dml (000003.sql) will be applied correctly, insert a row before migration. + _, err := client.spannerClient.Apply( + ctx, + []*spanner.Mutation{ + spanner.Insert(singerTable, []string{"SingerID", "FirstName"}, []interface{}{"1", "foo"}), + }, + ) + if err != nil { + t.Fatalf("failed to apply mutation: %v", err) + } + + migrations, err := LoadMigrations("testdata/migrations") + if err != nil { + t.Fatalf("failed to load migrations: %v", err) + } + + // only apply 000002.sql by specifying limit 1. + if err := client.ExecuteMigrations(ctx, migrations, 1, migrationTable); err != nil { + t.Fatalf("failed to execute migration: %v", err) + } + + // ensure that only 000002.sql has been applied. + ensureMigrationColumn(t, ctx, client, "LastName", "STRING(MAX)", "YES") + ensureMigrationVersionRecord(t, ctx, client, 2, false) + + if err := client.ExecuteMigrations(ctx, migrations, len(migrations), migrationTable); err != nil { + t.Fatalf("failed to execute migration: %v", err) + } + + // ensure that 000003.sql and 000004.sql have been applied. + ensureMigrationColumn(t, ctx, client, "LastName", "STRING(MAX)", "NO") + ensureMigrationVersionRecord(t, ctx, client, 4, false) + + // ensure that schema is not changed and ExecuteMigrate is safely finished even though no migrations should be applied. + ensureMigrationColumn(t, ctx, client, "LastName", "STRING(MAX)", "NO") + ensureMigrationVersionRecord(t, ctx, client, 4, false) +} + +func ensureMigrationColumn(t *testing.T, ctx context.Context, client *Client, columnName, spannerType, isNullable string) { + t.Helper() + + ri := client.spannerClient.Single().Query(ctx, spanner.Statement{ + SQL: "SELECT column_name, spanner_type, is_nullable FROM information_schema.columns WHERE table_catalog = '' AND table_name = @table AND column_name = @column", + Params: map[string]interface{}{ + "table": singerTable, + "column": columnName, + }, + }) + defer ri.Stop() + + row, err := ri.Next() + if err == iterator.Done { + t.Fatalf("failed to get table information: %v", err) + } + + c := &column{} + if err := row.ToStruct(c); err != nil { + t.Fatalf("failed to convert row to struct: %v", err) + } + + if want, got := spannerType, c.SpannerType; want != got { + t.Errorf("want %s, but got %s", want, got) + } + + if want, got := isNullable, c.IsNullable; want != got { + t.Errorf("want %s, but got %s", want, got) + } +} + +func ensureMigrationVersionRecord(t *testing.T, ctx context.Context, client *Client, version int64, dirty bool) { + t.Helper() + + row, err := client.spannerClient.Single().ReadRow(ctx, migrationTable, spanner.Key{version}, []string{"Version", "Dirty"}) + if err != nil { + t.Fatalf("failed to read row: %v", err) + } + + m := &migration{} + if err := row.ToStruct(m); err != nil { + t.Fatalf("failed to convert row to struct: %v", err) + } + + if want, got := version, m.Version; want != got { + t.Errorf("want %d, but got %d", want, got) + } + + if want, got := dirty, m.Dirty; want != got { + t.Errorf("want %t, but got %t", want, got) + } +} + +func TestGetSchemaMigrationVersion(t *testing.T) { + ctx := context.Background() + + client, done := testClientWithDatabase(t, ctx) + defer done() + + version := 1 + dirty := false + + _, err := client.spannerClient.Apply( + ctx, + []*spanner.Mutation{ + spanner.Insert(migrationTable, []string{"Version", "Dirty"}, []interface{}{version, dirty}), + }, + ) + if err != nil { + t.Fatalf("failed to apply mutation: %v", err) + } + + v, d, err := client.GetSchemaMigrationVersion(ctx, migrationTable) + if err != nil { + t.Fatalf("failed to get version: %v", err) + } + + if want, got := uint(version), v; want != got { + t.Errorf("want %d, but got %d", want, got) + } + + if want, got := dirty, d; want != got { + t.Errorf("want %t, but got %t", want, got) + } +} + +func TestSetSchemaMigrationVersion(t *testing.T) { + ctx := context.Background() + + client, done := testClientWithDatabase(t, ctx) + defer done() + + version := 1 + dirty := false + + _, err := client.spannerClient.Apply( + ctx, + []*spanner.Mutation{ + spanner.Insert(migrationTable, []string{"Version", "Dirty"}, []interface{}{version, dirty}), + }, + ) + if err != nil { + t.Fatalf("failed to apply mutation: %v", err) + } + + nextVersion := 2 + nextDirty := true + + if err := client.SetSchemaMigrationVersion(ctx, uint(nextVersion), nextDirty, migrationTable); err != nil { + t.Fatalf("failed to set version: %v", err) + } + + ensureMigrationVersionRecord(t, ctx, client, int64(nextVersion), nextDirty) +} + +func TestEnsureMigrationTable(t *testing.T) { + ctx := context.Background() + + client, done := testClientWithDatabase(t, ctx) + defer done() + + tests := map[string]struct { + table string + }{ + "table already exists": {table: migrationTable}, + "table does not exist": {table: "SchemaMigrations2"}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + if err := client.EnsureMigrationTable(ctx, test.table); err != nil { + t.Fatalf("failed to ensure migration table: %v", err) + } + + ri := client.spannerClient.Single().Query(ctx, spanner.Statement{ + SQL: "SELECT table_name FROM information_schema.tables WHERE table_catalog = '' AND table_name = @table", + Params: map[string]interface{}{ + "table": test.table, + }, + }) + defer ri.Stop() + + row, err := ri.Next() + if err == iterator.Done { + t.Fatalf("failed to get table information: %v", err) + } + + ta := &table{} + if err := row.ToStruct(ta); err != nil { + t.Fatalf("failed to convert row to struct: %v", err) + } + + if want, got := test.table, ta.TableName; want != got { + t.Errorf("want %s, but got %s", want, got) + } + }) + } +} + +func testClientWithDatabase(t *testing.T, ctx context.Context) (*Client, func()) { + t.Helper() + + project := os.Getenv(envSpannerProjectID) + if project == "" { + t.Fatalf("must set %s", envSpannerProjectID) + } + + instance := os.Getenv(envSpannerInstanceID) + if instance == "" { + t.Fatalf("must set %s", envSpannerInstanceID) + } + + // TODO: take random database name and run tests parallelly. + database := os.Getenv(envSpannerDatabaseID) + if database == "" { + t.Fatalf("must set %s", envSpannerDatabaseID) + } + + config := &Config{ + Project: project, + Instance: instance, + Database: database, + } + + client, err := NewClient(ctx, config) + if err != nil { + t.Fatalf("failed to create spanner client: %v", err) + } + + ddl, err := ioutil.ReadFile("testdata/schema.sql") + if err != nil { + t.Fatalf("failed to read schema file: %v", err) + } + + if err := client.CreateDatabase(ctx, ddl); err != nil { + t.Fatalf("failed to create database: %v", err) + } + + return client, func() { + defer client.Close() + + if err := client.DropDatabase(ctx); err != nil { + t.Fatalf("failed to delete database: %v", err) + } + } +} diff --git a/pkg/spanner/config.go b/pkg/spanner/config.go new file mode 100644 index 0000000..d765258 --- /dev/null +++ b/pkg/spanner/config.go @@ -0,0 +1,19 @@ +package spanner + +import "fmt" + +type Config struct { + Project string + Instance string + Database string + CredentialsFile string +} + +func (c *Config) URL() string { + return fmt.Sprintf( + "projects/%s/instances/%s/databases/%s", + c.Project, + c.Instance, + c.Database, + ) +} diff --git a/pkg/spanner/errors.go b/pkg/spanner/errors.go new file mode 100644 index 0000000..8cac35e --- /dev/null +++ b/pkg/spanner/errors.go @@ -0,0 +1,35 @@ +package spanner + +import "google.golang.org/grpc/status" + +type ErrorCode int + +const ( + ErrorCodeCreateClient = iota + 1 + ErrorCodeCloseClient + ErrorCodeCreateDatabase + ErrorCodeDropDatabase + ErrorCodeLoadSchema + ErrorCodeUpdateDDL + ErrorCodeUpdateDML + ErrorCodeUpdatePartitionedDML + ErrorCodeExecuteMigrations + ErrorCodeGetMigrationVersion + ErrorCodeSetMigrationVersion + ErrorCodeNoMigration + ErrorCodeMigrationVersionDirty + ErrorCodeWaitOperation +) + +type Error struct { + Code ErrorCode + err error +} + +func (e *Error) Error() string { + if st, ok := status.FromError(e.err); ok { + return st.Message() + } + + return e.err.Error() +} diff --git a/pkg/spanner/migration.go b/pkg/spanner/migration.go new file mode 100644 index 0000000..06fbeb6 --- /dev/null +++ b/pkg/spanner/migration.go @@ -0,0 +1,150 @@ +package spanner + +import ( + "bytes" + "errors" + "io/ioutil" + "path/filepath" + "regexp" + "strconv" + "strings" +) + +const ( + statementsSeparator = ";" +) + +var ( + // migrationFileRegex matches the following patterns + // 001.sql + // 001_name.sql + // 001_name.up.sql + migrationFileRegex = regexp.MustCompile(`^([0-9]+)(?:_([a-zA-Z0-9_\-]+))?(\.up)?\.sql$`) + + MigrationNameRegex = regexp.MustCompile(`[a-zA-Z0-9_\-]+`) + + dmlRegex = regexp.MustCompile("^(UPDATE|DELETE)[\t\n\f\r ].*") +) + +const ( + statementKindDDL statementKind = "DDL" + statementKindDML statementKind = "DML" +) + +type ( + // migration represents the parsed migration file. e.g. version_name.sql + Migration struct { + // Version is the version of the migration + Version uint + + // Name is the name of the migration + Name string + + // Statements is the migration statements + Statements []string + + kind statementKind + } + + Migrations []*Migration + + statementKind string +) + +func (ms Migrations) Len() int { + return len(ms) +} + +func (ms Migrations) Swap(i, j int) { + ms[i], ms[j] = ms[j], ms[i] +} + +func (ms Migrations) Less(i, j int) bool { + return ms[i].Version < ms[j].Version +} + +func LoadMigrations(dir string) (Migrations, error) { + files, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + + var migrations Migrations + for _, f := range files { + if f.IsDir() { + continue + } + + matches := migrationFileRegex.FindStringSubmatch(f.Name()) + if len(matches) != 4 { + continue + } + + version, err := strconv.ParseUint(matches[1], 10, 64) + if err != nil { + continue + } + + file, err := ioutil.ReadFile(filepath.Join(dir, f.Name())) + if err != nil { + continue + } + + statements := toStatements(file) + kind, err := inspectStatementsKind(statements) + if err != nil { + return nil, err + } + + migrations = append(migrations, &Migration{ + Version: uint(version), + Name: matches[2], + Statements: statements, + kind: kind, + }) + } + + return migrations, nil +} + +func toStatements(file []byte) []string { + contents := bytes.Split(file, []byte(statementsSeparator)) + + statements := make([]string, 0, len(contents)) + for _, c := range contents { + if statement := strings.TrimSpace(string(c)); statement != "" { + statements = append(statements, statement) + } + } + + return statements +} + +func inspectStatementsKind(statements []string) (statementKind, error) { + kindMap := map[statementKind]uint64{ + statementKindDDL: 0, + statementKindDML: 0, + } + + for _, s := range statements { + if isDML(s) { + kindMap[statementKindDML]++ + } else { + kindMap[statementKindDDL]++ + } + } + + if kindMap[statementKindDML] > 0 { + if kindMap[statementKindDDL] > 0 { + return "", errors.New("Cannot specify DDL and DML at same migration file.") + } + + return statementKindDML, nil + } + + return statementKindDDL, nil +} + +func isDML(statement string) bool { + return dmlRegex.Match([]byte(statement)) +} diff --git a/pkg/spanner/migration_test.go b/pkg/spanner/migration_test.go new file mode 100644 index 0000000..63d71e1 --- /dev/null +++ b/pkg/spanner/migration_test.go @@ -0,0 +1,46 @@ +package spanner_test + +import ( + "path/filepath" + "testing" + + "github.com/mercari/wrench/pkg/spanner" +) + +func TestLoadMigrations(t *testing.T) { + ms, err := spanner.LoadMigrations(filepath.Join("testdata", "migrations")) + if err != nil { + t.Fatal(err) + } + + if len(ms) != 3 { + t.Fatalf("migrations length want 3, but got %v", len(ms)) + } + + testcases := []struct { + idx int + wantVersion uint + wantName string + }{ + { + idx: 0, + wantVersion: 2, + wantName: "test", + }, + { + idx: 1, + wantVersion: 3, + wantName: "", + }, + } + + for _, tc := range testcases { + if ms[tc.idx].Version != tc.wantVersion { + t.Errorf("migrations[%d].version want %v, but got %v", tc.idx, tc.wantVersion, ms[tc.idx].Version) + } + + if ms[tc.idx].Name != tc.wantName { + t.Errorf("migrations[%d].name want %v, but got %v", tc.idx, tc.wantName, ms[tc.idx].Name) + } + } +} diff --git a/pkg/spanner/testdata/ddl.sql b/pkg/spanner/testdata/ddl.sql new file mode 100644 index 0000000..8565107 --- /dev/null +++ b/pkg/spanner/testdata/ddl.sql @@ -0,0 +1 @@ +ALTER TABLE Singers ADD COLUMN Foo STRING(MAX); diff --git a/pkg/spanner/testdata/dml.sql b/pkg/spanner/testdata/dml.sql new file mode 100644 index 0000000..c341d5b --- /dev/null +++ b/pkg/spanner/testdata/dml.sql @@ -0,0 +1 @@ +UPDATE Singers SET FirstName = "Bar" WHERE SingerID = "1"; diff --git a/pkg/spanner/testdata/migrations/000002_test.sql b/pkg/spanner/testdata/migrations/000002_test.sql new file mode 100644 index 0000000..ce86ce6 --- /dev/null +++ b/pkg/spanner/testdata/migrations/000002_test.sql @@ -0,0 +1 @@ +ALTER TABLE Singers ADD COLUMN LastName STRING(MAX); diff --git a/pkg/spanner/testdata/migrations/000003.sql b/pkg/spanner/testdata/migrations/000003.sql new file mode 100644 index 0000000..51101af --- /dev/null +++ b/pkg/spanner/testdata/migrations/000003.sql @@ -0,0 +1 @@ +UPDATE Singers SET LastName = "" WHERE LastName IS NULL; diff --git a/pkg/spanner/testdata/migrations/000004.sql b/pkg/spanner/testdata/migrations/000004.sql new file mode 100644 index 0000000..dd0548c --- /dev/null +++ b/pkg/spanner/testdata/migrations/000004.sql @@ -0,0 +1 @@ +ALTER TABLE Singers ALTER COLUMN LastName STRING(MAX) NOT NULL; diff --git a/pkg/spanner/testdata/schema.sql b/pkg/spanner/testdata/schema.sql new file mode 100644 index 0000000..921e523 --- /dev/null +++ b/pkg/spanner/testdata/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE SchemaMigrations ( + Version INT64 NOT NULL, + Dirty BOOL NOT NULL, +) PRIMARY KEY(Version); + +CREATE TABLE Singers ( + SingerID STRING(36) NOT NULL, + FirstName STRING(1024), +) PRIMARY KEY(SingerID);