├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── client │ └── main.go └── server │ └── main.go ├── doc.go ├── gofmt.sh ├── proto ├── README.md ├── service.pb.go └── service.proto └── service ├── doc.go ├── service.go └── service_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/go-grpc-pg/src/github.com/otoolep/go-grpc-pg 5 | docker: 6 | - image: circleci/golang:1.10 7 | 8 | 9 | steps: 10 | - checkout 11 | - run: go tool vet . 12 | - run: go get -t -d -v ./... 13 | - run: go test -timeout 60s -v ./... 14 | - run: 15 | command: go test -race -timeout 120s -v ./... 16 | environment: 17 | GORACE: "halt_on_error=1" 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | # Editor files 17 | *.swp 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Philip O'Toole http://www.philipotoole.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-grpc-pg [![Circle CI](https://circleci.com/gh/otoolep/go-grpc-pg/tree/master.svg?style=svg)](https://circleci.com/gh/otoolep/go-grpc-pg/tree/master) [![GoDoc](https://godoc.org/github.com/otoolep/go-grpc-pg?status.svg)](https://godoc.org/github.com/otoolep/go-grpc-pg) [![Go Report Card](https://goreportcard.com/badge/github.com/otoolep/go-grpc-pg)](https://goreportcard.com/report/github.com/otoolep/go-grpc-pg) 2 | ====== 3 | 4 | A simple service demonstrating Go, gRPC, and PostgreSQL. Integration with [CircleCI (version 1.0)](http://www.circleci.com) included. 5 | 6 | ## Building go-grpc-pg 7 | *Building go-httpd requires Go 1.8 or later. [gvm](https://github.com/moovweb/gvm) is a great tool for installing and managing your versions of Go.* 8 | 9 | Download and build it like so: 10 | ``` 11 | mkdir go-grpc-pg # Or any directory of your choice 12 | cd go-grpc-pg/ 13 | export GOPATH=$PWD 14 | go get -t github.com/otoolep/go-grpc-pg/... 15 | cd src/github.com/otoolep/go-grpc-pg 16 | go install ./... 17 | ``` 18 | Some people consider using a distinct `GOPATH` environment variable for each project _doing it wrong_. In practise I, and many other Go programmers, find this actually most convenient. 19 | 20 | ### Optional step to speed up testing 21 | Unit testing actually uses SQLite, which is built as part of the test suite -- there is no need to install SQLite separately. However the compilation of SQLite is slow, and quickly becomes tedious if continually repeated. To avoid continual compilation every test run, execute the following command after performing the steps above: 22 | ``` 23 | go install github.com/mattn/go-sqlite3 24 | ``` 25 | 26 | ## Running go-grpc-pg 27 | Once built as per above, launch the server as follows: 28 | ``` 29 | $GOPATH/bin/server 30 | ``` 31 | This assumes PostgreSQL is listening on `localhost`, port 5432. Run `$GOPATH/bin/server -h` to learn the full configuration the server expects of PostgreSQL, including the database, user, and password that must exist. 32 | 33 | ### Generating queries 34 | Assuming the server is up and running, execute the client as follows. 35 | ``` 36 | $GOPATH/bin/client 37 | ``` 38 | An example session is shown below. 39 | ``` 40 | >> CREATE TABLE bar (id INTEGER NOT NULL PRIMARY KEY, name TEXT) 41 | Last Insert ID: -1, rows affected: 1 42 | >> SELECT * FROM bar 43 | >> INSERT INTO bar(id, name) VALUES(1, 'bob') 44 | Last Insert ID: -1, rows affected: 1 45 | >> INSERT INTO bar(id, name) VALUES(2, 'tom') 46 | Last Insert ID: -1, rows affected: 1 47 | >> SELECT * FROM bar 48 | 1 bob 49 | 2 tom 50 | >> 51 | ``` 52 | -------------------------------------------------------------------------------- /cmd/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "strings" 10 | 11 | pb "github.com/otoolep/go-grpc-pg/proto" 12 | 13 | "golang.org/x/net/context" 14 | "google.golang.org/grpc" 15 | ) 16 | 17 | // Command line defaults 18 | const ( 19 | DefaultgRPCAddr = "localhost:11000" 20 | ) 21 | 22 | // Command line parameters 23 | var gRPCAddr string 24 | 25 | func init() { 26 | flag.StringVar(&gRPCAddr, "grpc-addr", DefaultgRPCAddr, "gRPC connection address") 27 | flag.Usage = func() { 28 | fmt.Fprintf(os.Stderr, "Usage: %s [options]\n", os.Args[0]) 29 | flag.PrintDefaults() 30 | } 31 | } 32 | 33 | func main() { 34 | flag.Parse() 35 | 36 | conn, err := grpc.Dial(gRPCAddr, grpc.WithInsecure()) 37 | if err != nil { 38 | log.Fatalf("failed to connect: %s", err.Error()) 39 | } 40 | defer conn.Close() 41 | 42 | client := pb.NewDBProviderClient(conn) 43 | 44 | s := bufio.NewScanner(os.Stdin) 45 | f := bufio.NewWriter(os.Stdout) 46 | 47 | prompt(f) 48 | 49 | for s.Scan() { 50 | line := strings.TrimSpace(s.Text()) 51 | if line != "" { 52 | if isQuery(line) { 53 | resp, err := client.Query(context.Background(), &pb.QueryRequest{s.Text()}) 54 | if err != nil { 55 | fmt.Printf("query error: %s\n", err.Error()) 56 | prompt(f) 57 | continue 58 | } 59 | for _, r := range resp.Rows { 60 | for _, v := range r.Values { 61 | fmt.Printf("%s\t", v) 62 | } 63 | fmt.Println() 64 | } 65 | } else { 66 | resp, err := client.Exec(context.Background(), &pb.ExecRequest{s.Text()}) 67 | if err != nil { 68 | fmt.Printf("exec error: %s\n", err.Error()) 69 | prompt(f) 70 | continue 71 | } 72 | fmt.Printf("Last Insert ID: %d, rows affected: %d\n", resp.LastInsertId, resp.RowsAffected) 73 | } 74 | } 75 | 76 | prompt(f) 77 | } 78 | 79 | fmt.Println() 80 | } 81 | 82 | func isQuery(line string) bool { 83 | index := strings.Index(line, " ") 84 | if index >= 0 { 85 | return strings.ToUpper(line[:index]) == "SELECT" 86 | } 87 | return false 88 | } 89 | 90 | func prompt(w *bufio.Writer) { 91 | w.Write([]byte(`>> `)) 92 | w.Flush() 93 | } 94 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "net" 9 | "os" 10 | "os/signal" 11 | 12 | _ "github.com/lib/pq" 13 | "github.com/otoolep/go-grpc-pg/service" 14 | ) 15 | 16 | // Command line defaults 17 | const ( 18 | DefaultgRPCAddr = "localhost:11000" 19 | DefaultPostgreSQLAddr = "localhost:5432" 20 | DefaultPostgreSQLDB = "pggo" 21 | DefaultPostgreSQLUser = "postgres" 22 | DefaultPostgreSQLPassword = "postgres" 23 | ) 24 | 25 | // Command line parameters 26 | var gRPCAddr string 27 | var pgAddr string 28 | var pgDB string 29 | var pgUser string 30 | var pgPassword string 31 | 32 | func init() { 33 | flag.StringVar(&gRPCAddr, "grpc-addr", DefaultgRPCAddr, "Set the gRPC bind address") 34 | flag.StringVar(&pgAddr, "pg-addr", DefaultPostgreSQLAddr, "Set PostgreSQL address") 35 | flag.StringVar(&pgDB, "pg-db", DefaultPostgreSQLDB, "Set PostgreSQL database") 36 | flag.StringVar(&pgUser, "pg-user", DefaultPostgreSQLUser, "Set PostgreSQL user") 37 | flag.StringVar(&pgPassword, "pg-password", DefaultPostgreSQLPassword, "Set PostgreSQL password") 38 | flag.Usage = func() { 39 | fmt.Fprintf(os.Stderr, "Usage: %s [options]\n", os.Args[0]) 40 | flag.PrintDefaults() 41 | } 42 | } 43 | 44 | func main() { 45 | flag.Parse() 46 | log.SetFlags(log.LstdFlags) 47 | log.SetPrefix("[main] ") 48 | 49 | // Create database connection. 50 | conn, err := pgConnection() 51 | if err != nil { 52 | log.Fatal("invalid PostgreSQL connection parameters:", err.Error()) 53 | } 54 | db, err := sql.Open("postgres", conn) 55 | if err != nil { 56 | log.Fatal("failed to open PostgreSQL:", err.Error()) 57 | } 58 | 59 | // Verify connection. 60 | if err := db.Ping(); err != nil { 61 | log.Fatal("failed to verify connection to PostgreSQL:", err.Error()) 62 | } 63 | log.Println("database connection verified to", pgAddr) 64 | 65 | // Create the service. 66 | srv := service.New(gRPCAddr, db) 67 | 68 | // Start the service. 69 | if err := srv.Open(); err != nil { 70 | fmt.Fprintf(os.Stderr, "failed to start service: %s", err.Error()) 71 | os.Exit(1) 72 | } 73 | log.Println("service started successfully") 74 | 75 | // Block until a signal is received. 76 | terminate := make(chan os.Signal, 1) 77 | signal.Notify(terminate, os.Interrupt) 78 | <-terminate 79 | log.Println("service exiting") 80 | } 81 | 82 | func pgConnection() (string, error) { 83 | host, port, err := net.SplitHostPort(pgAddr) 84 | if err != nil { 85 | return "", err 86 | } 87 | 88 | return fmt.Sprintf(`user=%s password=%s dbname=%s host=%s port=%s`, 89 | pgUser, pgPassword, pgDB, host, port), nil 90 | } 91 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package gogrpcpg is a simple example of a service that exposes a gRPC interface, and contacts a PostgreSQL backend. 3 | */ 4 | package gogrpcpg 5 | -------------------------------------------------------------------------------- /gofmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | fmtcount=`git ls-files | grep '.go$' | xargs gofmt -l 2>&1 | wc -l` 3 | if [ $fmtcount -gt 0 ]; then 4 | echo "run 'go fmt ./...' to format your source code." 5 | exit 1 6 | fi -------------------------------------------------------------------------------- /proto/README.md: -------------------------------------------------------------------------------- 1 | # Generating new protobuf code 2 | 3 | ``` 4 | export PATH=$PATH:$GOPATH/bin 5 | export PATH=$PATH:/protoc/bin 6 | cd $GOPATH/src/github.com/otoolep/go-grpc-pg/proto 7 | protoc service.proto --go_out=plugins=grpc:. 8 | ``` 9 | 10 | Once a new `.go` file has been generated, this file should be committed. 11 | -------------------------------------------------------------------------------- /proto/service.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: service.proto 3 | 4 | /* 5 | Package proto is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | service.proto 9 | 10 | It has these top-level messages: 11 | Row 12 | QueryRequest 13 | QueryResponse 14 | ExecRequest 15 | ExecResponse 16 | */ 17 | package proto 18 | 19 | import proto1 "github.com/golang/protobuf/proto" 20 | import fmt "fmt" 21 | import math "math" 22 | 23 | import ( 24 | context "golang.org/x/net/context" 25 | grpc "google.golang.org/grpc" 26 | ) 27 | 28 | // Reference imports to suppress errors if they are not otherwise used. 29 | var _ = proto1.Marshal 30 | var _ = fmt.Errorf 31 | var _ = math.Inf 32 | 33 | // This is a compile-time assertion to ensure that this generated file 34 | // is compatible with the proto package it is being compiled against. 35 | // A compilation error at this line likely means your copy of the 36 | // proto package needs to be updated. 37 | const _ = proto1.ProtoPackageIsVersion2 // please upgrade the proto package 38 | 39 | type Row struct { 40 | Values []string `protobuf:"bytes,1,rep,name=values" json:"values,omitempty"` 41 | } 42 | 43 | func (m *Row) Reset() { *m = Row{} } 44 | func (m *Row) String() string { return proto1.CompactTextString(m) } 45 | func (*Row) ProtoMessage() {} 46 | func (*Row) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } 47 | 48 | func (m *Row) GetValues() []string { 49 | if m != nil { 50 | return m.Values 51 | } 52 | return nil 53 | } 54 | 55 | // The query statement 56 | type QueryRequest struct { 57 | Stmt string `protobuf:"bytes,1,opt,name=stmt" json:"stmt,omitempty"` 58 | } 59 | 60 | func (m *QueryRequest) Reset() { *m = QueryRequest{} } 61 | func (m *QueryRequest) String() string { return proto1.CompactTextString(m) } 62 | func (*QueryRequest) ProtoMessage() {} 63 | func (*QueryRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } 64 | 65 | func (m *QueryRequest) GetStmt() string { 66 | if m != nil { 67 | return m.Stmt 68 | } 69 | return "" 70 | } 71 | 72 | // The query response 73 | type QueryResponse struct { 74 | Columns []string `protobuf:"bytes,1,rep,name=columns" json:"columns,omitempty"` 75 | Rows []*Row `protobuf:"bytes,3,rep,name=rows" json:"rows,omitempty"` 76 | } 77 | 78 | func (m *QueryResponse) Reset() { *m = QueryResponse{} } 79 | func (m *QueryResponse) String() string { return proto1.CompactTextString(m) } 80 | func (*QueryResponse) ProtoMessage() {} 81 | func (*QueryResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } 82 | 83 | func (m *QueryResponse) GetColumns() []string { 84 | if m != nil { 85 | return m.Columns 86 | } 87 | return nil 88 | } 89 | 90 | func (m *QueryResponse) GetRows() []*Row { 91 | if m != nil { 92 | return m.Rows 93 | } 94 | return nil 95 | } 96 | 97 | // The exec statement 98 | type ExecRequest struct { 99 | Stmt string `protobuf:"bytes,1,opt,name=stmt" json:"stmt,omitempty"` 100 | } 101 | 102 | func (m *ExecRequest) Reset() { *m = ExecRequest{} } 103 | func (m *ExecRequest) String() string { return proto1.CompactTextString(m) } 104 | func (*ExecRequest) ProtoMessage() {} 105 | func (*ExecRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} } 106 | 107 | func (m *ExecRequest) GetStmt() string { 108 | if m != nil { 109 | return m.Stmt 110 | } 111 | return "" 112 | } 113 | 114 | // The exec response 115 | type ExecResponse struct { 116 | LastInsertId int64 `protobuf:"varint,1,opt,name=lastInsertId" json:"lastInsertId,omitempty"` 117 | RowsAffected int64 `protobuf:"varint,2,opt,name=rowsAffected" json:"rowsAffected,omitempty"` 118 | } 119 | 120 | func (m *ExecResponse) Reset() { *m = ExecResponse{} } 121 | func (m *ExecResponse) String() string { return proto1.CompactTextString(m) } 122 | func (*ExecResponse) ProtoMessage() {} 123 | func (*ExecResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} } 124 | 125 | func (m *ExecResponse) GetLastInsertId() int64 { 126 | if m != nil { 127 | return m.LastInsertId 128 | } 129 | return 0 130 | } 131 | 132 | func (m *ExecResponse) GetRowsAffected() int64 { 133 | if m != nil { 134 | return m.RowsAffected 135 | } 136 | return 0 137 | } 138 | 139 | func init() { 140 | proto1.RegisterType((*Row)(nil), "proto.Row") 141 | proto1.RegisterType((*QueryRequest)(nil), "proto.QueryRequest") 142 | proto1.RegisterType((*QueryResponse)(nil), "proto.QueryResponse") 143 | proto1.RegisterType((*ExecRequest)(nil), "proto.ExecRequest") 144 | proto1.RegisterType((*ExecResponse)(nil), "proto.ExecResponse") 145 | } 146 | 147 | // Reference imports to suppress errors if they are not otherwise used. 148 | var _ context.Context 149 | var _ grpc.ClientConn 150 | 151 | // This is a compile-time assertion to ensure that this generated file 152 | // is compatible with the grpc package it is being compiled against. 153 | const _ = grpc.SupportPackageIsVersion4 154 | 155 | // Client API for DBProvider service 156 | 157 | type DBProviderClient interface { 158 | // Query executes a statement that reads data. 159 | Query(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*QueryResponse, error) 160 | // Exec executes a statement that writes data. 161 | Exec(ctx context.Context, in *ExecRequest, opts ...grpc.CallOption) (*ExecResponse, error) 162 | } 163 | 164 | type dBProviderClient struct { 165 | cc *grpc.ClientConn 166 | } 167 | 168 | func NewDBProviderClient(cc *grpc.ClientConn) DBProviderClient { 169 | return &dBProviderClient{cc} 170 | } 171 | 172 | func (c *dBProviderClient) Query(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*QueryResponse, error) { 173 | out := new(QueryResponse) 174 | err := grpc.Invoke(ctx, "/proto.DBProvider/Query", in, out, c.cc, opts...) 175 | if err != nil { 176 | return nil, err 177 | } 178 | return out, nil 179 | } 180 | 181 | func (c *dBProviderClient) Exec(ctx context.Context, in *ExecRequest, opts ...grpc.CallOption) (*ExecResponse, error) { 182 | out := new(ExecResponse) 183 | err := grpc.Invoke(ctx, "/proto.DBProvider/Exec", in, out, c.cc, opts...) 184 | if err != nil { 185 | return nil, err 186 | } 187 | return out, nil 188 | } 189 | 190 | // Server API for DBProvider service 191 | 192 | type DBProviderServer interface { 193 | // Query executes a statement that reads data. 194 | Query(context.Context, *QueryRequest) (*QueryResponse, error) 195 | // Exec executes a statement that writes data. 196 | Exec(context.Context, *ExecRequest) (*ExecResponse, error) 197 | } 198 | 199 | func RegisterDBProviderServer(s *grpc.Server, srv DBProviderServer) { 200 | s.RegisterService(&_DBProvider_serviceDesc, srv) 201 | } 202 | 203 | func _DBProvider_Query_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 204 | in := new(QueryRequest) 205 | if err := dec(in); err != nil { 206 | return nil, err 207 | } 208 | if interceptor == nil { 209 | return srv.(DBProviderServer).Query(ctx, in) 210 | } 211 | info := &grpc.UnaryServerInfo{ 212 | Server: srv, 213 | FullMethod: "/proto.DBProvider/Query", 214 | } 215 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 216 | return srv.(DBProviderServer).Query(ctx, req.(*QueryRequest)) 217 | } 218 | return interceptor(ctx, in, info, handler) 219 | } 220 | 221 | func _DBProvider_Exec_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 222 | in := new(ExecRequest) 223 | if err := dec(in); err != nil { 224 | return nil, err 225 | } 226 | if interceptor == nil { 227 | return srv.(DBProviderServer).Exec(ctx, in) 228 | } 229 | info := &grpc.UnaryServerInfo{ 230 | Server: srv, 231 | FullMethod: "/proto.DBProvider/Exec", 232 | } 233 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 234 | return srv.(DBProviderServer).Exec(ctx, req.(*ExecRequest)) 235 | } 236 | return interceptor(ctx, in, info, handler) 237 | } 238 | 239 | var _DBProvider_serviceDesc = grpc.ServiceDesc{ 240 | ServiceName: "proto.DBProvider", 241 | HandlerType: (*DBProviderServer)(nil), 242 | Methods: []grpc.MethodDesc{ 243 | { 244 | MethodName: "Query", 245 | Handler: _DBProvider_Query_Handler, 246 | }, 247 | { 248 | MethodName: "Exec", 249 | Handler: _DBProvider_Exec_Handler, 250 | }, 251 | }, 252 | Streams: []grpc.StreamDesc{}, 253 | Metadata: "service.proto", 254 | } 255 | 256 | func init() { proto1.RegisterFile("service.proto", fileDescriptor0) } 257 | 258 | var fileDescriptor0 = []byte{ 259 | // 256 bytes of a gzipped FileDescriptorProto 260 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x90, 0xbf, 0x4e, 0xc3, 0x30, 261 | 0x10, 0x87, 0x09, 0x4e, 0x8b, 0x7a, 0x4d, 0x97, 0x2b, 0x42, 0x56, 0x25, 0x50, 0xf0, 0x94, 0xa9, 262 | 0x12, 0x85, 0x17, 0x00, 0xc1, 0x90, 0x0d, 0x3c, 0xb0, 0x97, 0xe4, 0x2a, 0x55, 0x4a, 0xe3, 0xe2, 263 | 0x3f, 0x09, 0xbc, 0x3d, 0x8a, 0xe3, 0x48, 0xc9, 0xc2, 0x94, 0xdc, 0xe7, 0xcf, 0x77, 0xbf, 0x33, 264 | 0xac, 0x0c, 0xe9, 0xe6, 0x58, 0xd0, 0xf6, 0xac, 0x95, 0x55, 0x38, 0xf3, 0x1f, 0x71, 0x0b, 0x4c, 265 | 0xaa, 0x16, 0x6f, 0x60, 0xde, 0xec, 0x2b, 0x47, 0x86, 0x47, 0x29, 0xcb, 0x16, 0x32, 0x54, 0x42, 266 | 0x40, 0xf2, 0xe1, 0x48, 0xff, 0x4a, 0xfa, 0x76, 0x64, 0x2c, 0x22, 0xc4, 0xc6, 0x9e, 0x2c, 0x8f, 267 | 0xd2, 0x28, 0x5b, 0x48, 0xff, 0x2f, 0x72, 0x58, 0x05, 0xc7, 0x9c, 0x55, 0x6d, 0x08, 0x39, 0x5c, 268 | 0x15, 0xaa, 0x72, 0xa7, 0x7a, 0xe8, 0x36, 0x94, 0x78, 0x07, 0xb1, 0x56, 0xad, 0xe1, 0x2c, 0x65, 269 | 0xd9, 0x72, 0x07, 0x7d, 0x94, 0xad, 0x54, 0xad, 0xf4, 0x5c, 0xdc, 0xc3, 0xf2, 0xed, 0x87, 0x8a, 270 | 0xff, 0xa6, 0x7d, 0x42, 0xd2, 0x2b, 0x61, 0x98, 0x80, 0xa4, 0xda, 0x1b, 0x9b, 0xd7, 0x86, 0xb4, 271 | 0xcd, 0x4b, 0xef, 0x32, 0x39, 0x61, 0x9d, 0xd3, 0xb5, 0x7f, 0x3e, 0x1c, 0xa8, 0xb0, 0x54, 0xf2, 272 | 0xcb, 0xde, 0x19, 0xb3, 0x9d, 0x03, 0x78, 0x7d, 0x79, 0xd7, 0xaa, 0x39, 0x96, 0xa4, 0xf1, 0x09, 273 | 0x66, 0x7e, 0x27, 0x5c, 0x87, 0x8c, 0xe3, 0x57, 0xd8, 0x5c, 0x4f, 0x61, 0x9f, 0x44, 0x5c, 0xe0, 274 | 0x03, 0xc4, 0x5d, 0x36, 0xc4, 0x70, 0x3e, 0xda, 0x65, 0xb3, 0x9e, 0xb0, 0xe1, 0xca, 0xd7, 0xdc, 275 | 0xd3, 0xc7, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, 0xba, 0xb6, 0x66, 0x68, 0x9e, 0x01, 0x00, 0x00, 276 | } 277 | -------------------------------------------------------------------------------- /proto/service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | 4 | package proto; 5 | 6 | // The database service definition 7 | service DBProvider { 8 | // Query executes a statement that reads data. 9 | rpc Query (QueryRequest) returns (QueryResponse) {} 10 | 11 | // Exec executes a statement that writes data. 12 | rpc Exec (ExecRequest) returns (ExecResponse) {} 13 | } 14 | 15 | message Row { 16 | repeated string values = 1; 17 | } 18 | 19 | // The query statement 20 | message QueryRequest { 21 | string stmt = 1; 22 | } 23 | 24 | // The query response 25 | message QueryResponse { 26 | repeated string columns = 1; 27 | repeated Row rows = 3; 28 | } 29 | 30 | // The exec statement 31 | message ExecRequest { 32 | string stmt = 1; 33 | } 34 | 35 | // The exec response 36 | message ExecResponse { 37 | int64 lastInsertId = 1; 38 | int64 rowsAffected = 2; 39 | } 40 | 41 | -------------------------------------------------------------------------------- /service/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package service represents a gRPC service that communicates with a database backend. 3 | 4 | It works with any database that provides a sql.DB object. 5 | */ 6 | package service 7 | -------------------------------------------------------------------------------- /service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "net" 7 | "os" 8 | "time" 9 | 10 | pb "github.com/otoolep/go-grpc-pg/proto" 11 | 12 | "golang.org/x/net/context" 13 | "google.golang.org/grpc" 14 | ) 15 | 16 | // Service represents a gRPC service that communicates with a database backend. 17 | type Service struct { 18 | grpc *grpc.Server 19 | db *sql.DB 20 | 21 | ln net.Listener 22 | addr string 23 | 24 | logger *log.Logger 25 | } 26 | 27 | // New returns an instantiated service. 28 | func New(addr string, db *sql.DB) *Service { 29 | s := Service{ 30 | grpc: grpc.NewServer(), 31 | db: db, 32 | addr: addr, 33 | logger: log.New(os.Stderr, "[service] ", log.LstdFlags), 34 | } 35 | 36 | pb.RegisterDBProviderServer(s.grpc, (*grpcService)(&s)) 37 | return &s 38 | } 39 | 40 | // Addr returns the address on which the service is listening. 41 | func (s *Service) Addr() string { 42 | return s.ln.Addr().String() 43 | } 44 | 45 | // Open opens the service, starting it listening on the configured address. 46 | func (s *Service) Open() error { 47 | ln, err := net.Listen("tcp", s.addr) 48 | if err != nil { 49 | return err 50 | } 51 | s.ln = ln 52 | s.logger.Println("listening on", s.ln.Addr().String()) 53 | 54 | go func() { 55 | err := s.grpc.Serve(s.ln) 56 | if err != nil { 57 | s.logger.Println("gRPC Serve() returned:", err.Error()) 58 | } 59 | }() 60 | 61 | return nil 62 | } 63 | 64 | // Close closes the service. 65 | func (s *Service) Close() error { 66 | s.grpc.GracefulStop() 67 | s.logger.Println("gRPC server stopped") 68 | return nil 69 | } 70 | 71 | // grpcService is an unexported type, that is the same type as Service. 72 | // 73 | // Having the methods that the gRPC service requires on this type means that even though 74 | // the methods are exported, since the type is not, these methods are not visible outside 75 | // this package. 76 | type grpcService Service 77 | 78 | // Query implements the Query interface of the gRPC service. 79 | func (g *grpcService) Query(c context.Context, q *pb.QueryRequest) (*pb.QueryResponse, error) { 80 | start := time.Now() 81 | rows, err := g.db.Query(q.Stmt) 82 | if err != nil { 83 | return nil, err 84 | } 85 | defer rows.Close() 86 | 87 | // Get the column names. 88 | cols, err := rows.Columns() 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | response := pb.QueryResponse{ 94 | Columns: cols, 95 | } 96 | 97 | // Iterate through each row returned by the query. 98 | for rows.Next() { 99 | row := make([]string, len(cols)) 100 | // Get a set of pointers to the strings allocated above. 101 | rowI := make([]interface{}, len(cols)) 102 | for i := range row { 103 | rowI[i] = &row[i] 104 | } 105 | 106 | if err := rows.Scan(rowI...); err != nil { 107 | return nil, err 108 | } 109 | 110 | // Add the latest rows to existing rows. 111 | response.Rows = append(response.Rows, &pb.Row{Values: row}) 112 | } 113 | 114 | g.logger.Printf(`query '%s' completed in %s, %d %s returned`, 115 | q.Stmt, time.Since(start), len(response.Rows), prettyRows(int64(len(response.Rows)))) 116 | return &response, nil 117 | } 118 | 119 | // Exec implements the Exec interface of the gRPC service. 120 | func (g *grpcService) Exec(c context.Context, e *pb.ExecRequest) (*pb.ExecResponse, error) { 121 | start := time.Now() 122 | r, err := g.db.Exec(e.Stmt) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | lid, err := r.LastInsertId() 128 | if err != nil { 129 | // Not all databases support LastInsertId() 130 | lid = -1 131 | } 132 | ra, err := r.RowsAffected() 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | g.logger.Printf(`exec '%s' completed in %s, %d %s affected`, 138 | e.Stmt, time.Since(start), ra, prettyRows(ra)) 139 | return &pb.ExecResponse{ 140 | LastInsertId: lid, 141 | RowsAffected: ra, 142 | }, nil 143 | } 144 | 145 | // prettyRows returns a singular or plural form of "row", depending on n. 146 | func prettyRows(n int64) string { 147 | if n == 1 { 148 | return "row" 149 | } 150 | return "rows" 151 | } 152 | -------------------------------------------------------------------------------- /service/service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "database/sql" 5 | "testing" 6 | 7 | pb "github.com/otoolep/go-grpc-pg/proto" 8 | 9 | _ "github.com/mattn/go-sqlite3" 10 | "golang.org/x/net/context" 11 | "google.golang.org/grpc" 12 | ) 13 | 14 | // Test_NewService tests creation of a service in the simplest manner. 15 | func Test_NewService(t *testing.T) { 16 | db := mustCreateSQLiteDatabase() 17 | defer db.Close() 18 | 19 | s := New(":0", db) 20 | if s == nil { 21 | t.Fatalf("failed to create service") 22 | } 23 | } 24 | 25 | // Test_NewServiceQueries performs a full test of the service. 26 | func Test_NewServiceOpen(t *testing.T) { 27 | db := mustCreateSQLiteDatabase() 28 | defer db.Close() 29 | 30 | s := New(":0", db) 31 | if s == nil { 32 | t.Fatalf("failed to create service") 33 | } 34 | 35 | // Test opening of the service. 36 | if err := s.Open(); err != nil { 37 | t.Fatalf("failed to open service: %s", err.Error()) 38 | } 39 | 40 | // Connect to the gRPC service. 41 | addr := s.Addr() 42 | conn, err := grpc.Dial(addr, grpc.WithInsecure()) 43 | if err != nil { 44 | t.Fatalf("failed to connect to service: %s", err.Error()) 45 | } 46 | defer conn.Close() 47 | 48 | // Create a client. 49 | client := pb.NewDBProviderClient(conn) 50 | 51 | // Test creation of a table. 52 | _, err = client.Exec(context.Background(), &pb.ExecRequest{Stmt: `CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)`}) 53 | if err != nil { 54 | t.Fatalf("failed to create table: %s", err.Error()) 55 | } 56 | 57 | // Run some queries. 58 | r, err := client.Query(context.Background(), &pb.QueryRequest{Stmt: "SELECT * FROM foo"}) 59 | if err != nil { 60 | t.Fatalf("failed to query empty table: %s", err.Error()) 61 | } 62 | if exp, got := 2, len(r.Columns); exp != got { 63 | t.Fatalf("wrong number of columns returned, exp: %d, got: %d ", exp, got) 64 | } 65 | 66 | // Insert some data. 67 | _, err = client.Exec(context.Background(), &pb.ExecRequest{Stmt: `INSERT INTO foo(name) VALUES("fiona")`}) 68 | if err != nil { 69 | t.Fatalf("failed to insert a row: %s", err.Error()) 70 | } 71 | 72 | // Run more queries. 73 | r, err = client.Query(context.Background(), &pb.QueryRequest{Stmt: "SELECT * FROM foo"}) 74 | if err != nil { 75 | t.Fatalf("failed to query non-empty table: %s", err.Error()) 76 | } 77 | if exp, got := 2, len(r.Columns); exp != got { 78 | t.Fatalf("wrong number of columns returned, exp: %d, got: %d ", exp, got) 79 | } 80 | } 81 | 82 | // mustCreateSQLiteDatabase creates an in-memory SQLite database, or panics. 83 | func mustCreateSQLiteDatabase() *sql.DB { 84 | db, err := sql.Open("sqlite3", ":memory:") 85 | if err != nil { 86 | panic("failed to create in-memory SQLite database") 87 | } 88 | return db 89 | } 90 | --------------------------------------------------------------------------------