diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 2980577..136df4f 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,5 +1,24 @@ { "ImportPath": "github.com/bluebird-tech/puffin_api", "GoVersion": "go1.4.2", - "Deps": [] + "Deps": [ + { + "ImportPath": "github.com/ant0ine/go-json-rest/rest", + "Comment": "v3.2.0-11-g2af9d00", + "Rev": "2af9d0034afcd83101abb1945f2a65ddbd563719" + }, + { + "ImportPath": "github.com/jinzhu/gorm", + "Rev": "dd0d4d931f0d6b52e3c8df75496b0a1aaa5667e6" + }, + { + "ImportPath": "github.com/lib/pq", + "Comment": "go1.0-cutoff-59-gb269bd0", + "Rev": "b269bd035a727d6c1081f76e7a239a1b00674c40" + }, + { + "ImportPath": "github.com/qor/inflection", + "Rev": "45321e63b98c756df17369c027c1df89298e1288" + } + ] } diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/access_log_apache.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/access_log_apache.go new file mode 100644 index 0000000..72761f8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/access_log_apache.go @@ -0,0 +1,231 @@ +package rest + +import ( + "bytes" + "fmt" + "log" + "os" + "strings" + "text/template" + "time" +) + +// TODO Future improvements: +// * support %{strftime}t ? +// * support %{
}o to print headers + +// AccessLogFormat defines the format of the access log record. +// This implementation is a subset of Apache mod_log_config. +// (See http://httpd.apache.org/docs/2.0/mod/mod_log_config.html) +// +// %b content length in bytes, - if 0 +// %B content length in bytes +// %D response elapsed time in microseconds +// %h remote address +// %H server protocol +// %l identd logname, not supported, - +// %m http method +// %P process id +// %q query string +// %r first line of the request +// %s status code +// %S status code preceeded by a terminal color +// %t time of the request +// %T response elapsed time in seconds, 3 decimals +// %u remote user, - if missing +// %{User-Agent}i user agent, - if missing +// %{Referer}i referer, - is missing +// +// Some predefined formats are provided as contants. +type AccessLogFormat string + +const ( + // CommonLogFormat is the Common Log Format (CLF). + CommonLogFormat = "%h %l %u %t \"%r\" %s %b" + + // CombinedLogFormat is the NCSA extended/combined log format. + CombinedLogFormat = "%h %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\"" + + // DefaultLogFormat is the default format, colored output and response time, convenient for development. + DefaultLogFormat = "%t %S\033[0m \033[36;1m%Dμs\033[0m \"%r\" \033[1;30m%u \"%{User-Agent}i\"\033[0m" +) + +// AccessLogApacheMiddleware produces the access log following a format inspired by Apache +// mod_log_config. It depends on TimerMiddleware and RecorderMiddleware that should be in the wrapped +// middlewares. It also uses request.Env["REMOTE_USER"].(string) set by the auth middlewares. +type AccessLogApacheMiddleware struct { + + // Logger points to the logger object used by this middleware, it defaults to + // log.New(os.Stderr, "", 0). + Logger *log.Logger + + // Format defines the format of the access log record. See AccessLogFormat for the details. + // It defaults to DefaultLogFormat. + Format AccessLogFormat + + textTemplate *template.Template +} + +// MiddlewareFunc makes AccessLogApacheMiddleware implement the Middleware interface. +func (mw *AccessLogApacheMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { + + // set the default Logger + if mw.Logger == nil { + mw.Logger = log.New(os.Stderr, "", 0) + } + + // set default format + if mw.Format == "" { + mw.Format = DefaultLogFormat + } + + mw.convertFormat() + + return func(w ResponseWriter, r *Request) { + + // call the handler + h(w, r) + + util := &accessLogUtil{w, r} + + mw.Logger.Print(mw.executeTextTemplate(util)) + } +} + +var apacheAdapter = strings.NewReplacer( + "%b", "{{.BytesWritten | dashIf0}}", + "%B", "{{.BytesWritten}}", + "%D", "{{.ResponseTime | microseconds}}", + "%h", "{{.ApacheRemoteAddr}}", + "%H", "{{.R.Proto}}", + "%l", "-", + "%m", "{{.R.Method}}", + "%P", "{{.Pid}}", + "%q", "{{.ApacheQueryString}}", + "%r", "{{.R.Method}} {{.R.URL.RequestURI}} {{.R.Proto}}", + "%s", "{{.StatusCode}}", + "%S", "\033[{{.StatusCode | statusCodeColor}}m{{.StatusCode}}", + "%t", "{{if .StartTime}}{{.StartTime.Format \"02/Jan/2006:15:04:05 -0700\"}}{{end}}", + "%T", "{{if .ResponseTime}}{{.ResponseTime.Seconds | printf \"%.3f\"}}{{end}}", + "%u", "{{.RemoteUser | dashIfEmptyStr}}", + "%{User-Agent}i", "{{.R.UserAgent | dashIfEmptyStr}}", + "%{Referer}i", "{{.R.Referer | dashIfEmptyStr}}", +) + +// Convert the Apache access log format into a text/template +func (mw *AccessLogApacheMiddleware) convertFormat() { + + tmplText := apacheAdapter.Replace(string(mw.Format)) + + funcMap := template.FuncMap{ + "dashIfEmptyStr": func(value string) string { + if value == "" { + return "-" + } + return value + }, + "dashIf0": func(value int64) string { + if value == 0 { + return "-" + } + return fmt.Sprintf("%d", value) + }, + "microseconds": func(dur *time.Duration) string { + return fmt.Sprintf("%d", dur.Nanoseconds()/1000) + }, + "statusCodeColor": func(statusCode int) string { + if statusCode >= 400 && statusCode < 500 { + return "1;33" + } else if statusCode >= 500 { + return "0;31" + } + return "0;32" + }, + } + + var err error + mw.textTemplate, err = template.New("accessLog").Funcs(funcMap).Parse(tmplText) + if err != nil { + panic(err) + } +} + +// Execute the text template with the data derived from the request, and return a string. +func (mw *AccessLogApacheMiddleware) executeTextTemplate(util *accessLogUtil) string { + buf := bytes.NewBufferString("") + err := mw.textTemplate.Execute(buf, util) + if err != nil { + panic(err) + } + return buf.String() +} + +// accessLogUtil provides a collection of utility functions that devrive data from the Request object. +// This object is used to provide data to the Apache Style template and the the JSON log record. +type accessLogUtil struct { + W ResponseWriter + R *Request +} + +// As stored by the auth middlewares. +func (u *accessLogUtil) RemoteUser() string { + if u.R.Env["REMOTE_USER"] != nil { + return u.R.Env["REMOTE_USER"].(string) + } + return "" +} + +// If qs exists then return it with a leadin "?", apache log style. +func (u *accessLogUtil) ApacheQueryString() string { + if u.R.URL.RawQuery != "" { + return "?" + u.R.URL.RawQuery + } + return "" +} + +// When the request entered the timer middleware. +func (u *accessLogUtil) StartTime() *time.Time { + if u.R.Env["START_TIME"] != nil { + return u.R.Env["START_TIME"].(*time.Time) + } + return nil +} + +// If remoteAddr is set then return is without the port number, apache log style. +func (u *accessLogUtil) ApacheRemoteAddr() string { + remoteAddr := u.R.RemoteAddr + if remoteAddr != "" { + parts := strings.SplitN(remoteAddr, ":", 2) + return parts[0] + } + return "" +} + +// As recorded by the recorder middleware. +func (u *accessLogUtil) StatusCode() int { + if u.R.Env["STATUS_CODE"] != nil { + return u.R.Env["STATUS_CODE"].(int) + } + return 0 +} + +// As mesured by the timer middleware. +func (u *accessLogUtil) ResponseTime() *time.Duration { + if u.R.Env["ELAPSED_TIME"] != nil { + return u.R.Env["ELAPSED_TIME"].(*time.Duration) + } + return nil +} + +// Process id. +func (u *accessLogUtil) Pid() int { + return os.Getpid() +} + +// As recorded by the recorder middleware. +func (u *accessLogUtil) BytesWritten() int64 { + if u.R.Env["BYTES_WRITTEN"] != nil { + return u.R.Env["BYTES_WRITTEN"].(int64) + } + return 0 +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/access_log_apache_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/access_log_apache_test.go new file mode 100644 index 0000000..6412744 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/access_log_apache_test.go @@ -0,0 +1,78 @@ +package rest + +import ( + "bytes" + "github.com/ant0ine/go-json-rest/rest/test" + "log" + "regexp" + "testing" +) + +func TestAccessLogApacheMiddleware(t *testing.T) { + + api := NewApi() + + // the middlewares stack + buffer := bytes.NewBufferString("") + api.Use(&AccessLogApacheMiddleware{ + Logger: log.New(buffer, "", 0), + Format: CommonLogFormat, + textTemplate: nil, + }) + api.Use(&TimerMiddleware{}) + api.Use(&RecorderMiddleware{}) + + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + // wrap all + handler := api.MakeHandler() + + req := test.MakeSimpleRequest("GET", "http://localhost/", nil) + req.RemoteAddr = "127.0.0.1:1234" + recorded := test.RunRequest(t, handler, req) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + + // log tests, eg: '127.0.0.1 - - 29/Nov/2014:22:28:34 +0000 "GET / HTTP/1.1" 200 12' + apacheCommon := regexp.MustCompile(`127.0.0.1 - - \d{2}/\w{3}/\d{4}:\d{2}:\d{2}:\d{2} [+\-]\d{4}\ "GET / HTTP/1.1" 200 12`) + + if !apacheCommon.Match(buffer.Bytes()) { + t.Errorf("Got: %s", buffer.String()) + } +} + +func TestAccessLogApacheMiddlewareMissingData(t *testing.T) { + + api := NewApi() + + // the uncomplete middlewares stack + buffer := bytes.NewBufferString("") + api.Use(&AccessLogApacheMiddleware{ + Logger: log.New(buffer, "", 0), + Format: CommonLogFormat, + textTemplate: nil, + }) + + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + // wrap all + handler := api.MakeHandler() + + req := test.MakeSimpleRequest("GET", "http://localhost/", nil) + recorded := test.RunRequest(t, handler, req) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + + // not much to log when the Env data is missing, but this should still work + apacheCommon := regexp.MustCompile(` - - "GET / HTTP/1.1" 0 -`) + + if !apacheCommon.Match(buffer.Bytes()) { + t.Errorf("Got: %s", buffer.String()) + } +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/access_log_json.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/access_log_json.go new file mode 100644 index 0000000..a6bc175 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/access_log_json.go @@ -0,0 +1,88 @@ +package rest + +import ( + "encoding/json" + "log" + "os" + "time" +) + +// AccessLogJsonMiddleware produces the access log with records written as JSON. This middleware +// depends on TimerMiddleware and RecorderMiddleware that must be in the wrapped middlewares. It +// also uses request.Env["REMOTE_USER"].(string) set by the auth middlewares. +type AccessLogJsonMiddleware struct { + + // Logger points to the logger object used by this middleware, it defaults to + // log.New(os.Stderr, "", 0). + Logger *log.Logger +} + +// MiddlewareFunc makes AccessLogJsonMiddleware implement the Middleware interface. +func (mw *AccessLogJsonMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { + + // set the default Logger + if mw.Logger == nil { + mw.Logger = log.New(os.Stderr, "", 0) + } + + return func(w ResponseWriter, r *Request) { + + // call the handler + h(w, r) + + mw.Logger.Printf("%s", makeAccessLogJsonRecord(r).asJson()) + } +} + +// AccessLogJsonRecord is the data structure used by AccessLogJsonMiddleware to create the JSON +// records. (Public for documentation only, no public method uses it) +type AccessLogJsonRecord struct { + Timestamp *time.Time + StatusCode int + ResponseTime *time.Duration + HttpMethod string + RequestURI string + RemoteUser string + UserAgent string +} + +func makeAccessLogJsonRecord(r *Request) *AccessLogJsonRecord { + + var timestamp *time.Time + if r.Env["START_TIME"] != nil { + timestamp = r.Env["START_TIME"].(*time.Time) + } + + var statusCode int + if r.Env["STATUS_CODE"] != nil { + statusCode = r.Env["STATUS_CODE"].(int) + } + + var responseTime *time.Duration + if r.Env["ELAPSED_TIME"] != nil { + responseTime = r.Env["ELAPSED_TIME"].(*time.Duration) + } + + var remoteUser string + if r.Env["REMOTE_USER"] != nil { + remoteUser = r.Env["REMOTE_USER"].(string) + } + + return &AccessLogJsonRecord{ + Timestamp: timestamp, + StatusCode: statusCode, + ResponseTime: responseTime, + HttpMethod: r.Method, + RequestURI: r.URL.RequestURI(), + RemoteUser: remoteUser, + UserAgent: r.UserAgent(), + } +} + +func (r *AccessLogJsonRecord) asJson() []byte { + b, err := json.Marshal(r) + if err != nil { + panic(err) + } + return b +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/access_log_json_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/access_log_json_test.go new file mode 100644 index 0000000..9085fcb --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/access_log_json_test.go @@ -0,0 +1,53 @@ +package rest + +import ( + "bytes" + "encoding/json" + "github.com/ant0ine/go-json-rest/rest/test" + "log" + "testing" +) + +func TestAccessLogJsonMiddleware(t *testing.T) { + + api := NewApi() + + // the middlewares stack + buffer := bytes.NewBufferString("") + api.Use(&AccessLogJsonMiddleware{ + Logger: log.New(buffer, "", 0), + }) + api.Use(&TimerMiddleware{}) + api.Use(&RecorderMiddleware{}) + + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + // wrap all + handler := api.MakeHandler() + + req := test.MakeSimpleRequest("GET", "http://localhost/", nil) + req.RemoteAddr = "127.0.0.1:1234" + recorded := test.RunRequest(t, handler, req) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + + // log tests + decoded := &AccessLogJsonRecord{} + err := json.Unmarshal(buffer.Bytes(), decoded) + if err != nil { + t.Fatal(err) + } + + if decoded.StatusCode != 200 { + t.Errorf("StatusCode 200 expected, got %d", decoded.StatusCode) + } + if decoded.RequestURI != "/" { + t.Errorf("RequestURI / expected, got %s", decoded.RequestURI) + } + if decoded.HttpMethod != "GET" { + t.Errorf("HttpMethod GET expected, got %s", decoded.HttpMethod) + } +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/api.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/api.go new file mode 100644 index 0000000..6295430 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/api.go @@ -0,0 +1,83 @@ +package rest + +import ( + "net/http" +) + +// Api defines a stack of Middlewares and an App. +type Api struct { + stack []Middleware + app App +} + +// NewApi makes a new Api object. The Middleware stack is empty, and the App is nil. +func NewApi() *Api { + return &Api{ + stack: []Middleware{}, + app: nil, + } +} + +// Use pushes one or multiple middlewares to the stack for middlewares +// maintained in the Api object. +func (api *Api) Use(middlewares ...Middleware) { + api.stack = append(api.stack, middlewares...) +} + +// SetApp sets the App in the Api object. +func (api *Api) SetApp(app App) { + api.app = app +} + +// MakeHandler wraps all the Middlewares of the stack and the App together, and returns an +// http.Handler ready to be used. If the Middleware stack is empty the App is used directly. If the +// App is nil, a HandlerFunc that does nothing is used instead. +func (api *Api) MakeHandler() http.Handler { + var appFunc HandlerFunc + if api.app != nil { + appFunc = api.app.AppFunc() + } else { + appFunc = func(w ResponseWriter, r *Request) {} + } + return http.HandlerFunc( + adapterFunc( + WrapMiddlewares(api.stack, appFunc), + ), + ) +} + +// Defines a stack of middlewares convenient for development. Among other things: +// console friendly logging, JSON indentation, error stack strace in the response. +var DefaultDevStack = []Middleware{ + &AccessLogApacheMiddleware{}, + &TimerMiddleware{}, + &RecorderMiddleware{}, + &PoweredByMiddleware{}, + &RecoverMiddleware{ + EnableResponseStackTrace: true, + }, + &JsonIndentMiddleware{}, + &ContentTypeCheckerMiddleware{}, +} + +// Defines a stack of middlewares convenient for production. Among other things: +// Apache CombinedLogFormat logging, gzip compression. +var DefaultProdStack = []Middleware{ + &AccessLogApacheMiddleware{ + Format: CombinedLogFormat, + }, + &TimerMiddleware{}, + &RecorderMiddleware{}, + &PoweredByMiddleware{}, + &RecoverMiddleware{}, + &GzipMiddleware{}, + &ContentTypeCheckerMiddleware{}, +} + +// Defines a stack of middlewares that should be common to most of the middleware stacks. +var DefaultCommonStack = []Middleware{ + &TimerMiddleware{}, + &RecorderMiddleware{}, + &PoweredByMiddleware{}, + &RecoverMiddleware{}, +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/api_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/api_test.go new file mode 100644 index 0000000..eb4b3aa --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/api_test.go @@ -0,0 +1,97 @@ +package rest + +import ( + "github.com/ant0ine/go-json-rest/rest/test" + "testing" +) + +func TestApiNoAppNoMiddleware(t *testing.T) { + + api := NewApi() + if api == nil { + t.Fatal("Api object must be instantiated") + } + + handler := api.MakeHandler() + if handler == nil { + t.Fatal("the http.Handler must be have been create") + } + + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) + recorded.CodeIs(200) +} + +func TestApiSimpleAppNoMiddleware(t *testing.T) { + + api := NewApi() + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + handler := api.MakeHandler() + if handler == nil { + t.Fatal("the http.Handler must be have been create") + } + + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.BodyIs(`{"Id":"123"}`) +} + +func TestDevStack(t *testing.T) { + + api := NewApi() + api.Use(DefaultDevStack...) + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + handler := api.MakeHandler() + if handler == nil { + t.Fatal("the http.Handler must be have been create") + } + + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.BodyIs("{\n \"Id\": \"123\"\n}") +} + +func TestProdStack(t *testing.T) { + + api := NewApi() + api.Use(DefaultProdStack...) + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + handler := api.MakeHandler() + if handler == nil { + t.Fatal("the http.Handler must be have been create") + } + + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.ContentEncodingIsGzip() +} + +func TestCommonStack(t *testing.T) { + + api := NewApi() + api.Use(DefaultCommonStack...) + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + handler := api.MakeHandler() + if handler == nil { + t.Fatal("the http.Handler must be have been create") + } + + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.BodyIs(`{"Id":"123"}`) +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/auth_basic.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/auth_basic.go new file mode 100644 index 0000000..dbf254c --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/auth_basic.go @@ -0,0 +1,100 @@ +package rest + +import ( + "encoding/base64" + "errors" + "log" + "net/http" + "strings" +) + +// AuthBasicMiddleware provides a simple AuthBasic implementation. On failure, a 401 HTTP response +//is returned. On success, the wrapped middleware is called, and the userId is made available as +// request.Env["REMOTE_USER"].(string) +type AuthBasicMiddleware struct { + + // Realm name to display to the user. Required. + Realm string + + // Callback function that should perform the authentication of the user based on userId and + // password. Must return true on success, false on failure. Required. + Authenticator func(userId string, password string) bool + + // Callback function that should perform the authorization of the authenticated user. Called + // only after an authentication success. Must return true on success, false on failure. + // Optional, default to success. + Authorizator func(userId string, request *Request) bool +} + +// MiddlewareFunc makes AuthBasicMiddleware implement the Middleware interface. +func (mw *AuthBasicMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc { + + if mw.Realm == "" { + log.Fatal("Realm is required") + } + + if mw.Authenticator == nil { + log.Fatal("Authenticator is required") + } + + if mw.Authorizator == nil { + mw.Authorizator = func(userId string, request *Request) bool { + return true + } + } + + return func(writer ResponseWriter, request *Request) { + + authHeader := request.Header.Get("Authorization") + if authHeader == "" { + mw.unauthorized(writer) + return + } + + providedUserId, providedPassword, err := mw.decodeBasicAuthHeader(authHeader) + + if err != nil { + Error(writer, "Invalid authentication", http.StatusBadRequest) + return + } + + if !mw.Authenticator(providedUserId, providedPassword) { + mw.unauthorized(writer) + return + } + + if !mw.Authorizator(providedUserId, request) { + mw.unauthorized(writer) + return + } + + request.Env["REMOTE_USER"] = providedUserId + + handler(writer, request) + } +} + +func (mw *AuthBasicMiddleware) unauthorized(writer ResponseWriter) { + writer.Header().Set("WWW-Authenticate", "Basic realm="+mw.Realm) + Error(writer, "Not Authorized", http.StatusUnauthorized) +} + +func (mw *AuthBasicMiddleware) decodeBasicAuthHeader(header string) (user string, password string, err error) { + + parts := strings.SplitN(header, " ", 2) + if !(len(parts) == 2 && parts[0] == "Basic") { + return "", "", errors.New("Invalid authentication") + } + + decoded, err := base64.StdEncoding.DecodeString(parts[1]) + if err != nil { + return "", "", errors.New("Invalid base64") + } + + creds := strings.SplitN(string(decoded), ":", 2) + if len(creds) != 2 { + return "", "", errors.New("Invalid authentication") + } + + return creds[0], creds[1], nil +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/auth_basic_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/auth_basic_test.go new file mode 100644 index 0000000..8206ca0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/auth_basic_test.go @@ -0,0 +1,78 @@ +package rest + +import ( + "encoding/base64" + "github.com/ant0ine/go-json-rest/rest/test" + "testing" +) + +func TestAuthBasic(t *testing.T) { + + // the middleware to test + authMiddleware := &AuthBasicMiddleware{ + Realm: "test zone", + Authenticator: func(userId string, password string) bool { + if userId == "admin" && password == "admin" { + return true + } + return false + }, + Authorizator: func(userId string, request *Request) bool { + if request.Method == "GET" { + return true + } + return false + }, + } + + // api for testing failure + apiFailure := NewApi() + apiFailure.Use(authMiddleware) + apiFailure.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + t.Error("Should never be executed") + })) + handler := apiFailure.MakeHandler() + + // simple request fails + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) + recorded.CodeIs(401) + recorded.ContentTypeIsJson() + + // auth with wrong cred and right method fails + wrongCredReq := test.MakeSimpleRequest("GET", "http://localhost/", nil) + encoded := base64.StdEncoding.EncodeToString([]byte("admin:AdmIn")) + wrongCredReq.Header.Set("Authorization", "Basic "+encoded) + recorded = test.RunRequest(t, handler, wrongCredReq) + recorded.CodeIs(401) + recorded.ContentTypeIsJson() + + // auth with right cred and wrong method fails + rightCredReq := test.MakeSimpleRequest("POST", "http://localhost/", nil) + encoded = base64.StdEncoding.EncodeToString([]byte("admin:admin")) + rightCredReq.Header.Set("Authorization", "Basic "+encoded) + recorded = test.RunRequest(t, handler, rightCredReq) + recorded.CodeIs(401) + recorded.ContentTypeIsJson() + + // api for testing success + apiSuccess := NewApi() + apiSuccess.Use(authMiddleware) + apiSuccess.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + if r.Env["REMOTE_USER"] == nil { + t.Error("REMOTE_USER is nil") + } + user := r.Env["REMOTE_USER"].(string) + if user != "admin" { + t.Error("REMOTE_USER is expected to be 'admin'") + } + w.WriteJson(map[string]string{"Id": "123"}) + })) + + // auth with right cred and right method succeeds + rightCredReq = test.MakeSimpleRequest("GET", "http://localhost/", nil) + encoded = base64.StdEncoding.EncodeToString([]byte("admin:admin")) + rightCredReq.Header.Set("Authorization", "Basic "+encoded) + recorded = test.RunRequest(t, apiSuccess.MakeHandler(), rightCredReq) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/content_type_checker.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/content_type_checker.go new file mode 100644 index 0000000..1d87877 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/content_type_checker.go @@ -0,0 +1,40 @@ +package rest + +import ( + "mime" + "net/http" + "strings" +) + +// ContentTypeCheckerMiddleware verifies the request Content-Type header and returns a +// StatusUnsupportedMediaType (415) HTTP error response if it's incorrect. The expected +// Content-Type is 'application/json' if the content is non-null. Note: If a charset parameter +// exists, it MUST be UTF-8. +type ContentTypeCheckerMiddleware struct{} + +// MiddlewareFunc makes ContentTypeCheckerMiddleware implement the Middleware interface. +func (mw *ContentTypeCheckerMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc { + + return func(w ResponseWriter, r *Request) { + + mediatype, params, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) + charset, ok := params["charset"] + if !ok { + charset = "UTF-8" + } + + // per net/http doc, means that the length is known and non-null + if r.ContentLength > 0 && + !(mediatype == "application/json" && strings.ToUpper(charset) == "UTF-8") { + + Error(w, + "Bad Content-Type or charset, expected 'application/json'", + http.StatusUnsupportedMediaType, + ) + return + } + + // call the wrapped handler + handler(w, r) + } +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/content_type_checker_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/content_type_checker_test.go new file mode 100644 index 0000000..d8fc7d0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/content_type_checker_test.go @@ -0,0 +1,42 @@ +package rest + +import ( + "github.com/ant0ine/go-json-rest/rest/test" + "testing" +) + +func TestContentTypeCheckerMiddleware(t *testing.T) { + + api := NewApi() + + // the middleware to test + api.Use(&ContentTypeCheckerMiddleware{}) + + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + // wrap all + handler := api.MakeHandler() + + // no payload, no content length, no check + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) + recorded.CodeIs(200) + + // JSON payload with correct content type + recorded = test.RunRequest(t, handler, test.MakeSimpleRequest("POST", "http://localhost/", map[string]string{"Id": "123"})) + recorded.CodeIs(200) + + // JSON payload with correct content type specifying the utf-8 charset + req := test.MakeSimpleRequest("POST", "http://localhost/", map[string]string{"Id": "123"}) + req.Header.Set("Content-Type", "application/json; charset=utf-8") + recorded = test.RunRequest(t, handler, req) + recorded.CodeIs(200) + + // JSON payload with incorrect content type + req = test.MakeSimpleRequest("POST", "http://localhost/", map[string]string{"Id": "123"}) + req.Header.Set("Content-Type", "text/x-json") + recorded = test.RunRequest(t, handler, req) + recorded.CodeIs(415) +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/cors.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/cors.go new file mode 100644 index 0000000..5b00543 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/cors.go @@ -0,0 +1,135 @@ +package rest + +import ( + "net/http" + "strconv" + "strings" +) + +// Possible improvements: +// If AllowedMethods["*"] then Access-Control-Allow-Methods is set to the requested methods +// If AllowedHeaderss["*"] then Access-Control-Allow-Headers is set to the requested headers +// Put some presets in AllowedHeaders +// Put some presets in AccessControlExposeHeaders + +// CorsMiddleware provides a configurable CORS implementation. +type CorsMiddleware struct { + allowedMethods map[string]bool + allowedMethodsCsv string + allowedHeaders map[string]bool + allowedHeadersCsv string + + // Reject non CORS requests if true. See CorsInfo.IsCors. + RejectNonCorsRequests bool + + // Function excecuted for every CORS requests to validate the Origin. (Required) + // Must return true if valid, false if invalid. + // For instance: simple equality, regexp, DB lookup, ... + OriginValidator func(origin string, request *Request) bool + + // List of allowed HTTP methods. Note that the comparison will be made in + // uppercase to avoid common mistakes. And that the + // Access-Control-Allow-Methods response header also uses uppercase. + // (see CorsInfo.AccessControlRequestMethod) + AllowedMethods []string + + // List of allowed HTTP Headers. Note that the comparison will be made with + // noarmalized names (http.CanonicalHeaderKey). And that the response header + // also uses normalized names. + // (see CorsInfo.AccessControlRequestHeaders) + AllowedHeaders []string + + // List of headers used to set the Access-Control-Expose-Headers header. + AccessControlExposeHeaders []string + + // User to se the Access-Control-Allow-Credentials response header. + AccessControlAllowCredentials bool + + // Used to set the Access-Control-Max-Age response header, in seconds. + AccessControlMaxAge int +} + +// MiddlewareFunc makes CorsMiddleware implement the Middleware interface. +func (mw *CorsMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc { + + // precompute as much as possible at init time + + mw.allowedMethods = map[string]bool{} + normedMethods := []string{} + for _, allowedMethod := range mw.AllowedMethods { + normed := strings.ToUpper(allowedMethod) + mw.allowedMethods[normed] = true + normedMethods = append(normedMethods, normed) + } + mw.allowedMethodsCsv = strings.Join(normedMethods, ",") + + mw.allowedHeaders = map[string]bool{} + normedHeaders := []string{} + for _, allowedHeader := range mw.AllowedHeaders { + normed := http.CanonicalHeaderKey(allowedHeader) + mw.allowedHeaders[normed] = true + normedHeaders = append(normedHeaders, normed) + } + mw.allowedHeadersCsv = strings.Join(normedHeaders, ",") + + return func(writer ResponseWriter, request *Request) { + + corsInfo := request.GetCorsInfo() + + // non CORS requests + if !corsInfo.IsCors { + if mw.RejectNonCorsRequests { + Error(writer, "Non CORS request", http.StatusForbidden) + return + } + // continue, execute the wrapped middleware + handler(writer, request) + return + } + + // Validate the Origin + if mw.OriginValidator(corsInfo.Origin, request) == false { + Error(writer, "Invalid Origin", http.StatusForbidden) + return + } + + if corsInfo.IsPreflight { + + // check the request methods + if mw.allowedMethods[corsInfo.AccessControlRequestMethod] == false { + Error(writer, "Invalid Preflight Request", http.StatusForbidden) + return + } + + // check the request headers + for _, requestedHeader := range corsInfo.AccessControlRequestHeaders { + if mw.allowedHeaders[requestedHeader] == false { + Error(writer, "Invalid Preflight Request", http.StatusForbidden) + return + } + } + + writer.Header().Set("Access-Control-Allow-Methods", mw.allowedMethodsCsv) + writer.Header().Set("Access-Control-Allow-Headers", mw.allowedHeadersCsv) + writer.Header().Set("Access-Control-Allow-Origin", corsInfo.Origin) + if mw.AccessControlAllowCredentials == true { + writer.Header().Set("Access-Control-Allow-Credentials", "true") + } + writer.Header().Set("Access-Control-Max-Age", strconv.Itoa(mw.AccessControlMaxAge)) + writer.WriteHeader(http.StatusOK) + return + } + + // Non-preflight requests + for _, exposed := range mw.AccessControlExposeHeaders { + writer.Header().Add("Access-Control-Expose-Headers", exposed) + } + writer.Header().Set("Access-Control-Allow-Origin", corsInfo.Origin) + if mw.AccessControlAllowCredentials == true { + writer.Header().Set("Access-Control-Allow-Credentials", "true") + } + // continure, execute the wrapped middleware + handler(writer, request) + return + } +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/doc.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/doc.go new file mode 100644 index 0000000..fa6f5b2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/doc.go @@ -0,0 +1,47 @@ +// A quick and easy way to setup a RESTful JSON API +// +// http://ant0ine.github.io/go-json-rest/ +// +// Go-Json-Rest is a thin layer on top of net/http that helps building RESTful JSON APIs easily. +// It provides fast and scalable request routing using a Trie based implementation, helpers to deal +// with JSON requests and responses, and middlewares for functionalities like CORS, Auth, Gzip, +// Status, ... +// +// Example: +// +// package main +// +// import ( +// "github.com/ant0ine/go-json-rest/rest" +// "log" +// "net/http" +// ) +// +// type User struct { +// Id string +// Name string +// } +// +// func GetUser(w rest.ResponseWriter, req *rest.Request) { +// user := User{ +// Id: req.PathParam("id"), +// Name: "Antoine", +// } +// w.WriteJson(&user) +// } +// +// func main() { +// api := rest.NewApi() +// api.Use(rest.DefaultDevStack...) +// router, err := rest.MakeRouter( +// rest.Get("/users/:id", GetUser), +// ) +// if err != nil { +// log.Fatal(err) +// } +// api.SetApp(router) +// log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) +// } +// +// +package rest diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/gzip.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/gzip.go new file mode 100644 index 0000000..10f4e9d --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/gzip.go @@ -0,0 +1,126 @@ +package rest + +import ( + "bufio" + "compress/gzip" + "net" + "net/http" + "strings" +) + +// GzipMiddleware is responsible for compressing the payload with gzip and setting the proper +// headers when supported by the client. It must be wrapped by TimerMiddleware for the +// compression time to be captured. And It must be wrapped by RecorderMiddleware for the +// compressed BYTES_WRITTEN to be captured. +type GzipMiddleware struct{} + +// MiddlewareFunc makes GzipMiddleware implement the Middleware interface. +func (mw *GzipMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { + return func(w ResponseWriter, r *Request) { + // gzip support enabled + canGzip := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") + // client accepts gzip ? + writer := &gzipResponseWriter{w, false, canGzip, nil} + // call the handler with the wrapped writer + h(writer, r) + } +} + +// Private responseWriter intantiated by the gzip middleware. +// It encodes the payload with gzip and set the proper headers. +// It implements the following interfaces: +// ResponseWriter +// http.ResponseWriter +// http.Flusher +// http.CloseNotifier +// http.Hijacker +type gzipResponseWriter struct { + ResponseWriter + wroteHeader bool + canGzip bool + gzipWriter *gzip.Writer +} + +// Set the right headers for gzip encoded responses. +func (w *gzipResponseWriter) WriteHeader(code int) { + + // Always set the Vary header, even if this particular request + // is not gzipped. + w.Header().Add("Vary", "Accept-Encoding") + + if w.canGzip { + w.Header().Set("Content-Encoding", "gzip") + } + + w.ResponseWriter.WriteHeader(code) + w.wroteHeader = true +} + +// Make sure the local Write is called. +func (w *gzipResponseWriter) WriteJson(v interface{}) error { + b, err := w.EncodeJson(v) + if err != nil { + return err + } + _, err = w.Write(b) + if err != nil { + return err + } + return nil +} + +// Make sure the local WriteHeader is called, and call the parent Flush. +// Provided in order to implement the http.Flusher interface. +func (w *gzipResponseWriter) Flush() { + if !w.wroteHeader { + w.WriteHeader(http.StatusOK) + } + flusher := w.ResponseWriter.(http.Flusher) + flusher.Flush() +} + +// Call the parent CloseNotify. +// Provided in order to implement the http.CloseNotifier interface. +func (w *gzipResponseWriter) CloseNotify() <-chan bool { + notifier := w.ResponseWriter.(http.CloseNotifier) + return notifier.CloseNotify() +} + +// Provided in order to implement the http.Hijacker interface. +func (w *gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hijacker := w.ResponseWriter.(http.Hijacker) + return hijacker.Hijack() +} + +// Make sure the local WriteHeader is called, and encode the payload if necessary. +// Provided in order to implement the http.ResponseWriter interface. +func (w *gzipResponseWriter) Write(b []byte) (int, error) { + + if !w.wroteHeader { + w.WriteHeader(http.StatusOK) + } + + writer := w.ResponseWriter.(http.ResponseWriter) + + if w.canGzip { + // Write can be called multiple times for a given response. + // (see the streaming example: + // https://github.com/ant0ine/go-json-rest-examples/tree/master/streaming) + // The gzipWriter is instantiated only once, and flushed after + // each write. + if w.gzipWriter == nil { + w.gzipWriter = gzip.NewWriter(writer) + } + count, errW := w.gzipWriter.Write(b) + errF := w.gzipWriter.Flush() + if errW != nil { + return count, errW + } + if errF != nil { + return count, errF + } + return count, nil + } + + return writer.Write(b) +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/gzip_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/gzip_test.go new file mode 100644 index 0000000..06a7e6f --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/gzip_test.go @@ -0,0 +1,68 @@ +package rest + +import ( + "github.com/ant0ine/go-json-rest/rest/test" + "testing" +) + +func TestGzipEnabled(t *testing.T) { + + api := NewApi() + + // the middleware to test + api.Use(&GzipMiddleware{}) + + // router app with success and error paths + router, err := MakeRouter( + Get("/ok", func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + }), + Get("/error", func(w ResponseWriter, r *Request) { + Error(w, "gzipped error", 500) + }), + ) + if err != nil { + t.Fatal(err) + } + + api.SetApp(router) + + // wrap all + handler := api.MakeHandler() + + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/ok", nil)) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.ContentEncodingIsGzip() + recorded.HeaderIs("Vary", "Accept-Encoding") + + recorded = test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/error", nil)) + recorded.CodeIs(500) + recorded.ContentTypeIsJson() + recorded.ContentEncodingIsGzip() + recorded.HeaderIs("Vary", "Accept-Encoding") +} + +func TestGzipDisabled(t *testing.T) { + + api := NewApi() + + // router app with success and error paths + router, err := MakeRouter( + Get("/ok", func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + }), + ) + if err != nil { + t.Fatal(err) + } + + api.SetApp(router) + handler := api.MakeHandler() + + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/ok", nil)) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.HeaderIs("Content-Encoding", "") + recorded.HeaderIs("Vary", "") +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/handler.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/handler.go new file mode 100644 index 0000000..e038cf7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/handler.go @@ -0,0 +1,191 @@ +package rest + +import ( + "log" + "net/http" +) + +// ResourceHandler implements the http.Handler interface and acts a router for the defined Routes. +// The defaults are intended to be developemnt friendly, for production you may want +// to turn on gzip and disable the JSON indentation for instance. +// ResourceHandler is now DEPRECATED in favor of the new Api object. See the migration guide. +type ResourceHandler struct { + internalRouter *router + statusMiddleware *StatusMiddleware + handlerFunc http.HandlerFunc + + // If true, and if the client accepts the Gzip encoding, the response payloads + // will be compressed using gzip, and the corresponding response header will set. + EnableGzip bool + + // If true, the JSON payload will be written in one line with no space. + DisableJsonIndent bool + + // If true, the status service will be enabled. Various stats and status will + // then be available at GET /.status in a JSON format. + EnableStatusService bool + + // If true, when a "panic" happens, the error string and the stack trace will be + // printed in the 500 response body. + EnableResponseStackTrace bool + + // If true, the records logged to the access log and the error log will be + // printed as JSON. Convenient for log parsing. + // See the AccessLogJsonRecord type for details of the access log JSON record. + EnableLogAsJson bool + + // If true, the handler does NOT check the request Content-Type. Otherwise, it + // must be set to 'application/json' if the content is non-null. + // Note: If a charset parameter exists, it MUST be UTF-8 + EnableRelaxedContentType bool + + // Optional global middlewares that can be used to wrap the all REST endpoints. + // They are used in the defined order, the first wrapping the second, ... + // They are run first, wrapping all go-json-rest middlewares, + // * request.PathParams is not set yet + // * "panic" won't be caught and converted to 500 + // * request.Env["STATUS_CODE"] and request.Env["ELAPSED_TIME"] are set. + // They can be used for extra logging, or reporting. + // (see statsd example in in https://github.com/ant0ine/go-json-rest-examples) + OuterMiddlewares []Middleware + + // Optional global middlewares that can be used to wrap the all REST endpoints. + // They are used in the defined order, the first wrapping the second, ... + // They are run pre REST routing, request.PathParams is not set yet. + // They are run post auto error handling, "panic" will be converted to 500 errors. + // They can be used for instance to manage CORS or authentication. + // (see CORS and Auth examples in https://github.com/ant0ine/go-json-rest-examples) + PreRoutingMiddlewares []Middleware + + // Custom logger for the access log, + // optional, defaults to log.New(os.Stderr, "", 0) + Logger *log.Logger + + // Define the format of the access log record. + // When EnableLogAsJson is false, this format is used to generate the access log. + // See AccessLogFormat for the options and the predefined formats. + // Defaults to a developement friendly format specified by the Default constant. + LoggerFormat AccessLogFormat + + // If true, the access log will be fully disabled. + // (the log middleware is not even instantiated, avoiding any performance penalty) + DisableLogger bool + + // Custom logger used for logging the panic errors, + // optional, defaults to log.New(os.Stderr, "", 0) + ErrorLogger *log.Logger + + // Custom X-Powered-By value, defaults to "go-json-rest". + XPoweredBy string + + // If true, the X-Powered-By header will NOT be set. + DisableXPoweredBy bool +} + +// SetRoutes defines the Routes. The order the Routes matters, +// if a request matches multiple Routes, the first one will be used. +func (rh *ResourceHandler) SetRoutes(routes ...*Route) error { + + log.Print("ResourceHandler is now DEPRECATED in favor of the new Api object, see the migration guide") + + // intantiate all the middlewares based on the settings. + middlewares := []Middleware{} + + middlewares = append(middlewares, + rh.OuterMiddlewares..., + ) + + // log as the first, depends on timer and recorder. + if !rh.DisableLogger { + if rh.EnableLogAsJson { + middlewares = append(middlewares, + &AccessLogJsonMiddleware{ + Logger: rh.Logger, + }, + ) + } else { + middlewares = append(middlewares, + &AccessLogApacheMiddleware{ + Logger: rh.Logger, + Format: rh.LoggerFormat, + }, + ) + } + } + + // also depends on timer and recorder + if rh.EnableStatusService { + // keep track of this middleware for GetStatus() + rh.statusMiddleware = &StatusMiddleware{} + middlewares = append(middlewares, rh.statusMiddleware) + } + + // after gzip in order to track to the content length and speed + middlewares = append(middlewares, + &TimerMiddleware{}, + &RecorderMiddleware{}, + ) + + if rh.EnableGzip { + middlewares = append(middlewares, &GzipMiddleware{}) + } + + if !rh.DisableXPoweredBy { + middlewares = append(middlewares, + &PoweredByMiddleware{ + XPoweredBy: rh.XPoweredBy, + }, + ) + } + + if !rh.DisableJsonIndent { + middlewares = append(middlewares, &JsonIndentMiddleware{}) + } + + // catch user errors + middlewares = append(middlewares, + &RecoverMiddleware{ + Logger: rh.ErrorLogger, + EnableLogAsJson: rh.EnableLogAsJson, + EnableResponseStackTrace: rh.EnableResponseStackTrace, + }, + ) + + middlewares = append(middlewares, + rh.PreRoutingMiddlewares..., + ) + + // verify the request content type + if !rh.EnableRelaxedContentType { + middlewares = append(middlewares, + &ContentTypeCheckerMiddleware{}, + ) + } + + // instantiate the router + rh.internalRouter = &router{ + Routes: routes, + } + err := rh.internalRouter.start() + if err != nil { + return err + } + + // wrap everything + rh.handlerFunc = adapterFunc( + WrapMiddlewares(middlewares, rh.internalRouter.AppFunc()), + ) + + return nil +} + +// This makes ResourceHandler implement the http.Handler interface. +// You probably don't want to use it directly. +func (rh *ResourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + rh.handlerFunc(w, r) +} + +// GetStatus returns a Status object. EnableStatusService must be true. +func (rh *ResourceHandler) GetStatus() *Status { + return rh.statusMiddleware.GetStatus() +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/handler_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/handler_test.go new file mode 100644 index 0000000..a85d438 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/handler_test.go @@ -0,0 +1,109 @@ +package rest + +import ( + "github.com/ant0ine/go-json-rest/rest/test" + "io/ioutil" + "log" + "testing" +) + +func TestHandler(t *testing.T) { + + handler := ResourceHandler{ + DisableJsonIndent: true, + // make the test output less verbose by discarding the error log + ErrorLogger: log.New(ioutil.Discard, "", 0), + } + handler.SetRoutes( + Get("/r/:id", func(w ResponseWriter, r *Request) { + id := r.PathParam("id") + w.WriteJson(map[string]string{"Id": id}) + }), + Post("/r/:id", func(w ResponseWriter, r *Request) { + // JSON echo + data := map[string]string{} + err := r.DecodeJsonPayload(&data) + if err != nil { + t.Fatal(err) + } + w.WriteJson(data) + }), + Get("/auto-fails", func(w ResponseWriter, r *Request) { + a := []int{} + _ = a[0] + }), + Get("/user-error", func(w ResponseWriter, r *Request) { + Error(w, "My error", 500) + }), + Get("/user-notfound", func(w ResponseWriter, r *Request) { + NotFound(w, r) + }), + ) + + // valid get resource + recorded := test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/r/123", nil)) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.BodyIs(`{"Id":"123"}`) + + // valid post resource + recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest( + "POST", "http://1.2.3.4/r/123", &map[string]string{"Test": "Test"})) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.BodyIs(`{"Test":"Test"}`) + + // broken Content-Type post resource + request := test.MakeSimpleRequest("POST", "http://1.2.3.4/r/123", &map[string]string{"Test": "Test"}) + request.Header.Set("Content-Type", "text/html") + recorded = test.RunRequest(t, &handler, request) + recorded.CodeIs(415) + recorded.ContentTypeIsJson() + recorded.BodyIs(`{"Error":"Bad Content-Type or charset, expected 'application/json'"}`) + + // broken Content-Type post resource + request = test.MakeSimpleRequest("POST", "http://1.2.3.4/r/123", &map[string]string{"Test": "Test"}) + request.Header.Set("Content-Type", "application/json; charset=ISO-8859-1") + recorded = test.RunRequest(t, &handler, request) + recorded.CodeIs(415) + recorded.ContentTypeIsJson() + recorded.BodyIs(`{"Error":"Bad Content-Type or charset, expected 'application/json'"}`) + + // Content-Type post resource with charset + request = test.MakeSimpleRequest("POST", "http://1.2.3.4/r/123", &map[string]string{"Test": "Test"}) + request.Header.Set("Content-Type", "application/json;charset=UTF-8") + recorded = test.RunRequest(t, &handler, request) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.BodyIs(`{"Test":"Test"}`) + + // auto 405 on undefined route (wrong method) + recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest("DELETE", "http://1.2.3.4/r/123", nil)) + recorded.CodeIs(405) + recorded.ContentTypeIsJson() + recorded.BodyIs(`{"Error":"Method not allowed"}`) + + // auto 404 on undefined route (wrong path) + recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/s/123", nil)) + recorded.CodeIs(404) + recorded.ContentTypeIsJson() + recorded.BodyIs(`{"Error":"Resource not found"}`) + + // auto 500 on unhandled userecorder error + recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/auto-fails", nil)) + recorded.CodeIs(500) + recorded.ContentTypeIsJson() + recorded.BodyIs(`{"Error":"Internal Server Error"}`) + + // userecorder error + recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/user-error", nil)) + recorded.CodeIs(500) + recorded.ContentTypeIsJson() + recorded.BodyIs(`{"Error":"My error"}`) + + // userecorder notfound + recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/user-notfound", nil)) + recorded.CodeIs(404) + recorded.ContentTypeIsJson() + recorded.BodyIs(`{"Error":"Resource not found"}`) +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/if.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/if.go new file mode 100644 index 0000000..daa37d1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/if.go @@ -0,0 +1,53 @@ +package rest + +import ( + "log" +) + +// IfMiddleware evaluates at runtime a condition based on the current request, and decides to +// execute one of the other Middleware based on this boolean. +type IfMiddleware struct { + + // Runtime condition that decides of the execution of IfTrue of IfFalse. + Condition func(r *Request) bool + + // Middleware to run when the condition is true. Note that the middleware is initialized + // weather if will be used or not. (Optional, pass-through if not set) + IfTrue Middleware + + // Middleware to run when the condition is false. Note that the middleware is initialized + // weather if will be used or not. (Optional, pass-through if not set) + IfFalse Middleware +} + +// MiddlewareFunc makes TimerMiddleware implement the Middleware interface. +func (mw *IfMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { + + if mw.Condition == nil { + log.Fatal("IfMiddleware Condition is required") + } + + var ifTrueHandler HandlerFunc + if mw.IfTrue != nil { + ifTrueHandler = mw.IfTrue.MiddlewareFunc(h) + } else { + ifTrueHandler = h + } + + var ifFalseHandler HandlerFunc + if mw.IfFalse != nil { + ifFalseHandler = mw.IfFalse.MiddlewareFunc(h) + } else { + ifFalseHandler = h + } + + return func(w ResponseWriter, r *Request) { + + if mw.Condition(r) { + ifTrueHandler(w, r) + } else { + ifFalseHandler(w, r) + } + + } +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/if_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/if_test.go new file mode 100644 index 0000000..fca57f4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/if_test.go @@ -0,0 +1,51 @@ +package rest + +import ( + "github.com/ant0ine/go-json-rest/rest/test" + "testing" +) + +func TestIfMiddleware(t *testing.T) { + + api := NewApi() + + // the middleware to test + api.Use(&IfMiddleware{ + Condition: func(r *Request) bool { + if r.URL.Path == "/true" { + return true + } + return false + }, + IfTrue: MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { + return func(w ResponseWriter, r *Request) { + r.Env["TRUE_MIDDLEWARE"] = true + handler(w, r) + } + }), + IfFalse: MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { + return func(w ResponseWriter, r *Request) { + r.Env["FALSE_MIDDLEWARE"] = true + handler(w, r) + } + }), + }) + + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(r.Env) + })) + + // wrap all + handler := api.MakeHandler() + + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.BodyIs("{\"FALSE_MIDDLEWARE\":true}") + + recorded = test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/true", nil)) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.BodyIs("{\"TRUE_MIDDLEWARE\":true}") +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/json_indent.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/json_indent.go new file mode 100644 index 0000000..ad9a5ca --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/json_indent.go @@ -0,0 +1,113 @@ +package rest + +import ( + "bufio" + "encoding/json" + "net" + "net/http" +) + +// JsonIndentMiddleware provides JSON encoding with indentation. +// It could be convenient to use it during development. +// It works by "subclassing" the responseWriter provided by the wrapping middleware, +// replacing the writer.EncodeJson and writer.WriteJson implementations, +// and making the parent implementations ignored. +type JsonIndentMiddleware struct { + + // prefix string, as in json.MarshalIndent + Prefix string + + // indentation string, as in json.MarshalIndent + Indent string +} + +// MiddlewareFunc makes JsonIndentMiddleware implement the Middleware interface. +func (mw *JsonIndentMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc { + + if mw.Indent == "" { + mw.Indent = " " + } + + return func(w ResponseWriter, r *Request) { + + writer := &jsonIndentResponseWriter{w, false, mw.Prefix, mw.Indent} + // call the wrapped handler + handler(writer, r) + } +} + +// Private responseWriter intantiated by the middleware. +// It implements the following interfaces: +// ResponseWriter +// http.ResponseWriter +// http.Flusher +// http.CloseNotifier +// http.Hijacker +type jsonIndentResponseWriter struct { + ResponseWriter + wroteHeader bool + prefix string + indent string +} + +// Replace the parent EncodeJson to provide indentation. +func (w *jsonIndentResponseWriter) EncodeJson(v interface{}) ([]byte, error) { + b, err := json.MarshalIndent(v, w.prefix, w.indent) + if err != nil { + return nil, err + } + return b, nil +} + +// Make sure the local EncodeJson and local Write are called. +// Does not call the parent WriteJson. +func (w *jsonIndentResponseWriter) WriteJson(v interface{}) error { + b, err := w.EncodeJson(v) + if err != nil { + return err + } + _, err = w.Write(b) + if err != nil { + return err + } + return nil +} + +// Call the parent WriteHeader. +func (w *jsonIndentResponseWriter) WriteHeader(code int) { + w.ResponseWriter.WriteHeader(code) + w.wroteHeader = true +} + +// Make sure the local WriteHeader is called, and call the parent Flush. +// Provided in order to implement the http.Flusher interface. +func (w *jsonIndentResponseWriter) Flush() { + if !w.wroteHeader { + w.WriteHeader(http.StatusOK) + } + flusher := w.ResponseWriter.(http.Flusher) + flusher.Flush() +} + +// Call the parent CloseNotify. +// Provided in order to implement the http.CloseNotifier interface. +func (w *jsonIndentResponseWriter) CloseNotify() <-chan bool { + notifier := w.ResponseWriter.(http.CloseNotifier) + return notifier.CloseNotify() +} + +// Provided in order to implement the http.Hijacker interface. +func (w *jsonIndentResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hijacker := w.ResponseWriter.(http.Hijacker) + return hijacker.Hijack() +} + +// Make sure the local WriteHeader is called, and call the parent Write. +// Provided in order to implement the http.ResponseWriter interface. +func (w *jsonIndentResponseWriter) Write(b []byte) (int, error) { + if !w.wroteHeader { + w.WriteHeader(http.StatusOK) + } + writer := w.ResponseWriter.(http.ResponseWriter) + return writer.Write(b) +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/json_indent_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/json_indent_test.go new file mode 100644 index 0000000..58924e0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/json_indent_test.go @@ -0,0 +1,28 @@ +package rest + +import ( + "github.com/ant0ine/go-json-rest/rest/test" + "testing" +) + +func TestJsonIndentMiddleware(t *testing.T) { + + api := NewApi() + + // the middleware to test + api.Use(&JsonIndentMiddleware{}) + + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + // wrap all + handler := api.MakeHandler() + + req := test.MakeSimpleRequest("GET", "http://localhost/", nil) + recorded := test.RunRequest(t, handler, req) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.BodyIs("{\n \"Id\": \"123\"\n}") +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/jsonp.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/jsonp.go new file mode 100644 index 0000000..6071b50 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/jsonp.go @@ -0,0 +1,116 @@ +package rest + +import ( + "bufio" + "net" + "net/http" +) + +// JsonpMiddleware provides JSONP responses on demand, based on the presence +// of a query string argument specifying the callback name. +type JsonpMiddleware struct { + + // Name of the query string parameter used to specify the + // the name of the JS callback used for the padding. + // Defaults to "callback". + CallbackNameKey string +} + +// MiddlewareFunc returns a HandlerFunc that implements the middleware. +func (mw *JsonpMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { + + if mw.CallbackNameKey == "" { + mw.CallbackNameKey = "callback" + } + + return func(w ResponseWriter, r *Request) { + + callbackName := r.URL.Query().Get(mw.CallbackNameKey) + // TODO validate the callbackName ? + + if callbackName != "" { + // the client request JSONP, instantiate JsonpMiddleware. + writer := &jsonpResponseWriter{w, false, callbackName} + // call the handler with the wrapped writer + h(writer, r) + } else { + // do nothing special + h(w, r) + } + + } +} + +// Private responseWriter intantiated by the JSONP middleware. +// It adds the padding to the payload and set the proper headers. +// It implements the following interfaces: +// ResponseWriter +// http.ResponseWriter +// http.Flusher +// http.CloseNotifier +// http.Hijacker +type jsonpResponseWriter struct { + ResponseWriter + wroteHeader bool + callbackName string +} + +// Overwrite the Content-Type to be text/javascript +func (w *jsonpResponseWriter) WriteHeader(code int) { + + w.Header().Set("Content-Type", "text/javascript") + + w.ResponseWriter.WriteHeader(code) + w.wroteHeader = true +} + +// Make sure the local Write is called. +func (w *jsonpResponseWriter) WriteJson(v interface{}) error { + b, err := w.EncodeJson(v) + if err != nil { + return err + } + // JSONP security fix (http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/) + w.Header().Set("Content-Disposition", "filename=f.txt") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Write([]byte("/**/" + w.callbackName + "(")) + w.Write(b) + w.Write([]byte(")")) + return nil +} + +// Make sure the local WriteHeader is called, and call the parent Flush. +// Provided in order to implement the http.Flusher interface. +func (w *jsonpResponseWriter) Flush() { + if !w.wroteHeader { + w.WriteHeader(http.StatusOK) + } + flusher := w.ResponseWriter.(http.Flusher) + flusher.Flush() +} + +// Call the parent CloseNotify. +// Provided in order to implement the http.CloseNotifier interface. +func (w *jsonpResponseWriter) CloseNotify() <-chan bool { + notifier := w.ResponseWriter.(http.CloseNotifier) + return notifier.CloseNotify() +} + +// Provided in order to implement the http.Hijacker interface. +func (w *jsonpResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hijacker := w.ResponseWriter.(http.Hijacker) + return hijacker.Hijack() +} + +// Make sure the local WriteHeader is called. +// Provided in order to implement the http.ResponseWriter interface. +func (w *jsonpResponseWriter) Write(b []byte) (int, error) { + + if !w.wroteHeader { + w.WriteHeader(http.StatusOK) + } + + writer := w.ResponseWriter.(http.ResponseWriter) + + return writer.Write(b) +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/jsonp_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/jsonp_test.go new file mode 100644 index 0000000..e556d8f --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/jsonp_test.go @@ -0,0 +1,47 @@ +package rest + +import ( + "testing" + + "github.com/ant0ine/go-json-rest/rest/test" +) + +func TestJsonpMiddleware(t *testing.T) { + + api := NewApi() + + // the middleware to test + api.Use(&JsonpMiddleware{}) + + // router app with success and error paths + router, err := MakeRouter( + Get("/ok", func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + }), + Get("/error", func(w ResponseWriter, r *Request) { + Error(w, "jsonp error", 500) + }), + ) + if err != nil { + t.Fatal(err) + } + + api.SetApp(router) + + // wrap all + handler := api.MakeHandler() + + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/ok?callback=parseResponse", nil)) + recorded.CodeIs(200) + recorded.HeaderIs("Content-Type", "text/javascript") + recorded.HeaderIs("Content-Disposition", "filename=f.txt") + recorded.HeaderIs("X-Content-Type-Options", "nosniff") + recorded.BodyIs("/**/parseResponse({\"Id\":\"123\"})") + + recorded = test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/error?callback=parseResponse", nil)) + recorded.CodeIs(500) + recorded.HeaderIs("Content-Type", "text/javascript") + recorded.HeaderIs("Content-Disposition", "filename=f.txt") + recorded.HeaderIs("X-Content-Type-Options", "nosniff") + recorded.BodyIs("/**/parseResponse({\"Error\":\"jsonp error\"})") +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/middleware.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/middleware.go new file mode 100644 index 0000000..ba03fb8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/middleware.go @@ -0,0 +1,72 @@ +package rest + +import ( + "net/http" +) + +// HandlerFunc defines the handler function. It is the go-json-rest equivalent of http.HandlerFunc. +type HandlerFunc func(ResponseWriter, *Request) + +// App defines the interface that an object should implement to be used as an app in this framework +// stack. The App is the top element of the stack, the other elements being middlewares. +type App interface { + AppFunc() HandlerFunc +} + +// AppSimple is an adapter type that makes it easy to write an App with a simple function. +// eg: rest.NewApi(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { ... })) +type AppSimple HandlerFunc + +// AppFunc makes AppSimple implement the App interface. +func (as AppSimple) AppFunc() HandlerFunc { + return HandlerFunc(as) +} + +// Middleware defines the interface that objects must implement in order to wrap a HandlerFunc and +// be used in the middleware stack. +type Middleware interface { + MiddlewareFunc(handler HandlerFunc) HandlerFunc +} + +// MiddlewareSimple is an adapter type that makes it easy to write a Middleware with a simple +// function. eg: api.Use(rest.MiddlewareSimple(func(h HandlerFunc) Handlerfunc { ... })) +type MiddlewareSimple func(handler HandlerFunc) HandlerFunc + +// MiddlewareFunc makes MiddlewareSimple implement the Middleware interface. +func (ms MiddlewareSimple) MiddlewareFunc(handler HandlerFunc) HandlerFunc { + return ms(handler) +} + +// WrapMiddlewares calls the MiddlewareFunc methods in the reverse order and returns an HandlerFunc +// ready to be executed. This can be used to wrap a set of middlewares, post routing, on a per Route +// basis. +func WrapMiddlewares(middlewares []Middleware, handler HandlerFunc) HandlerFunc { + wrapped := handler + for i := len(middlewares) - 1; i >= 0; i-- { + wrapped = middlewares[i].MiddlewareFunc(wrapped) + } + return wrapped +} + +// Handle the transition between net/http and go-json-rest objects. +// It intanciates the rest.Request and rest.ResponseWriter, ... +func adapterFunc(handler HandlerFunc) http.HandlerFunc { + + return func(origWriter http.ResponseWriter, origRequest *http.Request) { + + // instantiate the rest objects + request := &Request{ + origRequest, + nil, + map[string]interface{}{}, + } + + writer := &responseWriter{ + origWriter, + false, + } + + // call the wrapped handler + handler(writer, request) + } +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/middleware_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/middleware_test.go new file mode 100644 index 0000000..63b3d1a --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/middleware_test.go @@ -0,0 +1,57 @@ +package rest + +import ( + "testing" +) + +type testMiddleware struct { + name string +} + +func (mw *testMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc { + return func(w ResponseWriter, r *Request) { + if r.Env["BEFORE"] == nil { + r.Env["BEFORE"] = mw.name + } else { + r.Env["BEFORE"] = r.Env["BEFORE"].(string) + mw.name + } + handler(w, r) + if r.Env["AFTER"] == nil { + r.Env["AFTER"] = mw.name + } else { + r.Env["AFTER"] = r.Env["AFTER"].(string) + mw.name + } + } +} + +func TestWrapMiddlewares(t *testing.T) { + + a := &testMiddleware{"A"} + b := &testMiddleware{"B"} + c := &testMiddleware{"C"} + + app := func(w ResponseWriter, r *Request) { + // do nothing + } + + handlerFunc := WrapMiddlewares([]Middleware{a, b, c}, app) + + // fake request + r := &Request{ + nil, + nil, + map[string]interface{}{}, + } + + handlerFunc(nil, r) + + before := r.Env["BEFORE"].(string) + if before != "ABC" { + t.Error("middleware executed in the wrong order, expected ABC") + } + + after := r.Env["AFTER"].(string) + if after != "CBA" { + t.Error("middleware executed in the wrong order, expected CBA") + } +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/powered_by.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/powered_by.go new file mode 100644 index 0000000..3b22ccf --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/powered_by.go @@ -0,0 +1,29 @@ +package rest + +const xPoweredByDefault = "go-json-rest" + +// PoweredByMiddleware adds the "X-Powered-By" header to the HTTP response. +type PoweredByMiddleware struct { + + // If specified, used as the value for the "X-Powered-By" response header. + // Defaults to "go-json-rest". + XPoweredBy string +} + +// MiddlewareFunc makes PoweredByMiddleware implement the Middleware interface. +func (mw *PoweredByMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { + + poweredBy := xPoweredByDefault + if mw.XPoweredBy != "" { + poweredBy = mw.XPoweredBy + } + + return func(w ResponseWriter, r *Request) { + + w.Header().Add("X-Powered-By", poweredBy) + + // call the handler + h(w, r) + + } +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/powered_by_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/powered_by_test.go new file mode 100644 index 0000000..9d1ca34 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/powered_by_test.go @@ -0,0 +1,30 @@ +package rest + +import ( + "github.com/ant0ine/go-json-rest/rest/test" + "testing" +) + +func TestPoweredByMiddleware(t *testing.T) { + + api := NewApi() + + // the middleware to test + api.Use(&PoweredByMiddleware{ + XPoweredBy: "test", + }) + + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + // wrap all + handler := api.MakeHandler() + + req := test.MakeSimpleRequest("GET", "http://localhost/", nil) + recorded := test.RunRequest(t, handler, req) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.HeaderIs("X-Powered-By", "test") +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/recorder.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/recorder.go new file mode 100644 index 0000000..6cebfa2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/recorder.go @@ -0,0 +1,97 @@ +package rest + +import ( + "bufio" + "net" + "net/http" +) + +// RecorderMiddleware keeps a record of the HTTP status code of the response, +// and the number of bytes written. +// The result is available to the wrapping handlers as request.Env["STATUS_CODE"].(int), +// and as request.Env["BYTES_WRITTEN"].(int64) +type RecorderMiddleware struct{} + +// MiddlewareFunc makes RecorderMiddleware implement the Middleware interface. +func (mw *RecorderMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { + return func(w ResponseWriter, r *Request) { + + writer := &recorderResponseWriter{w, 0, false, 0} + + // call the handler + h(writer, r) + + r.Env["STATUS_CODE"] = writer.statusCode + r.Env["BYTES_WRITTEN"] = writer.bytesWritten + } +} + +// Private responseWriter intantiated by the recorder middleware. +// It keeps a record of the HTTP status code of the response. +// It implements the following interfaces: +// ResponseWriter +// http.ResponseWriter +// http.Flusher +// http.CloseNotifier +// http.Hijacker +type recorderResponseWriter struct { + ResponseWriter + statusCode int + wroteHeader bool + bytesWritten int64 +} + +// Record the status code. +func (w *recorderResponseWriter) WriteHeader(code int) { + w.ResponseWriter.WriteHeader(code) + w.statusCode = code + w.wroteHeader = true +} + +// Make sure the local Write is called. +func (w *recorderResponseWriter) WriteJson(v interface{}) error { + b, err := w.EncodeJson(v) + if err != nil { + return err + } + _, err = w.Write(b) + if err != nil { + return err + } + return nil +} + +// Make sure the local WriteHeader is called, and call the parent Flush. +// Provided in order to implement the http.Flusher interface. +func (w *recorderResponseWriter) Flush() { + if !w.wroteHeader { + w.WriteHeader(http.StatusOK) + } + flusher := w.ResponseWriter.(http.Flusher) + flusher.Flush() +} + +// Call the parent CloseNotify. +// Provided in order to implement the http.CloseNotifier interface. +func (w *recorderResponseWriter) CloseNotify() <-chan bool { + notifier := w.ResponseWriter.(http.CloseNotifier) + return notifier.CloseNotify() +} + +// Provided in order to implement the http.Hijacker interface. +func (w *recorderResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hijacker := w.ResponseWriter.(http.Hijacker) + return hijacker.Hijack() +} + +// Make sure the local WriteHeader is called, and call the parent Write. +// Provided in order to implement the http.ResponseWriter interface. +func (w *recorderResponseWriter) Write(b []byte) (int, error) { + if !w.wroteHeader { + w.WriteHeader(http.StatusOK) + } + writer := w.ResponseWriter.(http.ResponseWriter) + written, err := writer.Write(b) + w.bytesWritten += int64(written) + return written, err +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/recorder_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/recorder_test.go new file mode 100644 index 0000000..c02b846 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/recorder_test.go @@ -0,0 +1,93 @@ +package rest + +import ( + "github.com/ant0ine/go-json-rest/rest/test" + "testing" +) + +func TestRecorderMiddleware(t *testing.T) { + + api := NewApi() + + // a middleware carrying the Env tests + api.Use(MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { + return func(w ResponseWriter, r *Request) { + + handler(w, r) + + if r.Env["STATUS_CODE"] == nil { + t.Error("STATUS_CODE is nil") + } + statusCode := r.Env["STATUS_CODE"].(int) + if statusCode != 200 { + t.Errorf("STATUS_CODE = 200 expected, got %d", statusCode) + } + + if r.Env["BYTES_WRITTEN"] == nil { + t.Error("BYTES_WRITTEN is nil") + } + bytesWritten := r.Env["BYTES_WRITTEN"].(int64) + // '{"Id":"123"}' => 12 chars + if bytesWritten != 12 { + t.Errorf("BYTES_WRITTEN 12 expected, got %d", bytesWritten) + } + } + })) + + // the middleware to test + api.Use(&RecorderMiddleware{}) + + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + // wrap all + handler := api.MakeHandler() + + req := test.MakeSimpleRequest("GET", "http://localhost/", nil) + recorded := test.RunRequest(t, handler, req) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() +} + +// See how many bytes are written when gzipping +func TestRecorderAndGzipMiddleware(t *testing.T) { + + api := NewApi() + + // a middleware carrying the Env tests + api.Use(MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { + return func(w ResponseWriter, r *Request) { + + handler(w, r) + + if r.Env["BYTES_WRITTEN"] == nil { + t.Error("BYTES_WRITTEN is nil") + } + bytesWritten := r.Env["BYTES_WRITTEN"].(int64) + // Yes, the gzipped version actually takes more space. + if bytesWritten != 28 { + t.Errorf("BYTES_WRITTEN 28 expected, got %d", bytesWritten) + } + } + })) + + // the middlewares to test + api.Use(&RecorderMiddleware{}) + api.Use(&GzipMiddleware{}) + + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + // wrap all + handler := api.MakeHandler() + + req := test.MakeSimpleRequest("GET", "http://localhost/", nil) + // "Accept-Encoding", "gzip" is set by test.MakeSimpleRequest + recorded := test.RunRequest(t, handler, req) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/recover.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/recover.go new file mode 100644 index 0000000..99f1515 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/recover.go @@ -0,0 +1,74 @@ +package rest + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "runtime/debug" +) + +// RecoverMiddleware catches the panic errors that occur in the wrapped HandleFunc, +// and convert them to 500 responses. +type RecoverMiddleware struct { + + // Custom logger used for logging the panic errors, + // optional, defaults to log.New(os.Stderr, "", 0) + Logger *log.Logger + + // If true, the log records will be printed as JSON. Convenient for log parsing. + EnableLogAsJson bool + + // If true, when a "panic" happens, the error string and the stack trace will be + // printed in the 500 response body. + EnableResponseStackTrace bool +} + +// MiddlewareFunc makes RecoverMiddleware implement the Middleware interface. +func (mw *RecoverMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { + + // set the default Logger + if mw.Logger == nil { + mw.Logger = log.New(os.Stderr, "", 0) + } + + return func(w ResponseWriter, r *Request) { + + // catch user code's panic, and convert to http response + defer func() { + if reco := recover(); reco != nil { + trace := debug.Stack() + + // log the trace + message := fmt.Sprintf("%s\n%s", reco, trace) + mw.logError(message) + + // write error response + if mw.EnableResponseStackTrace { + Error(w, message, http.StatusInternalServerError) + } else { + Error(w, "Internal Server Error", http.StatusInternalServerError) + } + } + }() + + // call the handler + h(w, r) + } +} + +func (mw *RecoverMiddleware) logError(message string) { + if mw.EnableLogAsJson { + record := map[string]string{ + "error": message, + } + b, err := json.Marshal(&record) + if err != nil { + panic(err) + } + mw.Logger.Printf("%s", b) + } else { + mw.Logger.Print(message) + } +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/recover_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/recover_test.go new file mode 100644 index 0000000..953ae5a --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/recover_test.go @@ -0,0 +1,43 @@ +package rest + +import ( + "github.com/ant0ine/go-json-rest/rest/test" + "io/ioutil" + "log" + "testing" +) + +func TestRecoverMiddleware(t *testing.T) { + + api := NewApi() + + // the middleware to test + api.Use(&RecoverMiddleware{ + Logger: log.New(ioutil.Discard, "", 0), + EnableLogAsJson: false, + EnableResponseStackTrace: true, + }) + + // a simple app that fails + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + panic("test") + })) + + // wrap all + handler := api.MakeHandler() + + req := test.MakeSimpleRequest("GET", "http://localhost/", nil) + recorded := test.RunRequest(t, handler, req) + recorded.CodeIs(500) + recorded.ContentTypeIsJson() + + // payload + payload := map[string]string{} + err := recorded.DecodeJsonPayload(&payload) + if err != nil { + t.Fatal(err) + } + if payload["Error"] == "" { + t.Errorf("Expected an error message, got: %v", payload) + } +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/request.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/request.go new file mode 100644 index 0000000..9d1d792 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/request.go @@ -0,0 +1,139 @@ +package rest + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "net/url" + "strings" +) + +var ( + // ErrJsonPayloadEmpty is returned when the JSON payload is empty. + ErrJsonPayloadEmpty = errors.New("JSON payload is empty") +) + +// Request inherits from http.Request, and provides additional methods. +type Request struct { + *http.Request + + // Map of parameters that have been matched in the URL Path. + PathParams map[string]string + + // Environment used by middlewares to communicate. + Env map[string]interface{} +} + +// PathParam provides a convenient access to the PathParams map. +func (r *Request) PathParam(name string) string { + return r.PathParams[name] +} + +// DecodeJsonPayload reads the request body and decodes the JSON using json.Unmarshal. +func (r *Request) DecodeJsonPayload(v interface{}) error { + content, err := ioutil.ReadAll(r.Body) + r.Body.Close() + if err != nil { + return err + } + if len(content) == 0 { + return ErrJsonPayloadEmpty + } + err = json.Unmarshal(content, v) + if err != nil { + return err + } + return nil +} + +// BaseUrl returns a new URL object with the Host and Scheme taken from the request. +// (without the trailing slash in the host) +func (r *Request) BaseUrl() *url.URL { + scheme := r.URL.Scheme + if scheme == "" { + scheme = "http" + } + + host := r.Host + if len(host) > 0 && host[len(host)-1] == '/' { + host = host[:len(host)-1] + } + + return &url.URL{ + Scheme: scheme, + Host: host, + } +} + +// UrlFor returns the URL object from UriBase with the Path set to path, and the query +// string built with queryParams. +func (r *Request) UrlFor(path string, queryParams map[string][]string) *url.URL { + baseUrl := r.BaseUrl() + baseUrl.Path = path + if queryParams != nil { + query := url.Values{} + for k, v := range queryParams { + for _, vv := range v { + query.Add(k, vv) + } + } + baseUrl.RawQuery = query.Encode() + } + return baseUrl +} + +// CorsInfo contains the CORS request info derived from a rest.Request. +type CorsInfo struct { + IsCors bool + IsPreflight bool + Origin string + OriginUrl *url.URL + + // The header value is converted to uppercase to avoid common mistakes. + AccessControlRequestMethod string + + // The header values are normalized with http.CanonicalHeaderKey. + AccessControlRequestHeaders []string +} + +// GetCorsInfo derives CorsInfo from Request. +func (r *Request) GetCorsInfo() *CorsInfo { + + origin := r.Header.Get("Origin") + + var originUrl *url.URL + var isCors bool + + if origin == "" { + isCors = false + } else if origin == "null" { + isCors = true + } else { + var err error + originUrl, err = url.ParseRequestURI(origin) + isCors = err == nil && r.Host != originUrl.Host + } + + reqMethod := r.Header.Get("Access-Control-Request-Method") + + reqHeaders := []string{} + rawReqHeaders := r.Header[http.CanonicalHeaderKey("Access-Control-Request-Headers")] + for _, rawReqHeader := range rawReqHeaders { + // net/http does not handle comma delimited headers for us + for _, reqHeader := range strings.Split(rawReqHeader, ",") { + reqHeaders = append(reqHeaders, http.CanonicalHeaderKey(strings.TrimSpace(reqHeader))) + } + } + + isPreflight := isCors && r.Method == "OPTIONS" && reqMethod != "" + + return &CorsInfo{ + IsCors: isCors, + IsPreflight: isPreflight, + Origin: origin, + OriginUrl: originUrl, + AccessControlRequestMethod: strings.ToUpper(reqMethod), + AccessControlRequestHeaders: reqHeaders, + } +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/request_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/request_test.go new file mode 100644 index 0000000..66424ac --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/request_test.go @@ -0,0 +1,150 @@ +package rest + +import ( + "io" + "net/http" + "strings" + "testing" +) + +func defaultRequest(method string, urlStr string, body io.Reader, t *testing.T) *Request { + origReq, err := http.NewRequest(method, urlStr, body) + if err != nil { + t.Fatal(err) + } + return &Request{ + origReq, + nil, + map[string]interface{}{}, + } +} + +func TestRequestEmptyJson(t *testing.T) { + req := defaultRequest("POST", "http://localhost", strings.NewReader(""), t) + err := req.DecodeJsonPayload(nil) + if err != ErrJsonPayloadEmpty { + t.Error("Expected ErrJsonPayloadEmpty") + } +} + +func TestRequestBaseUrl(t *testing.T) { + req := defaultRequest("GET", "http://localhost", nil, t) + urlBase := req.BaseUrl() + urlString := urlBase.String() + + expected := "http://localhost" + if urlString != expected { + t.Error(expected + " was the expected URL base, but instead got " + urlString) + } +} + +func TestRequestUrlScheme(t *testing.T) { + req := defaultRequest("GET", "https://localhost", nil, t) + urlBase := req.BaseUrl() + + expected := "https" + if urlBase.Scheme != expected { + t.Error(expected + " was the expected scheme, but instead got " + urlBase.Scheme) + } +} + +func TestRequestUrlFor(t *testing.T) { + req := defaultRequest("GET", "http://localhost", nil, t) + + path := "/foo/bar" + + urlObj := req.UrlFor(path, nil) + if urlObj.Path != path { + t.Error(path + " was expected to be the path, but got " + urlObj.Path) + } + + expected := "http://localhost/foo/bar" + if urlObj.String() != expected { + t.Error(expected + " was expected, but the returned URL was " + urlObj.String()) + } +} + +func TestRequestUrlForQueryString(t *testing.T) { + req := defaultRequest("GET", "http://localhost", nil, t) + + params := map[string][]string{ + "id": []string{"foo", "bar"}, + } + + urlObj := req.UrlFor("/foo/bar", params) + + expected := "http://localhost/foo/bar?id=foo&id=bar" + if urlObj.String() != expected { + t.Error(expected + " was expected, but the returned URL was " + urlObj.String()) + } +} + +func TestCorsInfoSimpleCors(t *testing.T) { + req := defaultRequest("GET", "http://localhost", nil, t) + req.Request.Header.Set("Origin", "http://another.host") + + corsInfo := req.GetCorsInfo() + if corsInfo == nil { + t.Error("Expected non nil CorsInfo") + } + if corsInfo.IsCors == false { + t.Error("This is a CORS request") + } + if corsInfo.IsPreflight == true { + t.Error("This is not a Preflight request") + } +} + +func TestCorsInfoNullOrigin(t *testing.T) { + req := defaultRequest("GET", "http://localhost", nil, t) + req.Request.Header.Set("Origin", "null") + + corsInfo := req.GetCorsInfo() + if corsInfo == nil { + t.Error("Expected non nil CorsInfo") + } + if corsInfo.IsCors == false { + t.Error("This is a CORS request") + } + if corsInfo.IsPreflight == true { + t.Error("This is not a Preflight request") + } + if corsInfo.OriginUrl != nil { + t.Error("OriginUrl cannot be set") + } +} + +func TestCorsInfoPreflightCors(t *testing.T) { + req := defaultRequest("OPTIONS", "http://localhost", nil, t) + req.Request.Header.Set("Origin", "http://another.host") + + corsInfo := req.GetCorsInfo() + if corsInfo == nil { + t.Error("Expected non nil CorsInfo") + } + if corsInfo.IsCors == false { + t.Error("This is a CORS request") + } + if corsInfo.IsPreflight == true { + t.Error("This is NOT a Preflight request") + } + + // Preflight must have the Access-Control-Request-Method header + req.Request.Header.Set("Access-Control-Request-Method", "PUT") + corsInfo = req.GetCorsInfo() + if corsInfo == nil { + t.Error("Expected non nil CorsInfo") + } + if corsInfo.IsCors == false { + t.Error("This is a CORS request") + } + if corsInfo.IsPreflight == false { + t.Error("This is a Preflight request") + } + if corsInfo.Origin != "http://another.host" { + t.Error("Origin must be identical to the header value") + } + if corsInfo.OriginUrl == nil { + t.Error("OriginUrl must be set") + } +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/response.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/response.go new file mode 100644 index 0000000..e2043ea --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/response.go @@ -0,0 +1,118 @@ +package rest + +import ( + "bufio" + "encoding/json" + "net" + "net/http" +) + +// A ResponseWriter interface dedicated to JSON HTTP response. +// Note, the responseWriter object instantiated by the framework also implements many other interfaces +// accessible by type assertion: http.ResponseWriter, http.Flusher, http.CloseNotifier, http.Hijacker. +type ResponseWriter interface { + + // Identical to the http.ResponseWriter interface + Header() http.Header + + // Use EncodeJson to generate the payload, write the headers with http.StatusOK if + // they are not already written, then write the payload. + // The Content-Type header is set to "application/json", unless already specified. + WriteJson(v interface{}) error + + // Encode the data structure to JSON, mainly used to wrap ResponseWriter in + // middlewares. + EncodeJson(v interface{}) ([]byte, error) + + // Similar to the http.ResponseWriter interface, with additional JSON related + // headers set. + WriteHeader(int) +} + +// Error produces an error response in JSON with the following structure, '{"Error":"My error message"}' +// The standard plain text net/http Error helper can still be called like this: +// http.Error(w, "error message", code) +func Error(w ResponseWriter, error string, code int) { + w.WriteHeader(code) + err := w.WriteJson(map[string]string{"Error": error}) + if err != nil { + panic(err) + } +} + +// NotFound produces a 404 response with the following JSON, '{"Error":"Resource not found"}' +// The standard plain text net/http NotFound helper can still be called like this: +// http.NotFound(w, r.Request) +func NotFound(w ResponseWriter, r *Request) { + Error(w, "Resource not found", http.StatusNotFound) +} + +// Private responseWriter intantiated by the resource handler. +// It implements the following interfaces: +// ResponseWriter +// http.ResponseWriter +// http.Flusher +// http.CloseNotifier +// http.Hijacker +type responseWriter struct { + http.ResponseWriter + wroteHeader bool +} + +func (w *responseWriter) WriteHeader(code int) { + if w.Header().Get("Content-Type") == "" { + w.Header().Set("Content-Type", "application/json") + } + w.ResponseWriter.WriteHeader(code) + w.wroteHeader = true +} + +func (w *responseWriter) EncodeJson(v interface{}) ([]byte, error) { + b, err := json.Marshal(v) + if err != nil { + return nil, err + } + return b, nil +} + +// Encode the object in JSON and call Write. +func (w *responseWriter) WriteJson(v interface{}) error { + b, err := w.EncodeJson(v) + if err != nil { + return err + } + _, err = w.Write(b) + if err != nil { + return err + } + return nil +} + +// Provided in order to implement the http.ResponseWriter interface. +func (w *responseWriter) Write(b []byte) (int, error) { + if !w.wroteHeader { + w.WriteHeader(http.StatusOK) + } + return w.ResponseWriter.Write(b) +} + +// Provided in order to implement the http.Flusher interface. +func (w *responseWriter) Flush() { + if !w.wroteHeader { + w.WriteHeader(http.StatusOK) + } + flusher := w.ResponseWriter.(http.Flusher) + flusher.Flush() +} + +// Provided in order to implement the http.CloseNotifier interface. +func (w *responseWriter) CloseNotify() <-chan bool { + notifier := w.ResponseWriter.(http.CloseNotifier) + return notifier.CloseNotify() +} + +// Provided in order to implement the http.Hijacker interface. +func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hijacker := w.ResponseWriter.(http.Hijacker) + return hijacker.Hijack() +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/response_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/response_test.go new file mode 100644 index 0000000..6de26c8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/response_test.go @@ -0,0 +1,23 @@ +package rest + +import ( + "testing" +) + +func TestResponseNotIndent(t *testing.T) { + + writer := responseWriter{ + nil, + false, + } + + got, err := writer.EncodeJson(map[string]bool{"test": true}) + if err != nil { + t.Error(err.Error()) + } + gotStr := string(got) + expected := "{\"test\":true}" + if gotStr != expected { + t.Error(expected + " was the expected, but instead got " + gotStr) + } +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/route.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/route.go new file mode 100644 index 0000000..efb94a7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/route.go @@ -0,0 +1,107 @@ +package rest + +import ( + "strings" +) + +// Route defines a route as consumed by the router. It can be instantiated directly, or using one +// of the shortcut methods: rest.Get, rest.Post, rest.Put, rest.Patch and rest.Delete. +type Route struct { + + // Any HTTP method. It will be used as uppercase to avoid common mistakes. + HttpMethod string + + // A string like "/resource/:id.json". + // Placeholders supported are: + // :paramName that matches any char to the first '/' or '.' + // #paramName that matches any char to the first '/' + // *paramName that matches everything to the end of the string + // (placeholder names must be unique per PathExp) + PathExp string + + // Code that will be executed when this route is taken. + Func HandlerFunc +} + +// MakePath generates the path corresponding to this Route and the provided path parameters. +// This is used for reverse route resolution. +func (route *Route) MakePath(pathParams map[string]string) string { + path := route.PathExp + for paramName, paramValue := range pathParams { + paramPlaceholder := ":" + paramName + relaxedPlaceholder := "#" + paramName + splatPlaceholder := "*" + paramName + r := strings.NewReplacer(paramPlaceholder, paramValue, splatPlaceholder, paramValue, relaxedPlaceholder, paramValue) + path = r.Replace(path) + } + return path +} + +// Head is a shortcut method that instantiates a HEAD route. See the Route object the parameters definitions. +// Equivalent to &Route{"HEAD", pathExp, handlerFunc} +func Head(pathExp string, handlerFunc HandlerFunc) *Route { + return &Route{ + HttpMethod: "HEAD", + PathExp: pathExp, + Func: handlerFunc, + } +} + +// Get is a shortcut method that instantiates a GET route. See the Route object the parameters definitions. +// Equivalent to &Route{"GET", pathExp, handlerFunc} +func Get(pathExp string, handlerFunc HandlerFunc) *Route { + return &Route{ + HttpMethod: "GET", + PathExp: pathExp, + Func: handlerFunc, + } +} + +// Post is a shortcut method that instantiates a POST route. See the Route object the parameters definitions. +// Equivalent to &Route{"POST", pathExp, handlerFunc} +func Post(pathExp string, handlerFunc HandlerFunc) *Route { + return &Route{ + HttpMethod: "POST", + PathExp: pathExp, + Func: handlerFunc, + } +} + +// Put is a shortcut method that instantiates a PUT route. See the Route object the parameters definitions. +// Equivalent to &Route{"PUT", pathExp, handlerFunc} +func Put(pathExp string, handlerFunc HandlerFunc) *Route { + return &Route{ + HttpMethod: "PUT", + PathExp: pathExp, + Func: handlerFunc, + } +} + +// Patch is a shortcut method that instantiates a PATCH route. See the Route object the parameters definitions. +// Equivalent to &Route{"PATCH", pathExp, handlerFunc} +func Patch(pathExp string, handlerFunc HandlerFunc) *Route { + return &Route{ + HttpMethod: "PATCH", + PathExp: pathExp, + Func: handlerFunc, + } +} + +// Delete is a shortcut method that instantiates a DELETE route. Equivalent to &Route{"DELETE", pathExp, handlerFunc} +func Delete(pathExp string, handlerFunc HandlerFunc) *Route { + return &Route{ + HttpMethod: "DELETE", + PathExp: pathExp, + Func: handlerFunc, + } +} + +// Options is a shortcut method that instantiates an OPTIONS route. See the Route object the parameters definitions. +// Equivalent to &Route{"OPTIONS", pathExp, handlerFunc} +func Options(pathExp string, handlerFunc HandlerFunc) *Route { + return &Route{ + HttpMethod: "OPTIONS", + PathExp: pathExp, + Func: handlerFunc, + } +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/route_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/route_test.go new file mode 100644 index 0000000..5ea63b8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/route_test.go @@ -0,0 +1,88 @@ +package rest + +import ( + "testing" +) + +func TestReverseRouteResolution(t *testing.T) { + + noParam := &Route{"GET", "/", nil} + got := noParam.MakePath(nil) + expected := "/" + if got != expected { + t.Errorf("expected %s, got %s", expected, got) + } + + twoParams := &Route{"GET", "/:id.:format", nil} + got = twoParams.MakePath( + map[string]string{ + "id": "123", + "format": "json", + }, + ) + expected = "/123.json" + if got != expected { + t.Errorf("expected %s, got %s", expected, got) + } + + splatParam := &Route{"GET", "/:id.*format", nil} + got = splatParam.MakePath( + map[string]string{ + "id": "123", + "format": "tar.gz", + }, + ) + expected = "/123.tar.gz" + if got != expected { + t.Errorf("expected %s, got %s", expected, got) + } + + relaxedParam := &Route{"GET", "/#file", nil} + got = relaxedParam.MakePath( + map[string]string{ + "file": "a.txt", + }, + ) + expected = "/a.txt" + if got != expected { + t.Errorf("expected %s, got %s", expected, got) + } +} + +func TestShortcutMethods(t *testing.T) { + + r := Head("/", nil) + if r.HttpMethod != "HEAD" { + t.Errorf("expected HEAD, got %s", r.HttpMethod) + } + + r = Get("/", nil) + if r.HttpMethod != "GET" { + t.Errorf("expected GET, got %s", r.HttpMethod) + } + + r = Post("/", nil) + if r.HttpMethod != "POST" { + t.Errorf("expected POST, got %s", r.HttpMethod) + } + + r = Put("/", nil) + if r.HttpMethod != "PUT" { + t.Errorf("expected PUT, got %s", r.HttpMethod) + } + + r = Patch("/", nil) + if r.HttpMethod != "PATCH" { + t.Errorf("expected PATCH, got %s", r.HttpMethod) + } + + r = Delete("/", nil) + if r.HttpMethod != "DELETE" { + t.Errorf("expected DELETE, got %s", r.HttpMethod) + } + + r = Options("/", nil) + if r.HttpMethod != "OPTIONS" { + t.Errorf("expected OPTIONS, got %s", r.HttpMethod) + } +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/router.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/router.go new file mode 100644 index 0000000..f7ab713 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/router.go @@ -0,0 +1,194 @@ +package rest + +import ( + "errors" + "github.com/ant0ine/go-json-rest/rest/trie" + "net/http" + "net/url" + "strings" +) + +type router struct { + Routes []*Route + + disableTrieCompression bool + index map[*Route]int + trie *trie.Trie +} + +// MakeRouter returns the router app. Given a set of Routes, it dispatches the request to the +// HandlerFunc of the first route that matches. The order of the Routes matters. +func MakeRouter(routes ...*Route) (App, error) { + r := &router{ + Routes: routes, + } + err := r.start() + if err != nil { + return nil, err + } + return r, nil +} + +// Handle the REST routing and run the user code. +func (rt *router) AppFunc() HandlerFunc { + return func(writer ResponseWriter, request *Request) { + + // find the route + route, params, pathMatched := rt.findRouteFromURL(request.Method, request.URL) + if route == nil { + + if pathMatched { + // no route found, but path was matched: 405 Method Not Allowed + Error(writer, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // no route found, the path was not matched: 404 Not Found + NotFound(writer, request) + return + } + + // a route was found, set the PathParams + request.PathParams = params + + // run the user code + handler := route.Func + handler(writer, request) + } +} + +// This is run for each new request, perf is important. +func escapedPath(urlObj *url.URL) string { + // the escape method of url.URL should be public + // that would avoid this split. + parts := strings.SplitN(urlObj.RequestURI(), "?", 2) + return parts[0] +} + +var preEscape = strings.NewReplacer("*", "__SPLAT_PLACEHOLDER__", "#", "__RELAXED_PLACEHOLDER__") + +var postEscape = strings.NewReplacer("__SPLAT_PLACEHOLDER__", "*", "__RELAXED_PLACEHOLDER__", "#") + +// This is run at init time only. +func escapedPathExp(pathExp string) (string, error) { + + // PathExp validation + if pathExp == "" { + return "", errors.New("empty PathExp") + } + if pathExp[0] != '/' { + return "", errors.New("PathExp must start with /") + } + if strings.Contains(pathExp, "?") { + return "", errors.New("PathExp must not contain the query string") + } + + // Get the right escaping + // XXX a bit hacky + + pathExp = preEscape.Replace(pathExp) + + urlObj, err := url.Parse(pathExp) + if err != nil { + return "", err + } + + // get the same escaping as find requests + pathExp = urlObj.RequestURI() + + pathExp = postEscape.Replace(pathExp) + + return pathExp, nil +} + +// This validates the Routes and prepares the Trie data structure. +// It must be called once the Routes are defined and before trying to find Routes. +// The order matters, if multiple Routes match, the first defined will be used. +func (rt *router) start() error { + + rt.trie = trie.New() + rt.index = map[*Route]int{} + + for i, route := range rt.Routes { + + // work with the PathExp urlencoded. + pathExp, err := escapedPathExp(route.PathExp) + if err != nil { + return err + } + + // insert in the Trie + err = rt.trie.AddRoute( + strings.ToUpper(route.HttpMethod), // work with the HttpMethod in uppercase + pathExp, + route, + ) + if err != nil { + return err + } + + // index + rt.index[route] = i + } + + if rt.disableTrieCompression == false { + rt.trie.Compress() + } + + return nil +} + +// return the result that has the route defined the earliest +func (rt *router) ofFirstDefinedRoute(matches []*trie.Match) *trie.Match { + minIndex := -1 + var bestMatch *trie.Match + + for _, result := range matches { + route := result.Route.(*Route) + routeIndex := rt.index[route] + if minIndex == -1 || routeIndex < minIndex { + minIndex = routeIndex + bestMatch = result + } + } + + return bestMatch +} + +// Return the first matching Route and the corresponding parameters for a given URL object. +func (rt *router) findRouteFromURL(httpMethod string, urlObj *url.URL) (*Route, map[string]string, bool) { + + // lookup the routes in the Trie + matches, pathMatched := rt.trie.FindRoutesAndPathMatched( + strings.ToUpper(httpMethod), // work with the httpMethod in uppercase + escapedPath(urlObj), // work with the path urlencoded + ) + + // short cuts + if len(matches) == 0 { + // no route found + return nil, nil, pathMatched + } + + if len(matches) == 1 { + // one route found + return matches[0].Route.(*Route), matches[0].Params, pathMatched + } + + // multiple routes found, pick the first defined + result := rt.ofFirstDefinedRoute(matches) + return result.Route.(*Route), result.Params, pathMatched +} + +// Parse the url string (complete or just the path) and return the first matching Route and the corresponding parameters. +func (rt *router) findRoute(httpMethod, urlStr string) (*Route, map[string]string, bool, error) { + + // parse the url + urlObj, err := url.Parse(urlStr) + if err != nil { + return nil, nil, false, err + } + + route, params, pathMatched := rt.findRouteFromURL(httpMethod, urlObj) + return route, params, pathMatched, nil +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/router_benchmark_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/router_benchmark_test.go new file mode 100644 index 0000000..59add29 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/router_benchmark_test.go @@ -0,0 +1,143 @@ +package rest + +import ( + "fmt" + "net/url" + "regexp" + "testing" +) + +func routes() []*Route { + // simulate the routes of a real but reasonable app. + // 6 + 10 * (5 + 2) + 1 = 77 routes + routePaths := []string{ + "/", + "/signin", + "/signout", + "/profile", + "/settings", + "/upload/*file", + } + for i := 0; i < 10; i++ { + for j := 0; j < 5; j++ { + routePaths = append(routePaths, fmt.Sprintf("/resource%d/:id/property%d", i, j)) + } + routePaths = append(routePaths, fmt.Sprintf("/resource%d/:id", i)) + routePaths = append(routePaths, fmt.Sprintf("/resource%d", i)) + } + routePaths = append(routePaths, "/*") + + routes := []*Route{} + for _, path := range routePaths { + routes = append(routes, &Route{ + HttpMethod: "GET", + PathExp: path, + }) + } + return routes +} + +func requestUrls() []*url.URL { + // simulate a few requests + urlStrs := []string{ + "http://example.org/", + "http://example.org/resource9/123", + "http://example.org/resource9/123/property1", + "http://example.org/doesnotexist", + } + urlObjs := []*url.URL{} + for _, urlStr := range urlStrs { + urlObj, _ := url.Parse(urlStr) + urlObjs = append(urlObjs, urlObj) + } + return urlObjs +} + +func BenchmarkNoCompression(b *testing.B) { + + b.StopTimer() + + r := router{ + Routes: routes(), + disableTrieCompression: true, + } + r.start() + urlObjs := requestUrls() + + b.StartTimer() + + for i := 0; i < b.N; i++ { + for _, urlObj := range urlObjs { + r.findRouteFromURL("GET", urlObj) + } + } +} + +func BenchmarkCompression(b *testing.B) { + + b.StopTimer() + + r := router{ + Routes: routes(), + } + r.start() + urlObjs := requestUrls() + + b.StartTimer() + + for i := 0; i < b.N; i++ { + for _, urlObj := range urlObjs { + r.findRouteFromURL("GET", urlObj) + } + } +} + +func BenchmarkRegExpLoop(b *testing.B) { + // reference benchmark using the usual RegExps + Loop strategy + + b.StopTimer() + + routes := routes() + urlObjs := requestUrls() + + // build the route regexps + r1, err := regexp.Compile(":[^/\\.]*") + if err != nil { + panic(err) + } + r2, err := regexp.Compile("\\*.*") + if err != nil { + panic(err) + } + routeRegexps := []*regexp.Regexp{} + for _, route := range routes { + + // generate the regexp string + regStr := r2.ReplaceAllString(route.PathExp, "([^/\\.]+)") + regStr = r1.ReplaceAllString(regStr, "(.+)") + regStr = "^" + regStr + "$" + + // compile it + reg, err := regexp.Compile(regStr) + if err != nil { + panic(err) + } + + routeRegexps = append(routeRegexps, reg) + } + + b.StartTimer() + + for i := 0; i < b.N; i++ { + // do it for a few urls + for _, urlObj := range urlObjs { + // stop at the first route that matches + for index, reg := range routeRegexps { + if reg.FindAllString(urlObj.Path, 1) != nil { + _ = routes[index] + break + } + } + } + } +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/router_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/router_test.go new file mode 100644 index 0000000..ad732f4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/router_test.go @@ -0,0 +1,392 @@ +package rest + +import ( + "net/url" + "strings" + "testing" +) + +func TestFindRouteAPI(t *testing.T) { + + r := router{ + Routes: []*Route{ + &Route{ + HttpMethod: "GET", + PathExp: "/", + }, + }, + } + + err := r.start() + if err != nil { + t.Fatal(err) + } + + // full url string + input := "http://example.org/" + route, params, pathMatched, err := r.findRoute("GET", input) + if err != nil { + t.Fatal(err) + } + if route.PathExp != "/" { + t.Error("Expected PathExp to be /") + } + if len(params) != 0 { + t.Error("Expected 0 param") + } + if pathMatched != true { + t.Error("Expected pathMatched to be true") + } + + // part of the url string + input = "/" + route, params, pathMatched, err = r.findRoute("GET", input) + if err != nil { + t.Fatal(err) + } + if route.PathExp != "/" { + t.Error("Expected PathExp to be /") + } + if len(params) != 0 { + t.Error("Expected 0 param") + } + if pathMatched != true { + t.Error("Expected pathMatched to be true") + } + + // url object + urlObj, err := url.Parse("http://example.org/") + if err != nil { + t.Fatal(err) + } + route, params, pathMatched = r.findRouteFromURL("GET", urlObj) + if route.PathExp != "/" { + t.Error("Expected PathExp to be /") + } + if len(params) != 0 { + t.Error("Expected 0 param") + } + if pathMatched != true { + t.Error("Expected pathMatched to be true") + } +} + +func TestNoRoute(t *testing.T) { + + r := router{ + Routes: []*Route{}, + } + + err := r.start() + if err != nil { + t.Fatal(err) + } + + input := "http://example.org/notfound" + route, params, pathMatched, err := r.findRoute("GET", input) + if err != nil { + t.Fatal(err) + } + + if route != nil { + t.Error("should not be able to find a route") + } + if params != nil { + t.Error("params must be nil too") + } + if pathMatched != false { + t.Error("Expected pathMatched to be false") + } +} + +func TestEmptyPathExp(t *testing.T) { + + r := router{ + Routes: []*Route{ + &Route{ + HttpMethod: "GET", + PathExp: "", + }, + }, + } + + err := r.start() + if err == nil || !strings.Contains(err.Error(), "empty") { + t.Error("expected the empty PathExp error") + } +} + +func TestInvalidPathExp(t *testing.T) { + + r := router{ + Routes: []*Route{ + &Route{ + HttpMethod: "GET", + PathExp: "invalid", + }, + }, + } + + err := r.start() + if err == nil || !strings.Contains(err.Error(), "/") { + t.Error("expected the / PathExp error") + } +} + +func TestUrlEncodedFind(t *testing.T) { + + r := router{ + Routes: []*Route{ + &Route{ + HttpMethod: "GET", + PathExp: "/with space", // not urlencoded + }, + }, + } + + err := r.start() + if err != nil { + t.Fatal(err) + } + + input := "http://example.org/with%20space" // urlencoded + route, _, pathMatched, err := r.findRoute("GET", input) + if err != nil { + t.Fatal(err) + } + if route.PathExp != "/with space" { + t.Error("Expected PathExp to be /with space") + } + if pathMatched != true { + t.Error("Expected pathMatched to be true") + } +} + +func TestWithQueryString(t *testing.T) { + + r := router{ + Routes: []*Route{ + &Route{ + HttpMethod: "GET", + PathExp: "/r/:id", + }, + }, + } + + err := r.start() + if err != nil { + t.Fatal(err) + } + + input := "http://example.org/r/123?arg=value" + route, params, pathMatched, err := r.findRoute("GET", input) + if err != nil { + t.Fatal(err) + } + if route == nil { + t.Fatal("Expected a match") + } + if params["id"] != "123" { + t.Errorf("expected 123, got %s", params["id"]) + } + if pathMatched != true { + t.Error("Expected pathMatched to be true") + } +} + +func TestNonUrlEncodedFind(t *testing.T) { + + r := router{ + Routes: []*Route{ + &Route{ + HttpMethod: "GET", + PathExp: "/with%20space", // urlencoded + }, + }, + } + + err := r.start() + if err != nil { + t.Fatal(err) + } + + input := "http://example.org/with space" // not urlencoded + route, _, pathMatched, err := r.findRoute("GET", input) + if err != nil { + t.Fatal(err) + } + if route.PathExp != "/with%20space" { + t.Errorf("Expected PathExp to be %s", "/with20space") + } + if pathMatched != true { + t.Error("Expected pathMatched to be true") + } +} + +func TestDuplicatedRoute(t *testing.T) { + + r := router{ + Routes: []*Route{ + &Route{ + HttpMethod: "GET", + PathExp: "/", + }, + &Route{ + HttpMethod: "GET", + PathExp: "/", + }, + }, + } + + err := r.start() + if err == nil { + t.Error("expected the duplicated route error") + } +} + +func TestSplatUrlEncoded(t *testing.T) { + + r := router{ + Routes: []*Route{ + &Route{ + HttpMethod: "GET", + PathExp: "/r/*rest", + }, + }, + } + + err := r.start() + if err != nil { + t.Fatal(err) + } + + input := "http://example.org/r/123" + route, params, pathMatched, err := r.findRoute("GET", input) + if err != nil { + t.Fatal(err) + } + if route == nil { + t.Fatal("Expected a match") + } + if params["rest"] != "123" { + t.Error("Expected rest to be 123") + } + if pathMatched != true { + t.Error("Expected pathMatched to be true") + } +} + +func TestRouteOrder(t *testing.T) { + + r := router{ + Routes: []*Route{ + &Route{ + HttpMethod: "GET", + PathExp: "/r/:id", + }, + &Route{ + HttpMethod: "GET", + PathExp: "/r/*rest", + }, + }, + } + + err := r.start() + if err != nil { + t.Fatal(err) + } + + input := "http://example.org/r/123" + route, params, pathMatched, err := r.findRoute("GET", input) + if err != nil { + t.Fatal(err) + } + if route == nil { + t.Fatal("Expected one route to be matched") + } + if route.PathExp != "/r/:id" { + t.Errorf("both match, expected the first defined, got %s", route.PathExp) + } + if params["id"] != "123" { + t.Error("Expected id to be 123") + } + if pathMatched != true { + t.Error("Expected pathMatched to be true") + } +} + +func TestRelaxedPlaceholder(t *testing.T) { + + r := router{ + Routes: []*Route{ + &Route{ + HttpMethod: "GET", + PathExp: "/r/:id", + }, + &Route{ + HttpMethod: "GET", + PathExp: "/r/#filename", + }, + }, + } + + err := r.start() + if err != nil { + t.Fatal(err) + } + + input := "http://example.org/r/a.txt" + route, params, pathMatched, err := r.findRoute("GET", input) + if err != nil { + t.Fatal(err) + } + if route == nil { + t.Fatal("Expected one route to be matched") + } + if route.PathExp != "/r/#filename" { + t.Errorf("expected the second route, got %s", route.PathExp) + } + if params["filename"] != "a.txt" { + t.Error("Expected filename to be a.txt") + } + if pathMatched != true { + t.Error("Expected pathMatched to be true") + } +} + +func TestSimpleExample(t *testing.T) { + + r := router{ + Routes: []*Route{ + &Route{ + HttpMethod: "GET", + PathExp: "/resources/:id", + }, + &Route{ + HttpMethod: "GET", + PathExp: "/resources", + }, + }, + } + + err := r.start() + if err != nil { + t.Fatal(err) + } + + input := "http://example.org/resources/123" + route, params, pathMatched, err := r.findRoute("GET", input) + if err != nil { + t.Fatal(err) + } + + if route.PathExp != "/resources/:id" { + t.Error("Expected PathExp to be /resources/:id") + } + if params["id"] != "123" { + t.Error("Expected id to be 123") + } + if pathMatched != true { + t.Error("Expected pathMatched to be true") + } +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/status.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/status.go new file mode 100644 index 0000000..6b6b5d1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/status.go @@ -0,0 +1,129 @@ +package rest + +import ( + "fmt" + "log" + "os" + "sync" + "time" +) + +// StatusMiddleware keeps track of various stats about the processed requests. +// It depends on request.Env["STATUS_CODE"] and request.Env["ELAPSED_TIME"], +// recorderMiddleware and timerMiddleware must be in the wrapped middlewares. +type StatusMiddleware struct { + lock sync.RWMutex + start time.Time + pid int + responseCounts map[string]int + totalResponseTime time.Time +} + +// MiddlewareFunc makes StatusMiddleware implement the Middleware interface. +func (mw *StatusMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { + + mw.start = time.Now() + mw.pid = os.Getpid() + mw.responseCounts = map[string]int{} + mw.totalResponseTime = time.Time{} + + return func(w ResponseWriter, r *Request) { + + // call the handler + h(w, r) + + if r.Env["STATUS_CODE"] == nil { + log.Fatal("StatusMiddleware: Env[\"STATUS_CODE\"] is nil, " + + "RecorderMiddleware may not be in the wrapped Middlewares.") + } + statusCode := r.Env["STATUS_CODE"].(int) + + if r.Env["ELAPSED_TIME"] == nil { + log.Fatal("StatusMiddleware: Env[\"ELAPSED_TIME\"] is nil, " + + "TimerMiddleware may not be in the wrapped Middlewares.") + } + responseTime := r.Env["ELAPSED_TIME"].(*time.Duration) + + mw.lock.Lock() + mw.responseCounts[fmt.Sprintf("%d", statusCode)]++ + mw.totalResponseTime = mw.totalResponseTime.Add(*responseTime) + mw.lock.Unlock() + } +} + +// Status contains stats and status information. It is returned by GetStatus. +// These information can be made available as an API endpoint, see the "status" +// example to install the following status route. +// GET /.status returns something like: +// +// { +// "Pid": 21732, +// "UpTime": "1m15.926272s", +// "UpTimeSec": 75.926272, +// "Time": "2013-03-04 08:00:27.152986 +0000 UTC", +// "TimeUnix": 1362384027, +// "StatusCodeCount": { +// "200": 53, +// "404": 11 +// }, +// "TotalCount": 64, +// "TotalResponseTime": "16.777ms", +// "TotalResponseTimeSec": 0.016777, +// "AverageResponseTime": "262.14us", +// "AverageResponseTimeSec": 0.00026214 +// } +type Status struct { + Pid int + UpTime string + UpTimeSec float64 + Time string + TimeUnix int64 + StatusCodeCount map[string]int + TotalCount int + TotalResponseTime string + TotalResponseTimeSec float64 + AverageResponseTime string + AverageResponseTimeSec float64 +} + +// GetStatus computes and returns a Status object based on the request informations accumulated +// since the start of the process. +func (mw *StatusMiddleware) GetStatus() *Status { + + mw.lock.RLock() + + now := time.Now() + + uptime := now.Sub(mw.start) + + totalCount := 0 + for _, count := range mw.responseCounts { + totalCount += count + } + + totalResponseTime := mw.totalResponseTime.Sub(time.Time{}) + + averageResponseTime := time.Duration(0) + if totalCount > 0 { + avgNs := int64(totalResponseTime) / int64(totalCount) + averageResponseTime = time.Duration(avgNs) + } + + status := &Status{ + Pid: mw.pid, + UpTime: uptime.String(), + UpTimeSec: uptime.Seconds(), + Time: now.String(), + TimeUnix: now.Unix(), + StatusCodeCount: mw.responseCounts, + TotalCount: totalCount, + TotalResponseTime: totalResponseTime.String(), + TotalResponseTimeSec: totalResponseTime.Seconds(), + AverageResponseTime: averageResponseTime.String(), + AverageResponseTimeSec: averageResponseTime.Seconds(), + } + + mw.lock.RUnlock() + + return status +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/status_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/status_test.go new file mode 100644 index 0000000..c2b93c4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/status_test.go @@ -0,0 +1,54 @@ +package rest + +import ( + "github.com/ant0ine/go-json-rest/rest/test" + "testing" +) + +func TestStatusMiddleware(t *testing.T) { + + api := NewApi() + + // the middlewares + status := &StatusMiddleware{} + api.Use(status) + api.Use(&TimerMiddleware{}) + api.Use(&RecorderMiddleware{}) + + // an app that return the Status + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(status.GetStatus()) + })) + + // wrap all + handler := api.MakeHandler() + + // one request + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/1", nil)) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + + // another request + recorded = test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/2", nil)) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + + // payload + payload := map[string]interface{}{} + err := recorded.DecodeJsonPayload(&payload) + if err != nil { + t.Fatal(err) + } + + if payload["Pid"] == nil { + t.Error("Expected a non nil Pid") + } + + if payload["TotalCount"].(float64) != 1 { + t.Errorf("TotalCount 1 Expected, got: %f", payload["TotalCount"].(float64)) + } + + if payload["StatusCodeCount"].(map[string]interface{})["200"].(float64) != 1 { + t.Errorf("StatusCodeCount 200 1 Expected, got: %f", payload["StatusCodeCount"].(map[string]interface{})["200"].(float64)) + } +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/test/doc.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/test/doc.go new file mode 100644 index 0000000..f58d50e --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/test/doc.go @@ -0,0 +1,33 @@ +// Utility functions to help writing tests for a Go-Json-Rest app +// +// Go comes with net/http/httptest to help writing test for an http +// server. When this http server implements a JSON REST API, some basic +// checks end up to be always the same. This test package tries to save +// some typing by providing helpers for this particular use case. +// +// package main +// +// import ( +// "github.com/ant0ine/go-json-rest/rest" +// "github.com/ant0ine/go-json-rest/rest/test" +// "testing" +// ) +// +// func TestSimpleRequest(t *testing.T) { +// api := rest.NewApi() +// api.Use(rest.DefaultDevStack...) +// router, err := rest.MakeRouter( +// rest.Get("/r", func(w rest.ResponseWriter, r *rest.Request) { +// w.WriteJson(map[string]string{"Id": "123"}) +// }), +// ) +// if err != nil { +// log.Fatal(err) +// } +// api.SetApp(router) +// recorded := test.RunRequest(t, api.MakeHandler(), +// test.MakeSimpleRequest("GET", "http://1.2.3.4/r", nil)) +// recorded.CodeIs(200) +// recorded.ContentTypeIsJson() +// } +package test diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/test/util.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/test/util.go new file mode 100644 index 0000000..54fdda4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/test/util.go @@ -0,0 +1,119 @@ +package test + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +// MakeSimpleRequest returns a http.Request. The returned request object can be +// further prepared by adding headers and query string parmaters, for instance. +func MakeSimpleRequest(method string, urlStr string, payload interface{}) *http.Request { + var s string + + if payload != nil { + b, err := json.Marshal(payload) + if err != nil { + panic(err) + } + s = fmt.Sprintf("%s", b) + } + + r, err := http.NewRequest(method, urlStr, strings.NewReader(s)) + if err != nil { + panic(err) + } + r.Header.Set("Accept-Encoding", "gzip") + if payload != nil { + r.Header.Set("Content-Type", "application/json") + } + + return r +} + +// CodeIs compares the rescorded status code +func CodeIs(t *testing.T, r *httptest.ResponseRecorder, expectedCode int) { + if r.Code != expectedCode { + t.Errorf("Code %d expected, got: %d", expectedCode, r.Code) + } +} + +// HeaderIs tests the first value for the given headerKey +func HeaderIs(t *testing.T, r *httptest.ResponseRecorder, headerKey, expectedValue string) { + value := r.HeaderMap.Get(headerKey) + if value != expectedValue { + t.Errorf( + "%s: %s expected, got: %s", + headerKey, + expectedValue, + value, + ) + } +} + +func ContentTypeIsJson(t *testing.T, r *httptest.ResponseRecorder) { + HeaderIs(t, r, "Content-Type", "application/json") +} + +func ContentEncodingIsGzip(t *testing.T, r *httptest.ResponseRecorder) { + HeaderIs(t, r, "Content-Encoding", "gzip") +} + +func BodyIs(t *testing.T, r *httptest.ResponseRecorder, expectedBody string) { + body := r.Body.String() + if body != expectedBody { + t.Errorf("Body '%s' expected, got: '%s'", expectedBody, body) + } +} + +func DecodeJsonPayload(r *httptest.ResponseRecorder, v interface{}) error { + content, err := ioutil.ReadAll(r.Body) + if err != nil { + return err + } + err = json.Unmarshal(content, v) + if err != nil { + return err + } + return nil +} + +type Recorded struct { + T *testing.T + Recorder *httptest.ResponseRecorder +} + +// RunRequest runs a HTTP request through the given handler +func RunRequest(t *testing.T, handler http.Handler, request *http.Request) *Recorded { + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, request) + return &Recorded{t, recorder} +} + +func (rd *Recorded) CodeIs(expectedCode int) { + CodeIs(rd.T, rd.Recorder, expectedCode) +} + +func (rd *Recorded) HeaderIs(headerKey, expectedValue string) { + HeaderIs(rd.T, rd.Recorder, headerKey, expectedValue) +} + +func (rd *Recorded) ContentTypeIsJson() { + rd.HeaderIs("Content-Type", "application/json") +} + +func (rd *Recorded) ContentEncodingIsGzip() { + rd.HeaderIs("Content-Encoding", "gzip") +} + +func (rd *Recorded) BodyIs(expectedBody string) { + BodyIs(rd.T, rd.Recorder, expectedBody) +} + +func (rd *Recorded) DecodeJsonPayload(v interface{}) error { + return DecodeJsonPayload(rd.Recorder, v) +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/timer.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/timer.go new file mode 100644 index 0000000..b2616c8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/timer.go @@ -0,0 +1,26 @@ +package rest + +import ( + "time" +) + +// TimerMiddleware computes the elapsed time spent during the execution of the wrapped handler. +// The result is available to the wrapping handlers as request.Env["ELAPSED_TIME"].(*time.Duration), +// and as request.Env["START_TIME"].(*time.Time) +type TimerMiddleware struct{} + +// MiddlewareFunc makes TimerMiddleware implement the Middleware interface. +func (mw *TimerMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { + return func(w ResponseWriter, r *Request) { + + start := time.Now() + r.Env["START_TIME"] = &start + + // call the handler + h(w, r) + + end := time.Now() + elapsed := end.Sub(start) + r.Env["ELAPSED_TIME"] = &elapsed + } +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/timer_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/timer_test.go new file mode 100644 index 0000000..b790fd5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/timer_test.go @@ -0,0 +1,58 @@ +package rest + +import ( + "github.com/ant0ine/go-json-rest/rest/test" + "testing" + "time" +) + +func TestTimerMiddleware(t *testing.T) { + + api := NewApi() + + // a middleware carrying the Env tests + api.Use(MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { + return func(w ResponseWriter, r *Request) { + + handler(w, r) + + if r.Env["ELAPSED_TIME"] == nil { + t.Error("ELAPSED_TIME is nil") + } + elapsedTime := r.Env["ELAPSED_TIME"].(*time.Duration) + if elapsedTime.Nanoseconds() <= 0 { + t.Errorf( + "ELAPSED_TIME is expected to be at least 1 nanosecond %d", + elapsedTime.Nanoseconds(), + ) + } + + if r.Env["START_TIME"] == nil { + t.Error("START_TIME is nil") + } + start := r.Env["START_TIME"].(*time.Time) + if start.After(time.Now()) { + t.Errorf( + "START_TIME is expected to be in the past %s", + start.String(), + ) + } + } + })) + + // the middleware to test + api.Use(&TimerMiddleware{}) + + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + // wrap all + handler := api.MakeHandler() + + req := test.MakeSimpleRequest("GET", "http://localhost/", nil) + recorded := test.RunRequest(t, handler, req) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/trie/impl.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/trie/impl.go new file mode 100644 index 0000000..2a9fff1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/trie/impl.go @@ -0,0 +1,426 @@ +// Special Trie implementation for HTTP routing. +// +// This Trie implementation is designed to support strings that includes +// :param and *splat parameters. Strings that are commonly used to represent +// the Path in HTTP routing. This implementation also maintain for each Path +// a map of HTTP Methods associated with the Route. +// +// You probably don't need to use this package directly. +// +package trie + +import ( + "errors" + "fmt" +) + +func splitParam(remaining string) (string, string) { + i := 0 + for len(remaining) > i && remaining[i] != '/' && remaining[i] != '.' { + i++ + } + return remaining[:i], remaining[i:] +} + +func splitRelaxed(remaining string) (string, string) { + i := 0 + for len(remaining) > i && remaining[i] != '/' { + i++ + } + return remaining[:i], remaining[i:] +} + +type node struct { + HttpMethodToRoute map[string]interface{} + + Children map[string]*node + ChildrenKeyLen int + + ParamChild *node + ParamName string + + RelaxedChild *node + RelaxedName string + + SplatChild *node + SplatName string +} + +func (n *node) addRoute(httpMethod, pathExp string, route interface{}, usedParams []string) error { + + if len(pathExp) == 0 { + // end of the path, leaf node, update the map + if n.HttpMethodToRoute == nil { + n.HttpMethodToRoute = map[string]interface{}{ + httpMethod: route, + } + return nil + } else { + if n.HttpMethodToRoute[httpMethod] != nil { + return errors.New("node.Route already set, duplicated path and method") + } + n.HttpMethodToRoute[httpMethod] = route + return nil + } + } + + token := pathExp[0:1] + remaining := pathExp[1:] + var nextNode *node + + if token[0] == ':' { + // :param case + var name string + name, remaining = splitParam(remaining) + + // Check param name is unique + for _, e := range usedParams { + if e == name { + return errors.New( + fmt.Sprintf("A route can't have two placeholders with the same name: %s", name), + ) + } + } + usedParams = append(usedParams, name) + + if n.ParamChild == nil { + n.ParamChild = &node{} + n.ParamName = name + } else { + if n.ParamName != name { + return errors.New( + fmt.Sprintf( + "Routes sharing a common placeholder MUST name it consistently: %s != %s", + n.ParamName, + name, + ), + ) + } + } + nextNode = n.ParamChild + } else if token[0] == '#' { + // #param case + var name string + name, remaining = splitRelaxed(remaining) + + // Check param name is unique + for _, e := range usedParams { + if e == name { + return errors.New( + fmt.Sprintf("A route can't have two placeholders with the same name: %s", name), + ) + } + } + usedParams = append(usedParams, name) + + if n.RelaxedChild == nil { + n.RelaxedChild = &node{} + n.RelaxedName = name + } else { + if n.RelaxedName != name { + return errors.New( + fmt.Sprintf( + "Routes sharing a common placeholder MUST name it consistently: %s != %s", + n.RelaxedName, + name, + ), + ) + } + } + nextNode = n.RelaxedChild + } else if token[0] == '*' { + // *splat case + name := remaining + remaining = "" + + // Check param name is unique + for _, e := range usedParams { + if e == name { + return errors.New( + fmt.Sprintf("A route can't have two placeholders with the same name: %s", name), + ) + } + } + + if n.SplatChild == nil { + n.SplatChild = &node{} + n.SplatName = name + } + nextNode = n.SplatChild + } else { + // general case + if n.Children == nil { + n.Children = map[string]*node{} + n.ChildrenKeyLen = 1 + } + if n.Children[token] == nil { + n.Children[token] = &node{} + } + nextNode = n.Children[token] + } + + return nextNode.addRoute(httpMethod, remaining, route, usedParams) +} + +func (n *node) compress() { + // *splat branch + if n.SplatChild != nil { + n.SplatChild.compress() + } + // :param branch + if n.ParamChild != nil { + n.ParamChild.compress() + } + // #param branch + if n.RelaxedChild != nil { + n.RelaxedChild.compress() + } + // main branch + if len(n.Children) == 0 { + return + } + // compressable ? + canCompress := true + for _, node := range n.Children { + if node.HttpMethodToRoute != nil || node.SplatChild != nil || node.ParamChild != nil || node.RelaxedChild != nil { + canCompress = false + } + } + // compress + if canCompress { + merged := map[string]*node{} + for key, node := range n.Children { + for gdKey, gdNode := range node.Children { + mergedKey := key + gdKey + merged[mergedKey] = gdNode + } + } + n.Children = merged + n.ChildrenKeyLen++ + n.compress() + // continue + } else { + for _, node := range n.Children { + node.compress() + } + } +} + +func printFPadding(padding int, format string, a ...interface{}) { + for i := 0; i < padding; i++ { + fmt.Print(" ") + } + fmt.Printf(format, a...) +} + +// Private function for now +func (n *node) printDebug(level int) { + level++ + // *splat branch + if n.SplatChild != nil { + printFPadding(level, "*splat\n") + n.SplatChild.printDebug(level) + } + // :param branch + if n.ParamChild != nil { + printFPadding(level, ":param\n") + n.ParamChild.printDebug(level) + } + // #param branch + if n.RelaxedChild != nil { + printFPadding(level, "#relaxed\n") + n.RelaxedChild.printDebug(level) + } + // main branch + for key, node := range n.Children { + printFPadding(level, "\"%s\"\n", key) + node.printDebug(level) + } +} + +// utility for the node.findRoutes recursive method + +type paramMatch struct { + name string + value string +} + +type findContext struct { + paramStack []paramMatch + matchFunc func(httpMethod, path string, node *node) +} + +func newFindContext() *findContext { + return &findContext{ + paramStack: []paramMatch{}, + } +} + +func (fc *findContext) pushParams(name, value string) { + fc.paramStack = append( + fc.paramStack, + paramMatch{name, value}, + ) +} + +func (fc *findContext) popParams() { + fc.paramStack = fc.paramStack[:len(fc.paramStack)-1] +} + +func (fc *findContext) paramsAsMap() map[string]string { + r := map[string]string{} + for _, param := range fc.paramStack { + if r[param.name] != "" { + // this is checked at addRoute time, and should never happen. + panic(fmt.Sprintf( + "placeholder %s already found, placeholder names should be unique per route", + param.name, + )) + } + r[param.name] = param.value + } + return r +} + +type Match struct { + // Same Route as in AddRoute + Route interface{} + // map of params matched for this result + Params map[string]string +} + +func (n *node) find(httpMethod, path string, context *findContext) { + + if n.HttpMethodToRoute != nil && path == "" { + context.matchFunc(httpMethod, path, n) + } + + if len(path) == 0 { + return + } + + // *splat branch + if n.SplatChild != nil { + context.pushParams(n.SplatName, path) + n.SplatChild.find(httpMethod, "", context) + context.popParams() + } + + // :param branch + if n.ParamChild != nil { + value, remaining := splitParam(path) + context.pushParams(n.ParamName, value) + n.ParamChild.find(httpMethod, remaining, context) + context.popParams() + } + + // #param branch + if n.RelaxedChild != nil { + value, remaining := splitRelaxed(path) + context.pushParams(n.RelaxedName, value) + n.RelaxedChild.find(httpMethod, remaining, context) + context.popParams() + } + + // main branch + length := n.ChildrenKeyLen + if len(path) < length { + return + } + token := path[0:length] + remaining := path[length:] + if n.Children[token] != nil { + n.Children[token].find(httpMethod, remaining, context) + } +} + +type Trie struct { + root *node +} + +// Instanciate a Trie with an empty node as the root. +func New() *Trie { + return &Trie{ + root: &node{}, + } +} + +// Insert the route in the Trie following or creating the nodes corresponding to the path. +func (t *Trie) AddRoute(httpMethod, pathExp string, route interface{}) error { + return t.root.addRoute(httpMethod, pathExp, route, []string{}) +} + +// Reduce the size of the tree, must be done after the last AddRoute. +func (t *Trie) Compress() { + t.root.compress() +} + +// Private function for now. +func (t *Trie) printDebug() { + fmt.Print("\n") + t.root.printDebug(0) + fmt.Print("\n") +} + +// Given a path and an http method, return all the matching routes. +func (t *Trie) FindRoutes(httpMethod, path string) []*Match { + context := newFindContext() + matches := []*Match{} + context.matchFunc = func(httpMethod, path string, node *node) { + if node.HttpMethodToRoute[httpMethod] != nil { + // path and method match, found a route ! + matches = append( + matches, + &Match{ + Route: node.HttpMethodToRoute[httpMethod], + Params: context.paramsAsMap(), + }, + ) + } + } + t.root.find(httpMethod, path, context) + return matches +} + +// Same as FindRoutes, but return in addition a boolean indicating if the path was matched. +// Useful to return 405 +func (t *Trie) FindRoutesAndPathMatched(httpMethod, path string) ([]*Match, bool) { + context := newFindContext() + pathMatched := false + matches := []*Match{} + context.matchFunc = func(httpMethod, path string, node *node) { + pathMatched = true + if node.HttpMethodToRoute[httpMethod] != nil { + // path and method match, found a route ! + matches = append( + matches, + &Match{ + Route: node.HttpMethodToRoute[httpMethod], + Params: context.paramsAsMap(), + }, + ) + } + } + t.root.find(httpMethod, path, context) + return matches, pathMatched +} + +// Given a path, and whatever the http method, return all the matching routes. +func (t *Trie) FindRoutesForPath(path string) []*Match { + context := newFindContext() + matches := []*Match{} + context.matchFunc = func(httpMethod, path string, node *node) { + params := context.paramsAsMap() + for _, route := range node.HttpMethodToRoute { + matches = append( + matches, + &Match{ + Route: route, + Params: params, + }, + ) + } + } + t.root.find("", path, context) + return matches +} diff --git a/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/trie/impl_test.go b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/trie/impl_test.go new file mode 100644 index 0000000..d8fc8b5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/ant0ine/go-json-rest/rest/trie/impl_test.go @@ -0,0 +1,276 @@ +package trie + +import ( + "testing" +) + +func TestPathInsert(t *testing.T) { + + trie := New() + if trie.root == nil { + t.Error("Expected to not be nil") + } + + trie.AddRoute("GET", "/", "1") + if trie.root.Children["/"] == nil { + t.Error("Expected to not be nil") + } + + trie.AddRoute("GET", "/r", "2") + if trie.root.Children["/"].Children["r"] == nil { + t.Error("Expected to not be nil") + } + + trie.AddRoute("GET", "/r/", "3") + if trie.root.Children["/"].Children["r"].Children["/"] == nil { + t.Error("Expected to not be nil") + } +} + +func TestTrieCompression(t *testing.T) { + + trie := New() + trie.AddRoute("GET", "/abc", "3") + trie.AddRoute("GET", "/adc", "3") + + // before compression + if trie.root.Children["/"].Children["a"].Children["b"].Children["c"] == nil { + t.Error("Expected to not be nil") + } + if trie.root.Children["/"].Children["a"].Children["d"].Children["c"] == nil { + t.Error("Expected to not be nil") + } + + trie.Compress() + + // after compression + if trie.root.Children["/abc"] == nil { + t.Errorf("%+v", trie.root) + } + if trie.root.Children["/adc"] == nil { + t.Errorf("%+v", trie.root) + } +} + +func TestParamInsert(t *testing.T) { + trie := New() + + trie.AddRoute("GET", "/:id/", "") + if trie.root.Children["/"].ParamChild.Children["/"] == nil { + t.Error("Expected to not be nil") + } + if trie.root.Children["/"].ParamName != "id" { + t.Error("Expected ParamName to be id") + } + + trie.AddRoute("GET", "/:id/:property.:format", "") + if trie.root.Children["/"].ParamChild.Children["/"].ParamChild.Children["."].ParamChild == nil { + t.Error("Expected to not be nil") + } + if trie.root.Children["/"].ParamName != "id" { + t.Error("Expected ParamName to be id") + } + if trie.root.Children["/"].ParamChild.Children["/"].ParamName != "property" { + t.Error("Expected ParamName to be property") + } + if trie.root.Children["/"].ParamChild.Children["/"].ParamChild.Children["."].ParamName != "format" { + t.Error("Expected ParamName to be format") + } +} + +func TestRelaxedInsert(t *testing.T) { + trie := New() + + trie.AddRoute("GET", "/#id/", "") + if trie.root.Children["/"].RelaxedChild.Children["/"] == nil { + t.Error("Expected to not be nil") + } + if trie.root.Children["/"].RelaxedName != "id" { + t.Error("Expected RelaxedName to be id") + } +} + +func TestSplatInsert(t *testing.T) { + trie := New() + trie.AddRoute("GET", "/*splat", "") + if trie.root.Children["/"].SplatChild == nil { + t.Error("Expected to not be nil") + } +} + +func TestDupeInsert(t *testing.T) { + trie := New() + trie.AddRoute("GET", "/", "1") + err := trie.AddRoute("GET", "/", "2") + if err == nil { + t.Error("Expected to not be nil") + } + if trie.root.Children["/"].HttpMethodToRoute["GET"] != "1" { + t.Error("Expected to be 1") + } +} + +func isInMatches(test string, matches []*Match) bool { + for _, match := range matches { + if match.Route.(string) == test { + return true + } + } + return false +} + +func TestFindRoute(t *testing.T) { + + trie := New() + + trie.AddRoute("GET", "/", "root") + trie.AddRoute("GET", "/r/:id", "resource") + trie.AddRoute("GET", "/r/:id/property", "property") + trie.AddRoute("GET", "/r/:id/property.*format", "property_format") + trie.AddRoute("GET", "/user/#username/property", "user_property") + + trie.Compress() + + matches := trie.FindRoutes("GET", "/") + if len(matches) != 1 { + t.Errorf("expected one route, got %d", len(matches)) + } + if !isInMatches("root", matches) { + t.Error("expected 'root'") + } + + matches = trie.FindRoutes("GET", "/notfound") + if len(matches) != 0 { + t.Errorf("expected zero route, got %d", len(matches)) + } + + matches = trie.FindRoutes("GET", "/r/1") + if len(matches) != 1 { + t.Errorf("expected one route, got %d", len(matches)) + } + if !isInMatches("resource", matches) { + t.Errorf("expected 'resource', got %+v", matches) + } + if matches[0].Params["id"] != "1" { + t.Error("Expected Params id to be 1") + } + + matches = trie.FindRoutes("GET", "/r/1/property") + if len(matches) != 1 { + t.Errorf("expected one route, got %d", len(matches)) + } + if !isInMatches("property", matches) { + t.Error("expected 'property'") + } + if matches[0].Params["id"] != "1" { + t.Error("Expected Params id to be 1") + } + + matches = trie.FindRoutes("GET", "/r/1/property.json") + if len(matches) != 1 { + t.Errorf("expected one route, got %d", len(matches)) + } + if !isInMatches("property_format", matches) { + t.Error("expected 'property_format'") + } + if matches[0].Params["id"] != "1" { + t.Error("Expected Params id to be 1") + } + if matches[0].Params["format"] != "json" { + t.Error("Expected Params format to be json") + } + + matches = trie.FindRoutes("GET", "/user/antoine.imbert/property") + if len(matches) != 1 { + t.Errorf("expected one route, got %d", len(matches)) + } + if !isInMatches("user_property", matches) { + t.Error("expected 'user_property'") + } + if matches[0].Params["username"] != "antoine.imbert" { + t.Error("Expected Params username to be antoine.imbert") + } +} + +func TestFindRouteMultipleMatches(t *testing.T) { + + trie := New() + + trie.AddRoute("GET", "/r/1", "resource1") + trie.AddRoute("GET", "/r/2", "resource2") + trie.AddRoute("GET", "/r/:id", "resource_generic") + trie.AddRoute("GET", "/s/*rest", "special_all") + trie.AddRoute("GET", "/s/:param", "special_generic") + trie.AddRoute("GET", "/s/#param", "special_relaxed") + trie.AddRoute("GET", "/", "root") + + trie.Compress() + + matches := trie.FindRoutes("GET", "/r/1") + if len(matches) != 2 { + t.Errorf("expected two matches, got %d", len(matches)) + } + if !isInMatches("resource_generic", matches) { + t.Error("Expected resource_generic to match") + } + if !isInMatches("resource1", matches) { + t.Error("Expected resource1 to match") + } + + matches = trie.FindRoutes("GET", "/s/1") + if len(matches) != 3 { + t.Errorf("expected two matches, got %d", len(matches)) + } + if !isInMatches("special_all", matches) { + t.Error("Expected special_all to match") + } + if !isInMatches("special_generic", matches) { + t.Error("Expected special_generic to match") + } + if !isInMatches("special_relaxed", matches) { + t.Error("Expected special_relaxed to match") + } +} + +func TestConsistentPlaceholderName(t *testing.T) { + + trie := New() + + trie.AddRoute("GET", "/r/:id", "oneph") + err := trie.AddRoute("GET", "/r/:rid/other", "twoph") + if err == nil { + t.Error("Should have died on inconsistent placeholder name") + } + + trie.AddRoute("GET", "/r/#id", "oneph") + err = trie.AddRoute("GET", "/r/#rid/other", "twoph") + if err == nil { + t.Error("Should have died on inconsistent placeholder name") + } + + trie.AddRoute("GET", "/r/*id", "oneph") + err = trie.AddRoute("GET", "/r/*rid", "twoph") + if err == nil { + t.Error("Should have died on duplicated route") + } +} + +func TestDuplicateName(t *testing.T) { + + trie := New() + + err := trie.AddRoute("GET", "/r/:id/o/:id", "two") + if err == nil { + t.Error("Should have died, this route has two placeholder named `id`") + } + + err = trie.AddRoute("GET", "/r/:id/o/*id", "two") + if err == nil { + t.Error("Should have died, this route has two placeholder named `id`") + } + + err = trie.AddRoute("GET", "/r/:id/o/#id", "two") + if err == nil { + t.Error("Should have died, this route has two placeholder named `id`") + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/License b/Godeps/_workspace/src/github.com/jinzhu/gorm/License new file mode 100644 index 0000000..037e165 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/License @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013-NOW Jinzhu + +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/Godeps/_workspace/src/github.com/jinzhu/gorm/README.md b/Godeps/_workspace/src/github.com/jinzhu/gorm/README.md new file mode 100644 index 0000000..dbfbedd --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/README.md @@ -0,0 +1,1226 @@ +# GORM + +[![Join the chat at https://gitter.im/jinzhu/gorm](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jinzhu/gorm?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +The fantastic ORM library for Golang, aims to be developer friendly. + +[![wercker status](https://app.wercker.com/status/0cb7bb1039e21b74f8274941428e0921/s/master "wercker status")](https://app.wercker.com/project/bykey/0cb7bb1039e21b74f8274941428e0921) + +## Overview + +* Full-Featured ORM (almost) +* Chainable API +* Auto Migrations +* Relations (Has One, Has Many, Belongs To, Many To Many, [Polymorphism](#polymorphism)) +* Callbacks (Before/After Create/Save/Update/Delete/Find) +* Preloading (eager loading) +* Transactions +* Embed Anonymous Struct +* Soft Deletes +* Customizable Logger +* Iteration Support via [Rows](#row--rows) +* Every feature comes with tests +* Developer Friendly + +# Getting Started + +## Install + +``` +go get -u github.com/jinzhu/gorm +``` + +## Define Models (Structs) + +```go +type User struct { + ID int + Birthday time.Time + Age int + Name string `sql:"size:255"` // Default size for string is 255, you could reset it with this tag + Num int `sql:"AUTO_INCREMENT"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt *time.Time + + Emails []Email // One-To-Many relationship (has many) + BillingAddress Address // One-To-One relationship (has one) + BillingAddressID sql.NullInt64 // Foreign key of BillingAddress + ShippingAddress Address // One-To-One relationship (has one) + ShippingAddressID int // Foreign key of ShippingAddress + IgnoreMe int `sql:"-"` // Ignore this field + Languages []Language `gorm:"many2many:user_languages;"` // Many-To-Many relationship, 'user_languages' is join table +} + +type Email struct { + ID int + UserID int `sql:"index"` // Foreign key (belongs to), tag `index` will create index for this field when using AutoMigrate + Email string `sql:"type:varchar(100);unique_index"` // Set field's sql type, tag `unique_index` will create unique index + Subscribed bool +} + +type Address struct { + ID int + Address1 string `sql:"not null;unique"` // Set field as not nullable and unique + Address2 string `sql:"type:varchar(100);unique"` + Post sql.NullString `sql:"not null"` +} + +type Language struct { + ID int + Name string `sql:"index:idx_name_code"` // Create index with name, and will create combined index if find other fields defined same name + Code string `sql:"index:idx_name_code"` // `unique_index` also works +} +``` + +## Conventions + +* Table name is the plural of struct name's snake case, you can disable pluralization with `db.SingularTable(true)`, or [Specifying The Table Name For A Struct Permanently With TableName](#specifying-the-table-name-for-a-struct-permanently-with-tablename) + +```go +type User struct{} // struct User's database table name is "users" by default, will be "user" if you disabled pluralisation +``` + +* Column name is the snake case of field's name +* Use `ID` field as primary key +* Use `CreatedAt` to store record's created time if field exists +* Use `UpdatedAt` to store record's updated time if field exists +* Use `DeletedAt` to store record's deleted time if field exists [Soft Delete](#soft-delete) +* Gorm provide a default model struct, you could embed it in your struct + +```go +type Model struct { + ID uint `gorm:"primary_key"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt *time.Time +} + +type User struct { + gorm.Model + Name string +} +``` + +## Initialize Database + +```go +import ( + "github.com/jinzhu/gorm" + _ "github.com/lib/pq" + _ "github.com/go-sql-driver/mysql" + _ "github.com/mattn/go-sqlite3" +) + +db, err := gorm.Open("postgres", "user=gorm dbname=gorm sslmode=disable") +// db, err := gorm.Open("foundation", "dbname=gorm") // FoundationDB. +// db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local") +// db, err := gorm.Open("sqlite3", "/tmp/gorm.db") + +// You can also use an existing database connection handle +// dbSql, _ := sql.Open("postgres", "user=gorm dbname=gorm sslmode=disable") +// db, _ := gorm.Open("postgres", dbSql) + +// Get database connection handle [*sql.DB](http://golang.org/pkg/database/sql/#DB) +db.DB() + +// Then you could invoke `*sql.DB`'s functions with it +db.DB().Ping() +db.DB().SetMaxIdleConns(10) +db.DB().SetMaxOpenConns(100) + +// Disable table name's pluralization +db.SingularTable(true) +``` + +## Migration + +```go +// Create table +db.CreateTable(&User{}) +db.Set("gorm:table_options", "ENGINE=InnoDB").CreateTable(&User{}) + +// Drop table +db.DropTable(&User{}) + +// Automating Migration +db.AutoMigrate(&User{}) +db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{}) +db.AutoMigrate(&User{}, &Product{}, &Order{}) +// Feel free to change your struct, AutoMigrate will keep your database up-to-date. +// AutoMigrate will ONLY add *new columns* and *new indexes*, +// WON'T update current column's type or delete unused columns, to protect your data. +// If the table is not existing, AutoMigrate will create the table automatically. +``` + +# Basic CRUD + +## Create Record + +```go +user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()} + +db.NewRecord(user) // => returns `true` if primary key is blank + +db.Create(&user) + +db.NewRecord(user) // => return `false` after `user` created + +// Associations will be inserted automatically when save the record +user := User{ + Name: "jinzhu", + BillingAddress: Address{Address1: "Billing Address - Address 1"}, + ShippingAddress: Address{Address1: "Shipping Address - Address 1"}, + Emails: []Email{{Email: "jinzhu@example.com"}, {Email: "jinzhu-2@example@example.com"}}, + Languages: []Language{{Name: "ZH"}, {Name: "EN"}}, +} + +db.Create(&user) +//// BEGIN TRANSACTION; +//// INSERT INTO "addresses" (address1) VALUES ("Billing Address - Address 1"); +//// INSERT INTO "addresses" (address1) VALUES ("Shipping Address - Address 1"); +//// INSERT INTO "users" (name,billing_address_id,shipping_address_id) VALUES ("jinzhu", 1, 2); +//// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu@example.com"); +//// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu-2@example.com"); +//// INSERT INTO "languages" ("name") VALUES ('ZH'); +//// INSERT INTO user_languages ("user_id","language_id") VALUES (111, 1); +//// INSERT INTO "languages" ("name") VALUES ('EN'); +//// INSERT INTO user_languages ("user_id","language_id") VALUES (111, 2); +//// COMMIT; +``` + +Refer [Associations](#associations) for more details + +## Query + +```go +// Get the first record +db.First(&user) +//// SELECT * FROM users ORDER BY id LIMIT 1; + +// Get the last record +db.Last(&user) +//// SELECT * FROM users ORDER BY id DESC LIMIT 1; + +// Get all records +db.Find(&users) +//// SELECT * FROM users; + +// Get record with primary key +db.First(&user, 10) +//// SELECT * FROM users WHERE id = 10; +``` + +### Query With Where (Plain SQL) + +```go +// Get the first matched record +db.Where("name = ?", "jinzhu").First(&user) +//// SELECT * FROM users WHERE name = 'jinzhu' limit 1; + +// Get all matched records +db.Where("name = ?", "jinzhu").Find(&users) +//// SELECT * FROM users WHERE name = 'jinzhu'; + +db.Where("name <> ?", "jinzhu").Find(&users) + +// IN +db.Where("name in (?)", []string{"jinzhu", "jinzhu 2"}).Find(&users) + +// LIKE +db.Where("name LIKE ?", "%jin%").Find(&users) + +// AND +db.Where("name = ? and age >= ?", "jinzhu", "22").Find(&users) + +// Time +db.Where("updated_at > ?", lastWeek).Find(&users) + +db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users) +``` + +### Query With Where (Struct & Map) + +```go +// Struct +db.Where(&User{Name: "jinzhu", Age: 20}).First(&user) +//// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 LIMIT 1; + +// Map +db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users) +//// SELECT * FROM users WHERE name = "jinzhu" AND age = 20; + +// Slice of primary keys +db.Where([]int64{20, 21, 22}).Find(&users) +//// SELECT * FROM users WHERE id IN (20, 21, 22); +``` + +### Query With Not + +```go +db.Not("name", "jinzhu").First(&user) +//// SELECT * FROM users WHERE name <> "jinzhu" LIMIT 1; + +// Not In +db.Not("name", []string{"jinzhu", "jinzhu 2"}).Find(&users) +//// SELECT * FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2"); + +// Not In slice of primary keys +db.Not([]int64{1,2,3}).First(&user) +//// SELECT * FROM users WHERE id NOT IN (1,2,3); + +db.Not([]int64{}).First(&user) +//// SELECT * FROM users; + +// Plain SQL +db.Not("name = ?", "jinzhu").First(&user) +//// SELECT * FROM users WHERE NOT(name = "jinzhu"); + +// Struct +db.Not(User{Name: "jinzhu"}).First(&user) +//// SELECT * FROM users WHERE name <> "jinzhu"; +``` + +### Query With Inline Condition + +```go +// Get by primary key +db.First(&user, 23) +//// SELECT * FROM users WHERE id = 23 LIMIT 1; + +// Plain SQL +db.Find(&user, "name = ?", "jinzhu") +//// SELECT * FROM users WHERE name = "jinzhu"; + +db.Find(&users, "name <> ? AND age > ?", "jinzhu", 20) +//// SELECT * FROM users WHERE name <> "jinzhu" AND age > 20; + +// Struct +db.Find(&users, User{Age: 20}) +//// SELECT * FROM users WHERE age = 20; + +// Map +db.Find(&users, map[string]interface{}{"age": 20}) +//// SELECT * FROM users WHERE age = 20; +``` + +### Query With Or + +```go +db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users) +//// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin'; + +// Struct +db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2"}).Find(&users) +//// SELECT * FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2'; + +// Map +db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2"}).Find(&users) +``` + +### Query Chains + +Gorm has a chainable API, you could use it like this + +```go +db.Where("name <> ?","jinzhu").Where("age >= ? and role <> ?",20,"admin").Find(&users) +//// SELECT * FROM users WHERE name <> 'jinzhu' AND age >= 20 AND role <> 'admin'; + +db.Where("role = ?", "admin").Or("role = ?", "super_admin").Not("name = ?", "jinzhu").Find(&users) +``` + +### Preloading (Eager loading) + +```go +db.Preload("Orders").Find(&users) +//// SELECT * FROM users; +//// SELECT * FROM orders WHERE user_id IN (1,2,3,4); + +db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users) +//// SELECT * FROM users; +//// SELECT * FROM orders WHERE user_id IN (1,2,3,4) AND state NOT IN ('cancelled'); + +db.Where("state = ?", "active").Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users) +//// SELECT * FROM users WHERE state = 'active'; +//// SELECT * FROM orders WHERE user_id IN (1,2) AND state NOT IN ('cancelled'); + +db.Preload("Orders").Preload("Profile").Preload("Role").Find(&users) +//// SELECT * FROM users; +//// SELECT * FROM orders WHERE user_id IN (1,2,3,4); // has many +//// SELECT * FROM profiles WHERE user_id IN (1,2,3,4); // has one +//// SELECT * FROM roles WHERE id IN (4,5,6); // belongs to +``` + +#### Nested Preloading + +```go +db.Preload("Orders.OrderItems").Find(&users) +db.Preload("Orders", "state = ?", "paid").Preload("Orders.OrderItems").Find(&users) +``` + +## Update + +```go +// Update an existing struct +db.First(&user) +user.Name = "jinzhu 2" +user.Age = 100 +db.Save(&user) +//// UPDATE users SET name='jinzhu 2', age=100, updated_at = '2013-11-17 21:34:10' WHERE id=111; + +db.Where("active = ?", true).Save(&user) +//// UPDATE users SET name='jinzhu 2', age=100, updated_at = '2013-11-17 21:34:10' WHERE id=111 AND active = true; + +// Update an attribute if it is changed +db.Model(&user).Update("name", "hello") +//// UPDATE users SET name='hello', updated_at = '2013-11-17 21:34:10' WHERE id=111; + +db.Model(&user).Where("active = ?", true).Update("name", "hello") +//// UPDATE users SET name='hello', updated_at = '2013-11-17 21:34:10' WHERE id=111 AND active = true; + +db.First(&user, 111).Update("name", "hello") +//// SELECT * FROM users LIMIT 1; +//// UPDATE users SET name='hello', updated_at = '2013-11-17 21:34:10' WHERE id=111; + +// Update multiple attributes if they are changed +db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false}) + +// Update multiple attributes if they are changed (update with struct only works with none zero values) +db.Model(&user).Updates(User{Name: "hello", Age: 18}) +//// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111; +``` + +### Update Without Callbacks + +By default, update will call BeforeUpdate, AfterUpdate callbacks, if you want to update w/o callbacks and w/o saving associations: + +```go +db.Model(&user).UpdateColumn("name", "hello") +//// UPDATE users SET name='hello' WHERE id = 111; + +// Update with struct only works with none zero values, or use map[string]interface{} +db.Model(&user).UpdateColumns(User{Name: "hello", Age: 18}) +//// UPDATE users SET name='hello', age=18 WHERE id = 111; +``` + +### Batch Updates + +```go +db.Table("users").Where("id = ?", 10).Updates(map[string]interface{}{"name": "hello", "age": 18}) +//// UPDATE users SET name='hello', age=18 WHERE id = 10; + +// Update with struct only works with none zero values, or use map[string]interface{} +db.Model(User{}).Updates(User{Name: "hello", Age: 18}) +//// UPDATE users SET name='hello', age=18; + +// Callbacks won't run when do batch updates + +// Use `RowsAffected` to get the count of affected records +db.Model(User{}).Updates(User{Name: "hello", Age: 18}).RowsAffected +``` + +### Update with SQL Expression + +```go +DB.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100)) +//// UPDATE "products" SET "code" = 'L1212', "price" = price * '2' + '100', "updated_at" = '2013-11-17 21:34:10' WHERE "id" = '2'; + +DB.Model(&product).Updates(map[string]interface{}{"price": gorm.Expr("price * ? + ?", 2, 100)}) +//// UPDATE "products" SET "code" = 'L1212', "price" = price * '2' + '100', "updated_at" = '2013-11-17 21:34:10' WHERE "id" = '2'; + +DB.Model(&product).UpdateColumn("quantity", gorm.Expr("quantity - ?", 1)) +//// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = '2'; + +DB.Model(&product).Where("quantity > 1").UpdateColumn("quantity", gorm.Expr("quantity - ?", 1)) +//// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = '2' AND quantity > 1; +``` + +## Delete + +```go +// Delete an existing record +db.Delete(&email) +//// DELETE from emails where id=10; +``` + +### Batch Delete + +```go +db.Where("email LIKE ?", "%jinzhu%").Delete(Email{}) +//// DELETE from emails where email LIKE "%jinhu%"; +``` + +### Soft Delete + +If struct has `DeletedAt` field, it will get soft delete ability automatically! +Then it won't be deleted from database permanently when call `Delete`. + +```go +db.Delete(&user) +//// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111; + +// Batch Delete +db.Where("age = ?", 20).Delete(&User{}) +//// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20; + +// Soft deleted records will be ignored when query them +db.Where("age = 20").Find(&user) +//// SELECT * FROM users WHERE age = 20 AND (deleted_at IS NULL OR deleted_at <= '0001-01-02'); + +// Find soft deleted records with Unscoped +db.Unscoped().Where("age = 20").Find(&users) +//// SELECT * FROM users WHERE age = 20; + +// Delete record permanently with Unscoped +db.Unscoped().Delete(&order) +//// DELETE FROM orders WHERE id=10; +``` + +## Associations + +### Has One + +```go +// User has one address +db.Model(&user).Related(&address) +//// SELECT * FROM addresses WHERE id = 123; // 123 is user's foreign key AddressId + +// Specify the foreign key +db.Model(&user).Related(&address1, "BillingAddressId") +//// SELECT * FROM addresses WHERE id = 123; // 123 is user's foreign key BillingAddressId +``` + +### Belongs To + +```go +// Email belongs to user +db.Model(&email).Related(&user) +//// SELECT * FROM users WHERE id = 111; // 111 is email's foreign key UserId + +// Specify the foreign key +db.Model(&email).Related(&user, "ProfileId") +//// SELECT * FROM users WHERE id = 111; // 111 is email's foreign key ProfileId +``` + +### Has Many + +```go +// User has many emails +db.Model(&user).Related(&emails) +//// SELECT * FROM emails WHERE user_id = 111; +// user_id is the foreign key, 111 is user's primary key's value + +// Specify the foreign key +db.Model(&user).Related(&emails, "ProfileId") +//// SELECT * FROM emails WHERE profile_id = 111; +// profile_id is the foreign key, 111 is user's primary key's value +``` + +### Many To Many + +```go +// User has many languages and belongs to many languages +db.Model(&user).Related(&languages, "Languages") +//// SELECT * FROM "languages" INNER JOIN "user_languages" ON "user_languages"."language_id" = "languages"."id" WHERE "user_languages"."user_id" = 111 +// `Languages` is user's column name, this column's tag defined join table like this `gorm:"many2many:user_languages;"` +``` + +There is also a mode used to handle many to many relations easily + +```go +// Query +db.Model(&user).Association("Languages").Find(&languages) +// same as `db.Model(&user).Related(&languages, "Languages")` + +db.Where("name = ?", "ZH").First(&languageZH) +db.Where("name = ?", "EN").First(&languageEN) + +// Append +db.Model(&user).Association("Languages").Append([]Language{languageZH, languageEN}) +db.Model(&user).Association("Languages").Append([]Language{{Name: "DE"}}) +db.Model(&user).Association("Languages").Append(Language{Name: "DE"}) + +// Delete +db.Model(&user).Association("Languages").Delete([]Language{languageZH, languageEN}) +db.Model(&user).Association("Languages").Delete(languageZH, languageEN) + +// Replace +db.Model(&user).Association("Languages").Replace([]Language{languageZH, languageEN}) +db.Model(&user).Association("Languages").Replace(Language{Name: "DE"}, languageEN) + +// Count +db.Model(&user).Association("Languages").Count() +// Return the count of languages the user has + +// Clear +db.Model(&user).Association("Languages").Clear() +// Remove all relations between the user and languages +``` + +### Polymorphism + +Supports polymorphic has-many and has-one associations. + +```go + type Cat struct { + Id int + Name string + Toy Toy `gorm:"polymorphic:Owner;"` + } + + type Dog struct { + Id int + Name string + Toy Toy `gorm:"polymorphic:Owner;"` + } + + type Toy struct { + Id int + Name string + OwnerId int + OwnerType string + } +``` +Note: polymorphic belongs-to and many-to-many are explicitly NOT supported, and will throw errors. + +## Advanced Usage + +## FirstOrInit + +Get the first matched record, or initialize a record with search conditions. + +```go +// Unfound +db.FirstOrInit(&user, User{Name: "non_existing"}) +//// user -> User{Name: "non_existing"} + +// Found +db.Where(User{Name: "Jinzhu"}).FirstOrInit(&user) +//// user -> User{Id: 111, Name: "Jinzhu", Age: 20} +db.FirstOrInit(&user, map[string]interface{}{"name": "jinzhu"}) +//// user -> User{Id: 111, Name: "Jinzhu", Age: 20} +``` + +### Attrs + +Ignore some values when searching, but use them to initialize the struct if record is not found. + +```go +// Unfound +db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrInit(&user) +//// SELECT * FROM USERS WHERE name = 'non_existing'; +//// user -> User{Name: "non_existing", Age: 20} + +db.Where(User{Name: "noexisting_user"}).Attrs("age", 20).FirstOrInit(&user) +//// SELECT * FROM USERS WHERE name = 'non_existing'; +//// user -> User{Name: "non_existing", Age: 20} + +// Found +db.Where(User{Name: "Jinzhu"}).Attrs(User{Age: 30}).FirstOrInit(&user) +//// SELECT * FROM USERS WHERE name = jinzhu'; +//// user -> User{Id: 111, Name: "Jinzhu", Age: 20} +``` + +### Assign + +Ignore some values when searching, but assign it to the result regardless it is found or not. + +```go +// Unfound +db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrInit(&user) +//// user -> User{Name: "non_existing", Age: 20} + +// Found +db.Where(User{Name: "Jinzhu"}).Assign(User{Age: 30}).FirstOrInit(&user) +//// SELECT * FROM USERS WHERE name = jinzhu'; +//// user -> User{Id: 111, Name: "Jinzhu", Age: 30} +``` + +## FirstOrCreate + +Get the first matched record, or create with search conditions. + +```go +// Unfound +db.FirstOrCreate(&user, User{Name: "non_existing"}) +//// INSERT INTO "users" (name) VALUES ("non_existing"); +//// user -> User{Id: 112, Name: "non_existing"} + +// Found +db.Where(User{Name: "Jinzhu"}).FirstOrCreate(&user) +//// user -> User{Id: 111, Name: "Jinzhu"} +``` + +### Attrs + +Ignore some values when searching, but use them to create the struct if record is not found. like `FirstOrInit` + +```go +// Unfound +db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrCreate(&user) +//// SELECT * FROM users WHERE name = 'non_existing'; +//// INSERT INTO "users" (name, age) VALUES ("non_existing", 20); +//// user -> User{Id: 112, Name: "non_existing", Age: 20} + +// Found +db.Where(User{Name: "jinzhu"}).Attrs(User{Age: 30}).FirstOrCreate(&user) +//// SELECT * FROM users WHERE name = 'jinzhu'; +//// user -> User{Id: 111, Name: "jinzhu", Age: 20} +``` + +### Assign + +Ignore some values when searching, but assign it to the record regardless it is found or not, then save back to database. like `FirstOrInit` + +```go +// Unfound +db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrCreate(&user) +//// SELECT * FROM users WHERE name = 'non_existing'; +//// INSERT INTO "users" (name, age) VALUES ("non_existing", 20); +//// user -> User{Id: 112, Name: "non_existing", Age: 20} + +// Found +db.Where(User{Name: "jinzhu"}).Assign(User{Age: 30}).FirstOrCreate(&user) +//// SELECT * FROM users WHERE name = 'jinzhu'; +//// UPDATE users SET age=30 WHERE id = 111; +//// user -> User{Id: 111, Name: "jinzhu", Age: 30} +``` + +## Select + +```go +db.Select("name, age").Find(&users) +//// SELECT name, age FROM users; + +db.Select([]string{"name", "age"}).Find(&users) +//// SELECT name, age FROM users; + +db.Table("users").Select("COALESCE(age,?)", 42).Rows() +//// SELECT COALESCE(age,'42') FROM users; +``` + +## Order + +```go +db.Order("age desc, name").Find(&users) +//// SELECT * FROM users ORDER BY age desc, name; + +// Multiple orders +db.Order("age desc").Order("name").Find(&users) +//// SELECT * FROM users ORDER BY age desc, name; + +// ReOrder +db.Order("age desc").Find(&users1).Order("age", true).Find(&users2) +//// SELECT * FROM users ORDER BY age desc; (users1) +//// SELECT * FROM users ORDER BY age; (users2) +``` + +## Limit + +```go +db.Limit(3).Find(&users) +//// SELECT * FROM users LIMIT 3; + +// Cancel limit condition with -1 +db.Limit(10).Find(&users1).Limit(-1).Find(&users2) +//// SELECT * FROM users LIMIT 10; (users1) +//// SELECT * FROM users; (users2) +``` + +## Offset + +```go +db.Offset(3).Find(&users) +//// SELECT * FROM users OFFSET 3; + +// Cancel offset condition with -1 +db.Offset(10).Find(&users1).Offset(-1).Find(&users2) +//// SELECT * FROM users OFFSET 10; (users1) +//// SELECT * FROM users; (users2) +``` + +## Count + +```go +db.Where("name = ?", "jinzhu").Or("name = ?", "jinzhu 2").Find(&users).Count(&count) +//// SELECT * from USERS WHERE name = 'jinzhu' OR name = 'jinzhu 2'; (users) +//// SELECT count(*) FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2'; (count) + +db.Model(User{}).Where("name = ?", "jinzhu").Count(&count) +//// SELECT count(*) FROM users WHERE name = 'jinzhu'; (count) + +db.Table("deleted_users").Count(&count) +//// SELECT count(*) FROM deleted_users; +``` + +## Pluck + +Get selected attributes as map + +```go +var ages []int64 +db.Find(&users).Pluck("age", &ages) + +var names []string +db.Model(&User{}).Pluck("name", &names) + +db.Table("deleted_users").Pluck("name", &names) + +// Requesting more than one column? Do it like this: +db.Select("name, age").Find(&users) +``` + +## Raw SQL + +```go +db.Exec("DROP TABLE users;") +db.Exec("UPDATE orders SET shipped_at=? WHERE id IN (?)", time.Now, []int64{11,22,33}) +``` + +## Row & Rows + +It is even possible to get query result as `*sql.Row` or `*sql.Rows` + +```go +row := db.Table("users").Where("name = ?", "jinzhu").Select("name, age").Row() // (*sql.Row) +row.Scan(&name, &age) + +rows, err := db.Model(User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() // (*sql.Rows, error) +defer rows.Close() +for rows.Next() { + ... + rows.Scan(&name, &age, &email) + ... +} + +// Raw SQL +rows, err := db.Raw("select name, age, email from users where name = ?", "jinzhu").Rows() // (*sql.Rows, error) +defer rows.Close() +for rows.Next() { + ... + rows.Scan(&name, &age, &email) + ... +} +``` + +## Scan + +Scan results into another struct. + +```go +type Result struct { + Name string + Age int +} + +var result Result +db.Table("users").Select("name, age").Where("name = ?", 3).Scan(&result) + +// Raw SQL +db.Raw("SELECT name, age FROM users WHERE name = ?", 3).Scan(&result) +``` + +## Group & Having + +```go +rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Rows() +for rows.Next() { + ... +} + +rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Rows() +for rows.Next() { + ... +} + +type Result struct { + Date time.Time + Total int64 +} +db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Scan(&results) +``` + +## Joins + +```go +rows, err := db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Rows() +for rows.Next() { + ... +} + +db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&results) + +// find a user by email address +db.Joins("inner join emails on emails.user_id = users.id").Where("emails.email = ?", "x@example.org").Find(&user) + +// find all email addresses for a user +db.Joins("left join users on users.id = emails.user_id").Where("users.name = ?", "jinzhu").Find(&emails) +``` + +## Transactions + +To perform a set of operations within a transaction, the general flow is as below. +The database handle returned from ``` db.Begin() ``` should be used for all operations within the transaction. +(Note that all individual save and delete operations are run in a transaction by default.) + +```go +// begin +tx := db.Begin() + +// do some database operations (use 'tx' from this point, not 'db') +tx.Create(...) +... + +// rollback in case of error +tx.Rollback() + +// Or commit if all is ok +tx.Commit() +``` + +### A Specific Example +``` +func CreateAnimals(db *gorm.DB) err { + tx := db.Begin() + // Note the use of tx as the database handle once you are within a transaction + + if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil { + tx.Rollback() + return err + } + + if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil { + tx.Rollback() + return err + } + + tx.Commit() + return nil +} +``` + +## Scopes + +```go +func AmountGreaterThan1000(db *gorm.DB) *gorm.DB { + return db.Where("amount > ?", 1000) +} + +func PaidWithCreditCard(db *gorm.DB) *gorm.DB { + return db.Where("pay_mode_sign = ?", "C") +} + +func PaidWithCod(db *gorm.DB) *gorm.DB { + return db.Where("pay_mode_sign = ?", "C") +} + +func OrderStatus(status []string) func (db *gorm.DB) *gorm.DB { + return func (db *gorm.DB) *gorm.DB { + return db.Scopes(AmountGreaterThan1000).Where("status in (?)", status) + } +} + +db.Scopes(AmountGreaterThan1000, PaidWithCreditCard).Find(&orders) +// Find all credit card orders and amount greater than 1000 + +db.Scopes(AmountGreaterThan1000, PaidWithCod).Find(&orders) +// Find all COD orders and amount greater than 1000 + +db.Scopes(OrderStatus([]string{"paid", "shipped"})).Find(&orders) +// Find all paid, shipped orders +``` + +## Callbacks + +Callbacks are methods defined on the pointer of struct. +If any callback returns an error, gorm will stop future operations and rollback all changes. + +Here is the list of all available callbacks: +(listed in the same order in which they will get called during the respective operations) + +### Creating An Object + +```go +BeforeSave +BeforeCreate +// save before associations +// save self +// save after associations +AfterCreate +AfterSave +``` +### Updating An Object + +```go +BeforeSave +BeforeUpdate +// save before associations +// save self +// save after associations +AfterUpdate +AfterSave +``` + +### Destroying An Object + +```go +BeforeDelete +// delete self +AfterDelete +``` + +### After Find + +```go +// load data from database +AfterFind +``` + +### Example + +```go +func (u *User) BeforeUpdate() (err error) { + if u.readonly() { + err = errors.New("read only user") + } + return +} + +// Rollback the insertion if user's id greater than 1000 +func (u *User) AfterCreate() (err error) { + if (u.Id > 1000) { + err = errors.New("user id is already greater than 1000") + } + return +} +``` + +As you know, save/delete operations in gorm are running in a transaction, +This is means if changes made in the transaction is not visiable unless it is commited, +So if you want to use those changes in your callbacks, you need to run SQL in same transaction. +Fortunately, gorm support pass transaction to callbacks as you needed, you could do it like this: + +```go +func (u *User) AfterCreate(tx *gorm.DB) (err error) { + tx.Model(u).Update("role", "admin") + return +} +``` + +## Specifying The Table Name + +```go +// Create `deleted_users` table with struct User's definition +db.Table("deleted_users").CreateTable(&User{}) + +var deleted_users []User +db.Table("deleted_users").Find(&deleted_users) +//// SELECT * FROM deleted_users; + +db.Table("deleted_users").Where("name = ?", "jinzhu").Delete() +//// DELETE FROM deleted_users WHERE name = 'jinzhu'; +``` + +### Specifying The Table Name For A Struct Permanently with TableName + +```go +type Cart struct { +} + +func (c Cart) TableName() string { + return "shopping_cart" +} + +func (u User) TableName() string { + if u.Role == "admin" { + return "admin_users" + } else { + return "users" + } +} +``` + +## Error Handling + +```go +query := db.Where("name = ?", "jinzhu").First(&user) +query := db.First(&user).Limit(10).Find(&users) +// query.Error will return the last happened error + +// So you could do error handing in your application like this: +if err := db.Where("name = ?", "jinzhu").First(&user).Error; err != nil { + // error handling... +} + +// RecordNotFound +// If no record found when you query data, gorm will return RecordNotFound error, you could check it like this: +db.Where("name = ?", "hello world").First(&User{}).Error == gorm.RecordNotFound +// Or use the shortcut method +db.Where("name = ?", "hello world").First(&user).RecordNotFound() + +if db.Model(&user).Related(&credit_card).RecordNotFound() { + // no credit card found error handling +} +``` + +## Logger + +Gorm has built-in logger support + +```go +// Enable Logger +db.LogMode(true) + +// Diable Logger +db.LogMode(false) + +// Debug a single operation +db.Debug().Where("name = ?", "jinzhu").First(&User{}) +``` + +![logger](https://raw.github.com/jinzhu/gorm/master/images/logger.png) + +### Customize Logger + +```go +// Refer gorm's default logger for how to: https://github.com/jinzhu/gorm/blob/master/logger.go#files +db.SetLogger(gorm.Logger{revel.TRACE}) +db.SetLogger(log.New(os.Stdout, "\r\n", 0)) +``` + +## Existing Schema + +If you have an existing database schema, and the primary key field is different from `id`, you can add a tag to the field structure to specify that this field is a primary key. + +```go +type Animal struct { + AnimalId int64 `gorm:"primary_key"` + Birthday time.Time `sql:"DEFAULT:current_timestamp"` + Name string `sql:"default:'galeone'"` + Age int64 +} +``` + +If your column names differ from the struct fields, you can specify them like this: + +```go +type Animal struct { + AnimalId int64 `gorm:"column:beast_id;primary_key"` + Birthday time.Time `gorm:"column:day_of_the_beast"` + Age int64 `gorm:"column:age_of_the_beast"` +} +``` + +## Composite Primary Key + +```go +type Product struct { + ID string `gorm:"primary_key"` + LanguageCode string `gorm:"primary_key"` +} +``` + +## Database Indexes & Foreign Key + +```go +// Add foreign key +// 1st param : foreignkey field +// 2nd param : destination table(id) +// 3rd param : ONDELETE +// 4th param : ONUPDATE +db.Model(&User{}).AddForeignKey("city_id", "cities(id)", "RESTRICT", "RESTRICT") + +// Add index +db.Model(&User{}).AddIndex("idx_user_name", "name") + +// Multiple column index +db.Model(&User{}).AddIndex("idx_user_name_age", "name", "age") + +// Add unique index +db.Model(&User{}).AddUniqueIndex("idx_user_name", "name") + +// Multiple column unique index +db.Model(&User{}).AddUniqueIndex("idx_user_name_age", "name", "age") + +// Remove index +db.Model(&User{}).RemoveIndex("idx_user_name") +``` + +## Default values + +If you have defined a default value in the `sql` tag (see the struct Animal above) the generated create/update SQl will ignore these fields if is set blank data. + +Eg. + +```go +db.Create(&Animal{Age: 99, Name: ""}) +``` + +The generated query will be: + +```sql +INSERT INTO animals("age") values('99'); +``` + +The same thing occurs in update statements. + +## More examples with query chain + +```go +db.First(&first_article).Count(&total_count).Limit(10).Find(&first_page_articles).Offset(10).Find(&second_page_articles) +//// SELECT * FROM articles LIMIT 1; (first_article) +//// SELECT count(*) FROM articles; (total_count) +//// SELECT * FROM articles LIMIT 10; (first_page_articles) +//// SELECT * FROM articles LIMIT 10 OFFSET 10; (second_page_articles) + + +db.Where("created_at > ?", "2013-10-10").Find(&cancelled_orders, "state = ?", "cancelled").Find(&shipped_orders, "state = ?", "shipped") +//// SELECT * FROM orders WHERE created_at > '2013/10/10' AND state = 'cancelled'; (cancelled_orders) +//// SELECT * FROM orders WHERE created_at > '2013/10/10' AND state = 'shipped'; (shipped_orders) + + +// Use variables to keep query chain +todays_orders := db.Where("created_at > ?", "2013-10-29") +cancelled_orders := todays_orders.Where("state = ?", "cancelled") +shipped_orders := todays_orders.Where("state = ?", "shipped") + + +// Search with shared conditions for different tables +db.Where("product_name = ?", "fancy_product").Find(&orders).Find(&shopping_carts) +//// SELECT * FROM orders WHERE product_name = 'fancy_product'; (orders) +//// SELECT * FROM carts WHERE product_name = 'fancy_product'; (shopping_carts) + + +// Search with shared conditions from different tables with specified table +db.Where("mail_type = ?", "TEXT").Find(&users1).Table("deleted_users").Find(&users2) +//// SELECT * FROM users WHERE mail_type = 'TEXT'; (users1) +//// SELECT * FROM deleted_users WHERE mail_type = 'TEXT'; (users2) + + +// FirstOrCreate example +db.Where("email = ?", "x@example.org").Attrs(User{RegisteredIp: "111.111.111.111"}).FirstOrCreate(&user) +//// SELECT * FROM users WHERE email = 'x@example.org'; +//// INSERT INTO "users" (email,registered_ip) VALUES ("x@example.org", "111.111.111.111") // if record not found +``` + +## TODO +* db.Select("Languages", "Name").Update(&user) + db.Omit("Languages").Update(&user) +* Auto migrate indexes +* Github Pages +* AlertColumn, DropColumn +* R/W Splitting, Validation + +# Author + +**jinzhu** + +* +* +* + +## License + +Released under the [MIT License](https://github.com/jinzhu/gorm/blob/master/License). + +[![GoDoc](https://godoc.org/github.com/jinzhu/gorm?status.png)](http://godoc.org/github.com/jinzhu/gorm) diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/association.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/association.go new file mode 100644 index 0000000..342dd6c --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/association.go @@ -0,0 +1,266 @@ +package gorm + +import ( + "errors" + "fmt" + "reflect" + "strings" +) + +type Association struct { + Scope *Scope + Column string + Error error + Field *Field +} + +func (association *Association) setErr(err error) *Association { + if err != nil { + association.Error = err + } + return association +} + +func (association *Association) Find(value interface{}) *Association { + association.Scope.related(value, association.Column) + return association.setErr(association.Scope.db.Error) +} + +func (association *Association) Append(values ...interface{}) *Association { + scope := association.Scope + field := association.Field + + for _, value := range values { + reflectvalue := reflect.Indirect(reflect.ValueOf(value)) + if reflectvalue.Kind() == reflect.Struct { + field.Set(reflect.Append(field.Field, reflectvalue)) + } else if reflectvalue.Kind() == reflect.Slice { + field.Set(reflect.AppendSlice(field.Field, reflectvalue)) + } else { + association.setErr(errors.New("invalid association type")) + } + } + scope.Search.Select(association.Column) + scope.callCallbacks(scope.db.parent.callback.updates) + return association.setErr(scope.db.Error) +} + +func (association *Association) Delete(values ...interface{}) *Association { + scope := association.Scope + relationship := association.Field.Relationship + + // many to many + if relationship.Kind == "many_to_many" { + query := scope.NewDB() + for idx, foreignKey := range relationship.ForeignDBNames { + if field, ok := scope.FieldByName(relationship.ForeignFieldNames[idx]); ok { + query = query.Where(fmt.Sprintf("%v = ?", scope.Quote(foreignKey)), field.Field.Interface()) + } + } + + primaryKeys := association.getPrimaryKeys(relationship.AssociationForeignFieldNames, values...) + sql := fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, relationship.AssociationForeignDBNames), toQueryMarks(primaryKeys)) + query = query.Where(sql, toQueryValues(primaryKeys)...) + + if err := relationship.JoinTableHandler.Delete(relationship.JoinTableHandler, query, relationship); err == nil { + leftValues := reflect.Zero(association.Field.Field.Type()) + for i := 0; i < association.Field.Field.Len(); i++ { + reflectValue := association.Field.Field.Index(i) + primaryKey := association.getPrimaryKeys(relationship.ForeignFieldNames, reflectValue.Interface())[0] + var included = false + for _, pk := range primaryKeys { + if equalAsString(primaryKey, pk) { + included = true + } + } + if !included { + leftValues = reflect.Append(leftValues, reflectValue) + } + } + association.Field.Set(leftValues) + } + } else { + association.setErr(errors.New("delete only support many to many")) + } + return association +} + +func (association *Association) Replace(values ...interface{}) *Association { + relationship := association.Field.Relationship + scope := association.Scope + if relationship.Kind == "many_to_many" { + field := association.Field.Field + + oldPrimaryKeys := association.getPrimaryKeys(relationship.AssociationForeignFieldNames, field.Interface()) + association.Field.Set(reflect.Zero(association.Field.Field.Type())) + association.Append(values...) + newPrimaryKeys := association.getPrimaryKeys(relationship.AssociationForeignFieldNames, field.Interface()) + + var addedPrimaryKeys = [][]interface{}{} + for _, newKey := range newPrimaryKeys { + hasEqual := false + for _, oldKey := range oldPrimaryKeys { + if equalAsString(newKey, oldKey) { + hasEqual = true + break + } + } + if !hasEqual { + addedPrimaryKeys = append(addedPrimaryKeys, newKey) + } + } + + for _, primaryKey := range association.getPrimaryKeys(relationship.AssociationForeignFieldNames, values...) { + addedPrimaryKeys = append(addedPrimaryKeys, primaryKey) + } + + if len(addedPrimaryKeys) > 0 { + query := scope.NewDB() + for idx, foreignKey := range relationship.ForeignDBNames { + if field, ok := scope.FieldByName(relationship.ForeignFieldNames[idx]); ok { + query = query.Where(fmt.Sprintf("%v = ?", scope.Quote(foreignKey)), field.Field.Interface()) + } + } + + sql := fmt.Sprintf("%v NOT IN (%v)", toQueryCondition(scope, relationship.AssociationForeignDBNames), toQueryMarks(addedPrimaryKeys)) + query = query.Where(sql, toQueryValues(addedPrimaryKeys)...) + association.setErr(relationship.JoinTableHandler.Delete(relationship.JoinTableHandler, query, relationship)) + } + } else { + association.setErr(errors.New("replace only support many to many")) + } + return association +} + +func (association *Association) Clear() *Association { + relationship := association.Field.Relationship + scope := association.Scope + if relationship.Kind == "many_to_many" { + query := scope.NewDB() + for idx, foreignKey := range relationship.ForeignDBNames { + if field, ok := scope.FieldByName(relationship.ForeignFieldNames[idx]); ok { + query = query.Where(fmt.Sprintf("%v = ?", scope.Quote(foreignKey)), field.Field.Interface()) + } + } + + if err := relationship.JoinTableHandler.Delete(relationship.JoinTableHandler, query, relationship); err == nil { + association.Field.Set(reflect.Zero(association.Field.Field.Type())) + } else { + association.setErr(err) + } + } else { + association.setErr(errors.New("clear only support many to many")) + } + return association +} + +func (association *Association) Count() int { + count := -1 + relationship := association.Field.Relationship + scope := association.Scope + newScope := scope.New(association.Field.Field.Interface()) + + if relationship.Kind == "many_to_many" { + relationship.JoinTableHandler.JoinWith(relationship.JoinTableHandler, scope.NewDB(), association.Scope.Value).Table(newScope.TableName()).Count(&count) + } else if relationship.Kind == "has_many" || relationship.Kind == "has_one" { + query := scope.DB() + for idx, foreignKey := range relationship.ForeignDBNames { + if field, ok := scope.FieldByName(relationship.AssociationForeignDBNames[idx]); ok { + query = query.Where(fmt.Sprintf("%v.%v = ?", newScope.QuotedTableName(), scope.Quote(foreignKey)), + field.Field.Interface()) + } + } + + if relationship.PolymorphicType != "" { + query = query.Where(fmt.Sprintf("%v.%v = ?", newScope.QuotedTableName(), newScope.Quote(relationship.PolymorphicDBName)), scope.TableName()) + } + query.Table(newScope.TableName()).Count(&count) + } else if relationship.Kind == "belongs_to" { + query := scope.DB() + for idx, foreignKey := range relationship.ForeignDBNames { + if field, ok := scope.FieldByName(relationship.AssociationForeignDBNames[idx]); ok { + query = query.Where(fmt.Sprintf("%v.%v = ?", newScope.QuotedTableName(), scope.Quote(foreignKey)), + field.Field.Interface()) + } + } + query.Table(newScope.TableName()).Count(&count) + } + + return count +} + +func (association *Association) getPrimaryKeys(columns []string, values ...interface{}) [][]interface{} { + results := [][]interface{}{} + scope := association.Scope + + for _, value := range values { + reflectValue := reflect.Indirect(reflect.ValueOf(value)) + if reflectValue.Kind() == reflect.Slice { + for i := 0; i < reflectValue.Len(); i++ { + primaryKeys := []interface{}{} + newScope := scope.New(reflectValue.Index(i).Interface()) + for _, column := range columns { + if field, ok := newScope.FieldByName(column); ok { + primaryKeys = append(primaryKeys, field.Field.Interface()) + } else { + primaryKeys = append(primaryKeys, "") + } + } + results = append(results, primaryKeys) + } + } else if reflectValue.Kind() == reflect.Struct { + newScope := scope.New(value) + var primaryKeys []interface{} + for _, column := range columns { + if field, ok := newScope.FieldByName(column); ok { + primaryKeys = append(primaryKeys, field.Field.Interface()) + } else { + primaryKeys = append(primaryKeys, "") + } + } + + results = append(results, primaryKeys) + } + } + return results +} + +func toQueryMarks(primaryValues [][]interface{}) string { + var results []string + + for _, primaryValue := range primaryValues { + var marks []string + for _,_ = range primaryValue { + marks = append(marks, "?") + } + + if len(marks) > 1 { + results = append(results, fmt.Sprintf("(%v)", strings.Join(marks, ","))) + } else { + results = append(results, strings.Join(marks, "")) + } + } + return strings.Join(results, ",") +} + +func toQueryCondition(scope *Scope, columns []string) string { + var newColumns []string + for _, column := range columns { + newColumns = append(newColumns, scope.Quote(column)) + } + + if len(columns) > 1 { + return fmt.Sprintf("(%v)", strings.Join(newColumns, ",")) + } else { + return strings.Join(columns, ",") + } +} + +func toQueryValues(primaryValues [][]interface{}) (values []interface{}) { + for _, primaryValue := range primaryValues { + for _, value := range primaryValue { + values = append(values, value) + } + } + return values +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/association_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/association_test.go new file mode 100644 index 0000000..dfda46a --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/association_test.go @@ -0,0 +1,263 @@ +package gorm_test + +import ( + "fmt" + "testing" +) + +func TestHasOneAndHasManyAssociation(t *testing.T) { + DB.DropTable(Category{}) + DB.DropTable(Post{}) + DB.DropTable(Comment{}) + + DB.CreateTable(Category{}) + DB.CreateTable(Post{}) + DB.CreateTable(Comment{}) + + post := Post{ + Title: "post 1", + Body: "body 1", + Comments: []*Comment{{Content: "Comment 1"}, {Content: "Comment 2"}}, + Category: Category{Name: "Category 1"}, + MainCategory: Category{Name: "Main Category 1"}, + } + + if err := DB.Save(&post).Error; err != nil { + t.Errorf("Got errors when save post", err.Error()) + } + + if err := DB.First(&Category{}, "name = ?", "Category 1").Error; err != nil { + t.Errorf("Category should be saved", err.Error()) + } + + var p Post + DB.First(&p, post.Id) + + if post.CategoryId.Int64 == 0 || p.CategoryId.Int64 == 0 || post.MainCategoryId == 0 || p.MainCategoryId == 0 { + t.Errorf("Category Id should exist") + } + + if DB.First(&Comment{}, "content = ?", "Comment 1").Error != nil { + t.Errorf("Comment 1 should be saved") + } + if post.Comments[0].PostId == 0 { + t.Errorf("Comment Should have post id") + } + + var comment Comment + if DB.First(&comment, "content = ?", "Comment 2").Error != nil { + t.Errorf("Comment 2 should be saved") + } + + if comment.PostId == 0 { + t.Errorf("Comment 2 Should have post id") + } + + comment3 := Comment{Content: "Comment 3", Post: Post{Title: "Title 3", Body: "Body 3"}} + DB.Save(&comment3) +} + +func TestRelated(t *testing.T) { + user := User{ + Name: "jinzhu", + BillingAddress: Address{Address1: "Billing Address - Address 1"}, + ShippingAddress: Address{Address1: "Shipping Address - Address 1"}, + Emails: []Email{{Email: "jinzhu@example.com"}, {Email: "jinzhu-2@example@example.com"}}, + CreditCard: CreditCard{Number: "1234567890"}, + Company: Company{Name: "company1"}, + } + + DB.Save(&user) + + if user.CreditCard.ID == 0 { + t.Errorf("After user save, credit card should have id") + } + + if user.BillingAddress.ID == 0 { + t.Errorf("After user save, billing address should have id") + } + + if user.Emails[0].Id == 0 { + t.Errorf("After user save, billing address should have id") + } + + var emails []Email + DB.Model(&user).Related(&emails) + if len(emails) != 2 { + t.Errorf("Should have two emails") + } + + var emails2 []Email + DB.Model(&user).Where("email = ?", "jinzhu@example.com").Related(&emails2) + if len(emails2) != 1 { + t.Errorf("Should have two emails") + } + + var user1 User + DB.Model(&user).Related(&user1.Emails) + if len(user1.Emails) != 2 { + t.Errorf("Should have only one email match related condition") + } + + var address1 Address + DB.Model(&user).Related(&address1, "BillingAddressId") + if address1.Address1 != "Billing Address - Address 1" { + t.Errorf("Should get billing address from user correctly") + } + + user1 = User{} + DB.Model(&address1).Related(&user1, "BillingAddressId") + if DB.NewRecord(user1) { + t.Errorf("Should get user from address correctly") + } + + var user2 User + DB.Model(&emails[0]).Related(&user2) + if user2.Id != user.Id || user2.Name != user.Name { + t.Errorf("Should get user from email correctly") + } + + var creditcard CreditCard + var user3 User + DB.First(&creditcard, "number = ?", "1234567890") + DB.Model(&creditcard).Related(&user3) + if user3.Id != user.Id || user3.Name != user.Name { + t.Errorf("Should get user from credit card correctly") + } + + if !DB.Model(&CreditCard{}).Related(&User{}).RecordNotFound() { + t.Errorf("RecordNotFound for Related") + } + + var company Company + if DB.Model(&user).Related(&company, "Company").RecordNotFound() || company.Name != "company1" { + t.Errorf("RecordNotFound for Related") + } +} + +func TestManyToMany(t *testing.T) { + DB.Raw("delete from languages") + var languages = []Language{{Name: "ZH"}, {Name: "EN"}} + user := User{Name: "Many2Many", Languages: languages} + DB.Save(&user) + + // Query + var newLanguages []Language + DB.Model(&user).Related(&newLanguages, "Languages") + if len(newLanguages) != len([]string{"ZH", "EN"}) { + t.Errorf("Query many to many relations") + } + + DB.Model(&user).Association("Languages").Find(&newLanguages) + if len(newLanguages) != len([]string{"ZH", "EN"}) { + t.Errorf("Should be able to find many to many relations") + } + + if DB.Model(&user).Association("Languages").Count() != len([]string{"ZH", "EN"}) { + t.Errorf("Count should return correct result") + } + + // Append + DB.Model(&user).Association("Languages").Append(&Language{Name: "DE"}) + if DB.Where("name = ?", "DE").First(&Language{}).RecordNotFound() { + t.Errorf("New record should be saved when append") + } + + languageA := Language{Name: "AA"} + DB.Save(&languageA) + DB.Model(&User{Id: user.Id}).Association("Languages").Append(languageA) + + languageC := Language{Name: "CC"} + DB.Save(&languageC) + DB.Model(&user).Association("Languages").Append(&[]Language{{Name: "BB"}, languageC}) + + DB.Model(&User{Id: user.Id}).Association("Languages").Append(&[]Language{{Name: "DD"}, {Name: "EE"}}) + + totalLanguages := []string{"ZH", "EN", "DE", "AA", "BB", "CC", "DD", "EE"} + + if DB.Model(&user).Association("Languages").Count() != len(totalLanguages) { + t.Errorf("All appended languages should be saved") + } + + // Delete + user.Languages = []Language{} + DB.Model(&user).Association("Languages").Find(&user.Languages) + + var language Language + DB.Where("name = ?", "EE").First(&language) + DB.Model(&user).Association("Languages").Delete(language, &language) + + if DB.Model(&user).Association("Languages").Count() != len(totalLanguages)-1 || len(user.Languages) != len(totalLanguages)-1 { + t.Errorf("Relations should be deleted with Delete") + } + if DB.Where("name = ?", "EE").First(&Language{}).RecordNotFound() { + t.Errorf("Language EE should not be deleted") + } + + DB.Where("name IN (?)", []string{"CC", "DD"}).Find(&languages) + + user2 := User{Name: "Many2Many_User2", Languages: languages} + DB.Save(&user2) + + DB.Model(&user).Association("Languages").Delete(languages, &languages) + if DB.Model(&user).Association("Languages").Count() != len(totalLanguages)-3 || len(user.Languages) != len(totalLanguages)-3 { + t.Errorf("Relations should be deleted with Delete") + } + + if DB.Model(&user2).Association("Languages").Count() == 0 { + t.Errorf("Other user's relations should not be deleted") + } + + // Replace + var languageB Language + DB.Where("name = ?", "BB").First(&languageB) + DB.Model(&user).Association("Languages").Replace(languageB) + if len(user.Languages) != 1 || DB.Model(&user).Association("Languages").Count() != 1 { + t.Errorf("Relations should be replaced") + } + + DB.Model(&user).Association("Languages").Replace(&[]Language{{Name: "FF"}, {Name: "JJ"}}) + if len(user.Languages) != 2 || DB.Model(&user).Association("Languages").Count() != len([]string{"FF", "JJ"}) { + t.Errorf("Relations should be replaced") + } + + // Clear + DB.Model(&user).Association("Languages").Clear() + if len(user.Languages) != 0 || DB.Model(&user).Association("Languages").Count() != 0 { + t.Errorf("Relations should be cleared") + } +} + +func TestForeignKey(t *testing.T) { + for _, structField := range DB.NewScope(&User{}).GetStructFields() { + for _, foreignKey := range []string{"BillingAddressID", "ShippingAddressId", "CompanyID"} { + if structField.Name == foreignKey && !structField.IsForeignKey { + t.Errorf(fmt.Sprintf("%v should be foreign key", foreignKey)) + } + } + } + + for _, structField := range DB.NewScope(&Email{}).GetStructFields() { + for _, foreignKey := range []string{"UserId"} { + if structField.Name == foreignKey && !structField.IsForeignKey { + t.Errorf(fmt.Sprintf("%v should be foreign key", foreignKey)) + } + } + } + + for _, structField := range DB.NewScope(&Post{}).GetStructFields() { + for _, foreignKey := range []string{"CategoryId", "MainCategoryId"} { + if structField.Name == foreignKey && !structField.IsForeignKey { + t.Errorf(fmt.Sprintf("%v should be foreign key", foreignKey)) + } + } + } + + for _, structField := range DB.NewScope(&Comment{}).GetStructFields() { + for _, foreignKey := range []string{"PostId"} { + if structField.Name == foreignKey && !structField.IsForeignKey { + t.Errorf(fmt.Sprintf("%v should be foreign key", foreignKey)) + } + } + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/callback.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/callback.go new file mode 100644 index 0000000..603e511 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/callback.go @@ -0,0 +1,200 @@ +package gorm + +import ( + "fmt" +) + +type callback struct { + creates []*func(scope *Scope) + updates []*func(scope *Scope) + deletes []*func(scope *Scope) + queries []*func(scope *Scope) + rowQueries []*func(scope *Scope) + processors []*callbackProcessor +} + +type callbackProcessor struct { + name string + before string + after string + replace bool + remove bool + typ string + processor *func(scope *Scope) + callback *callback +} + +func (c *callback) addProcessor(typ string) *callbackProcessor { + cp := &callbackProcessor{typ: typ, callback: c} + c.processors = append(c.processors, cp) + return cp +} + +func (c *callback) clone() *callback { + return &callback{ + creates: c.creates, + updates: c.updates, + deletes: c.deletes, + queries: c.queries, + processors: c.processors, + } +} + +func (c *callback) Create() *callbackProcessor { + return c.addProcessor("create") +} + +func (c *callback) Update() *callbackProcessor { + return c.addProcessor("update") +} + +func (c *callback) Delete() *callbackProcessor { + return c.addProcessor("delete") +} + +func (c *callback) Query() *callbackProcessor { + return c.addProcessor("query") +} + +func (c *callback) RowQuery() *callbackProcessor { + return c.addProcessor("row_query") +} + +func (cp *callbackProcessor) Before(name string) *callbackProcessor { + cp.before = name + return cp +} + +func (cp *callbackProcessor) After(name string) *callbackProcessor { + cp.after = name + return cp +} + +func (cp *callbackProcessor) Register(name string, fc func(scope *Scope)) { + cp.name = name + cp.processor = &fc + cp.callback.sort() +} + +func (cp *callbackProcessor) Remove(name string) { + fmt.Printf("[info] removing callback `%v` from %v\n", name, fileWithLineNum()) + cp.name = name + cp.remove = true + cp.callback.sort() +} + +func (cp *callbackProcessor) Replace(name string, fc func(scope *Scope)) { + fmt.Printf("[info] replacing callback `%v` from %v\n", name, fileWithLineNum()) + cp.name = name + cp.processor = &fc + cp.replace = true + cp.callback.sort() +} + +func getRIndex(strs []string, str string) int { + for i := len(strs) - 1; i >= 0; i-- { + if strs[i] == str { + return i + } + } + return -1 +} + +func sortProcessors(cps []*callbackProcessor) []*func(scope *Scope) { + var sortCallbackProcessor func(c *callbackProcessor) + var names, sortedNames = []string{}, []string{} + + for _, cp := range cps { + if index := getRIndex(names, cp.name); index > -1 { + if !cp.replace && !cp.remove { + fmt.Printf("[warning] duplicated callback `%v` from %v\n", cp.name, fileWithLineNum()) + } + } + names = append(names, cp.name) + } + + sortCallbackProcessor = func(c *callbackProcessor) { + if getRIndex(sortedNames, c.name) > -1 { + return + } + + if len(c.before) > 0 { + if index := getRIndex(sortedNames, c.before); index > -1 { + sortedNames = append(sortedNames[:index], append([]string{c.name}, sortedNames[index:]...)...) + } else if index := getRIndex(names, c.before); index > -1 { + sortedNames = append(sortedNames, c.name) + sortCallbackProcessor(cps[index]) + } else { + sortedNames = append(sortedNames, c.name) + } + } + + if len(c.after) > 0 { + if index := getRIndex(sortedNames, c.after); index > -1 { + sortedNames = append(sortedNames[:index+1], append([]string{c.name}, sortedNames[index+1:]...)...) + } else if index := getRIndex(names, c.after); index > -1 { + cp := cps[index] + if len(cp.before) == 0 { + cp.before = c.name + } + sortCallbackProcessor(cp) + } else { + sortedNames = append(sortedNames, c.name) + } + } + + if getRIndex(sortedNames, c.name) == -1 { + sortedNames = append(sortedNames, c.name) + } + } + + for _, cp := range cps { + sortCallbackProcessor(cp) + } + + var funcs = []*func(scope *Scope){} + var sortedFuncs = []*func(scope *Scope){} + for _, name := range sortedNames { + index := getRIndex(names, name) + if !cps[index].remove { + sortedFuncs = append(sortedFuncs, cps[index].processor) + } + } + + for _, cp := range cps { + if sindex := getRIndex(sortedNames, cp.name); sindex == -1 { + if !cp.remove { + funcs = append(funcs, cp.processor) + } + } + } + + return append(sortedFuncs, funcs...) +} + +func (c *callback) sort() { + var creates, updates, deletes, queries, rowQueries []*callbackProcessor + + for _, processor := range c.processors { + switch processor.typ { + case "create": + creates = append(creates, processor) + case "update": + updates = append(updates, processor) + case "delete": + deletes = append(deletes, processor) + case "query": + queries = append(queries, processor) + case "row_query": + rowQueries = append(rowQueries, processor) + } + } + + c.creates = sortProcessors(creates) + c.updates = sortProcessors(updates) + c.deletes = sortProcessors(deletes) + c.queries = sortProcessors(queries) + c.rowQueries = sortProcessors(rowQueries) +} + +var DefaultCallback = &callback{processors: []*callbackProcessor{}} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/callback_create.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/callback_create.go new file mode 100644 index 0000000..bded532 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/callback_create.go @@ -0,0 +1,112 @@ +package gorm + +import ( + "fmt" + "strings" +) + +func BeforeCreate(scope *Scope) { + scope.CallMethodWithErrorCheck("BeforeSave") + scope.CallMethodWithErrorCheck("BeforeCreate") +} + +func UpdateTimeStampWhenCreate(scope *Scope) { + if !scope.HasError() { + now := NowFunc() + scope.SetColumn("CreatedAt", now) + scope.SetColumn("UpdatedAt", now) + } +} + +func Create(scope *Scope) { + defer scope.Trace(NowFunc()) + + if !scope.HasError() { + // set create sql + var sqls, columns []string + fields := scope.Fields() + for _, field := range fields { + if scope.changeableField(field) { + if field.IsNormal { + if !field.IsPrimaryKey || (field.IsPrimaryKey && !field.IsBlank) { + if !field.IsBlank || !field.HasDefaultValue { + columns = append(columns, scope.Quote(field.DBName)) + sqls = append(sqls, scope.AddToVars(field.Field.Interface())) + } + } + } else if relationship := field.Relationship; relationship != nil && relationship.Kind == "belongs_to" { + for _, dbName := range relationship.ForeignDBNames { + if relationField := fields[dbName]; !scope.changeableField(relationField) { + columns = append(columns, scope.Quote(relationField.DBName)) + sqls = append(sqls, scope.AddToVars(relationField.Field.Interface())) + } + } + } + } + } + + returningKey := "*" + primaryField := scope.PrimaryField() + if primaryField != nil { + returningKey = scope.Quote(primaryField.DBName) + } + + if len(columns) == 0 { + scope.Raw(fmt.Sprintf("INSERT INTO %v DEFAULT VALUES %v", + scope.QuotedTableName(), + scope.Dialect().ReturningStr(scope.TableName(), returningKey), + )) + } else { + scope.Raw(fmt.Sprintf( + "INSERT INTO %v (%v) VALUES (%v) %v", + scope.QuotedTableName(), + strings.Join(columns, ","), + strings.Join(sqls, ","), + scope.Dialect().ReturningStr(scope.TableName(), returningKey), + )) + } + + // execute create sql + if scope.Dialect().SupportLastInsertId() { + if result, err := scope.SqlDB().Exec(scope.Sql, scope.SqlVars...); scope.Err(err) == nil { + id, err := result.LastInsertId() + if scope.Err(err) == nil { + scope.db.RowsAffected, _ = result.RowsAffected() + if primaryField != nil && primaryField.IsBlank { + scope.Err(scope.SetColumn(primaryField, id)) + } + } + } + } else { + if primaryField == nil { + if results, err := scope.SqlDB().Exec(scope.Sql, scope.SqlVars...); err == nil { + scope.db.RowsAffected, _ = results.RowsAffected() + } else { + scope.Err(err) + } + } else { + if err := scope.Err(scope.SqlDB().QueryRow(scope.Sql, scope.SqlVars...).Scan(primaryField.Field.Addr().Interface())); err == nil { + scope.db.RowsAffected = 1 + } else { + scope.Err(err) + } + } + } + } +} + +func AfterCreate(scope *Scope) { + scope.CallMethodWithErrorCheck("AfterCreate") + scope.CallMethodWithErrorCheck("AfterSave") +} + +func init() { + DefaultCallback.Create().Register("gorm:begin_transaction", BeginTransaction) + DefaultCallback.Create().Register("gorm:before_create", BeforeCreate) + DefaultCallback.Create().Register("gorm:save_before_associations", SaveBeforeAssociations) + DefaultCallback.Create().Register("gorm:update_time_stamp_when_create", UpdateTimeStampWhenCreate) + DefaultCallback.Create().Register("gorm:create", Create) + DefaultCallback.Create().Register("gorm:save_after_associations", SaveAfterAssociations) + DefaultCallback.Create().Register("gorm:after_create", AfterCreate) + DefaultCallback.Create().Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/callback_delete.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/callback_delete.go new file mode 100644 index 0000000..7223665 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/callback_delete.go @@ -0,0 +1,36 @@ +package gorm + +import "fmt" + +func BeforeDelete(scope *Scope) { + scope.CallMethodWithErrorCheck("BeforeDelete") +} + +func Delete(scope *Scope) { + if !scope.HasError() { + if !scope.Search.Unscoped && scope.HasColumn("DeletedAt") { + scope.Raw( + fmt.Sprintf("UPDATE %v SET deleted_at=%v %v", + scope.QuotedTableName(), + scope.AddToVars(NowFunc()), + scope.CombinedConditionSql(), + )) + } else { + scope.Raw(fmt.Sprintf("DELETE FROM %v %v", scope.QuotedTableName(), scope.CombinedConditionSql())) + } + + scope.Exec() + } +} + +func AfterDelete(scope *Scope) { + scope.CallMethodWithErrorCheck("AfterDelete") +} + +func init() { + DefaultCallback.Delete().Register("gorm:begin_transaction", BeginTransaction) + DefaultCallback.Delete().Register("gorm:before_delete", BeforeDelete) + DefaultCallback.Delete().Register("gorm:delete", Delete) + DefaultCallback.Delete().Register("gorm:after_delete", AfterDelete) + DefaultCallback.Delete().Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/callback_query.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/callback_query.go new file mode 100644 index 0000000..387e813 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/callback_query.go @@ -0,0 +1,118 @@ +package gorm + +import ( + "errors" + "fmt" + "reflect" +) + +func Query(scope *Scope) { + defer scope.Trace(NowFunc()) + + var ( + isSlice bool + isPtr bool + anyRecordFound bool + destType reflect.Type + ) + + if orderBy, ok := scope.Get("gorm:order_by_primary_key"); ok { + if primaryKey := scope.PrimaryKey(); primaryKey != "" { + scope.Search.Order(fmt.Sprintf("%v.%v %v", scope.QuotedTableName(), scope.Quote(primaryKey), orderBy)) + } + } + + var dest = scope.IndirectValue() + if value, ok := scope.Get("gorm:query_destination"); ok { + dest = reflect.Indirect(reflect.ValueOf(value)) + } + + if kind := dest.Kind(); kind == reflect.Slice { + isSlice = true + destType = dest.Type().Elem() + dest.Set(reflect.MakeSlice(dest.Type(), 0, 0)) + + if destType.Kind() == reflect.Ptr { + isPtr = true + destType = destType.Elem() + } + } else if kind != reflect.Struct { + scope.Err(errors.New("unsupported destination, should be slice or struct")) + return + } + + scope.prepareQuerySql() + + if !scope.HasError() { + rows, err := scope.SqlDB().Query(scope.Sql, scope.SqlVars...) + scope.db.RowsAffected = 0 + + if scope.Err(err) != nil { + return + } + defer rows.Close() + + columns, _ := rows.Columns() + for rows.Next() { + scope.db.RowsAffected++ + + anyRecordFound = true + elem := dest + if isSlice { + elem = reflect.New(destType).Elem() + } + + var values = make([]interface{}, len(columns)) + + fields := scope.New(elem.Addr().Interface()).Fields() + + for index, column := range columns { + if field, ok := fields[column]; ok { + if field.Field.Kind() == reflect.Ptr { + values[index] = field.Field.Addr().Interface() + } else { + values[index] = reflect.New(reflect.PtrTo(field.Field.Type())).Interface() + } + } else { + var value interface{} + values[index] = &value + } + } + + scope.Err(rows.Scan(values...)) + + for index, column := range columns { + value := values[index] + if field, ok := fields[column]; ok { + if field.Field.Kind() == reflect.Ptr { + field.Field.Set(reflect.ValueOf(value).Elem()) + } else if v := reflect.ValueOf(value).Elem().Elem(); v.IsValid() { + field.Field.Set(v) + } + } + } + + if isSlice { + if isPtr { + dest.Set(reflect.Append(dest, elem.Addr())) + } else { + dest.Set(reflect.Append(dest, elem)) + } + } + } + + if !anyRecordFound && !isSlice { + scope.Err(RecordNotFound) + } + } +} + +func AfterQuery(scope *Scope) { + scope.CallMethodWithErrorCheck("AfterFind") +} + +func init() { + DefaultCallback.Query().Register("gorm:query", Query) + DefaultCallback.Query().Register("gorm:after_query", AfterQuery) + DefaultCallback.Query().Register("gorm:preload", Preload) +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/callback_shared.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/callback_shared.go new file mode 100644 index 0000000..547059e --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/callback_shared.go @@ -0,0 +1,91 @@ +package gorm + +import "reflect" + +func BeginTransaction(scope *Scope) { + scope.Begin() +} + +func CommitOrRollbackTransaction(scope *Scope) { + scope.CommitOrRollback() +} + +func SaveBeforeAssociations(scope *Scope) { + if !scope.shouldSaveAssociations() { + return + } + for _, field := range scope.Fields() { + if scope.changeableField(field) && !field.IsBlank && !field.IsIgnored { + if relationship := field.Relationship; relationship != nil && relationship.Kind == "belongs_to" { + value := field.Field + scope.Err(scope.NewDB().Save(value.Addr().Interface()).Error) + if len(relationship.ForeignFieldNames) != 0 { + for idx, fieldName := range relationship.ForeignFieldNames { + associationForeignName := relationship.AssociationForeignDBNames[idx] + if f, ok := scope.New(value.Addr().Interface()).FieldByName(associationForeignName); ok { + scope.Err(scope.SetColumn(fieldName, f.Field.Interface())) + } + } + } + } + } + } +} + +func SaveAfterAssociations(scope *Scope) { + if !scope.shouldSaveAssociations() { + return + } + for _, field := range scope.Fields() { + if scope.changeableField(field) && !field.IsBlank && !field.IsIgnored { + if relationship := field.Relationship; relationship != nil && + (relationship.Kind == "has_one" || relationship.Kind == "has_many" || relationship.Kind == "many_to_many") { + value := field.Field + + switch value.Kind() { + case reflect.Slice: + for i := 0; i < value.Len(); i++ { + newDB := scope.NewDB() + elem := value.Index(i).Addr().Interface() + newScope := newDB.NewScope(elem) + + if relationship.JoinTableHandler == nil && len(relationship.ForeignFieldNames) != 0 { + for idx, fieldName := range relationship.ForeignFieldNames { + associationForeignName := relationship.AssociationForeignDBNames[idx] + if f, ok := scope.FieldByName(associationForeignName); ok { + scope.Err(newScope.SetColumn(fieldName, f.Field.Interface())) + } + } + } + + if relationship.PolymorphicType != "" { + scope.Err(newScope.SetColumn(relationship.PolymorphicType, scope.TableName())) + } + + scope.Err(newDB.Save(elem).Error) + + if joinTableHandler := relationship.JoinTableHandler; joinTableHandler != nil { + scope.Err(joinTableHandler.Add(joinTableHandler, scope.NewDB(), scope.Value, newScope.Value)) + } + } + default: + elem := value.Addr().Interface() + newScope := scope.New(elem) + if len(relationship.ForeignFieldNames) != 0 { + for idx, fieldName := range relationship.ForeignFieldNames { + associationForeignName := relationship.AssociationForeignDBNames[idx] + if f, ok := scope.FieldByName(associationForeignName); ok { + scope.Err(newScope.SetColumn(fieldName, f.Field.Interface())) + } + } + } + + if relationship.PolymorphicType != "" { + scope.Err(newScope.SetColumn(relationship.PolymorphicType, scope.TableName())) + } + scope.Err(scope.NewDB().Save(elem).Error) + } + } + } + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/callback_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/callback_test.go new file mode 100644 index 0000000..b416d6a --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/callback_test.go @@ -0,0 +1,112 @@ +package gorm + +import ( + "reflect" + "runtime" + "strings" + "testing" +) + +func equalFuncs(funcs []*func(s *Scope), fnames []string) bool { + var names []string + for _, f := range funcs { + fnames := strings.Split(runtime.FuncForPC(reflect.ValueOf(*f).Pointer()).Name(), ".") + names = append(names, fnames[len(fnames)-1]) + } + return reflect.DeepEqual(names, fnames) +} + +func create(s *Scope) {} +func beforeCreate1(s *Scope) {} +func beforeCreate2(s *Scope) {} +func afterCreate1(s *Scope) {} +func afterCreate2(s *Scope) {} + +func TestRegisterCallback(t *testing.T) { + var callback = &callback{processors: []*callbackProcessor{}} + + callback.Create().Register("before_create1", beforeCreate1) + callback.Create().Register("before_create2", beforeCreate2) + callback.Create().Register("create", create) + callback.Create().Register("after_create1", afterCreate1) + callback.Create().Register("after_create2", afterCreate2) + + if !equalFuncs(callback.creates, []string{"beforeCreate1", "beforeCreate2", "create", "afterCreate1", "afterCreate2"}) { + t.Errorf("register callback") + } +} + +func TestRegisterCallbackWithOrder(t *testing.T) { + var callback1 = &callback{processors: []*callbackProcessor{}} + callback1.Create().Register("before_create1", beforeCreate1) + callback1.Create().Register("create", create) + callback1.Create().Register("after_create1", afterCreate1) + callback1.Create().Before("after_create1").Register("after_create2", afterCreate2) + if !equalFuncs(callback1.creates, []string{"beforeCreate1", "create", "afterCreate2", "afterCreate1"}) { + t.Errorf("register callback with order") + } + + var callback2 = &callback{processors: []*callbackProcessor{}} + + callback2.Update().Register("create", create) + callback2.Update().Before("create").Register("before_create1", beforeCreate1) + callback2.Update().After("after_create2").Register("after_create1", afterCreate1) + callback2.Update().Before("before_create1").Register("before_create2", beforeCreate2) + callback2.Update().Register("after_create2", afterCreate2) + + if !equalFuncs(callback2.updates, []string{"beforeCreate2", "beforeCreate1", "create", "afterCreate2", "afterCreate1"}) { + t.Errorf("register callback with order") + } +} + +func TestRegisterCallbackWithComplexOrder(t *testing.T) { + var callback1 = &callback{processors: []*callbackProcessor{}} + + callback1.Query().Before("after_create1").After("before_create1").Register("create", create) + callback1.Query().Register("before_create1", beforeCreate1) + callback1.Query().Register("after_create1", afterCreate1) + + if !equalFuncs(callback1.queries, []string{"beforeCreate1", "create", "afterCreate1"}) { + t.Errorf("register callback with order") + } + + var callback2 = &callback{processors: []*callbackProcessor{}} + + callback2.Delete().Before("after_create1").After("before_create1").Register("create", create) + callback2.Delete().Before("create").Register("before_create1", beforeCreate1) + callback2.Delete().After("before_create1").Register("before_create2", beforeCreate2) + callback2.Delete().Register("after_create1", afterCreate1) + callback2.Delete().After("after_create1").Register("after_create2", afterCreate2) + + if !equalFuncs(callback2.deletes, []string{"beforeCreate1", "beforeCreate2", "create", "afterCreate1", "afterCreate2"}) { + t.Errorf("register callback with order") + } +} + +func replaceCreate(s *Scope) {} + +func TestReplaceCallback(t *testing.T) { + var callback = &callback{processors: []*callbackProcessor{}} + + callback.Create().Before("after_create1").After("before_create1").Register("create", create) + callback.Create().Register("before_create1", beforeCreate1) + callback.Create().Register("after_create1", afterCreate1) + callback.Create().Replace("create", replaceCreate) + + if !equalFuncs(callback.creates, []string{"beforeCreate1", "replaceCreate", "afterCreate1"}) { + t.Errorf("replace callback") + } +} + +func TestRemoveCallback(t *testing.T) { + var callback = &callback{processors: []*callbackProcessor{}} + + callback.Create().Before("after_create1").After("before_create1").Register("create", create) + callback.Create().Register("before_create1", beforeCreate1) + callback.Create().Register("after_create1", afterCreate1) + callback.Create().Remove("create") + + if !equalFuncs(callback.creates, []string{"beforeCreate1", "afterCreate1"}) { + t.Errorf("remove callback") + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/callback_update.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/callback_update.go new file mode 100644 index 0000000..6090ee6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/callback_update.go @@ -0,0 +1,97 @@ +package gorm + +import ( + "fmt" + "strings" +) + +func AssignUpdateAttributes(scope *Scope) { + if attrs, ok := scope.InstanceGet("gorm:update_interface"); ok { + if maps := convertInterfaceToMap(attrs); len(maps) > 0 { + protected, ok := scope.Get("gorm:ignore_protected_attrs") + _, updateColumn := scope.Get("gorm:update_column") + updateAttrs, hasUpdate := scope.updatedAttrsWithValues(maps, ok && protected.(bool)) + + if updateColumn { + scope.InstanceSet("gorm:update_attrs", maps) + } else if len(updateAttrs) > 0 { + scope.InstanceSet("gorm:update_attrs", updateAttrs) + } else if !hasUpdate { + scope.SkipLeft() + return + } + } + } +} + +func BeforeUpdate(scope *Scope) { + if _, ok := scope.Get("gorm:update_column"); !ok { + scope.CallMethodWithErrorCheck("BeforeSave") + scope.CallMethodWithErrorCheck("BeforeUpdate") + } +} + +func UpdateTimeStampWhenUpdate(scope *Scope) { + if _, ok := scope.Get("gorm:update_column"); !ok { + scope.SetColumn("UpdatedAt", NowFunc()) + } +} + +func Update(scope *Scope) { + if !scope.HasError() { + var sqls []string + + if updateAttrs, ok := scope.InstanceGet("gorm:update_attrs"); ok { + for key, value := range updateAttrs.(map[string]interface{}) { + if scope.changeableDBColumn(key) { + sqls = append(sqls, fmt.Sprintf("%v = %v", scope.Quote(key), scope.AddToVars(value))) + } + } + } else { + fields := scope.Fields() + for _, field := range fields { + if scope.changeableField(field) && !field.IsPrimaryKey && field.IsNormal { + if !field.IsBlank || !field.HasDefaultValue { + sqls = append(sqls, fmt.Sprintf("%v = %v", scope.Quote(field.DBName), scope.AddToVars(field.Field.Interface()))) + } + } else if relationship := field.Relationship; relationship != nil && relationship.Kind == "belongs_to" { + for _, dbName := range relationship.ForeignDBNames { + if relationField := fields[dbName]; !scope.changeableField(relationField) && !relationField.IsBlank { + sql := fmt.Sprintf("%v = %v", scope.Quote(relationField.DBName), scope.AddToVars(relationField.Field.Interface())) + sqls = append(sqls, sql) + } + } + } + } + } + + if len(sqls) > 0 { + scope.Raw(fmt.Sprintf( + "UPDATE %v SET %v %v", + scope.QuotedTableName(), + strings.Join(sqls, ", "), + scope.CombinedConditionSql(), + )) + scope.Exec() + } + } +} + +func AfterUpdate(scope *Scope) { + if _, ok := scope.Get("gorm:update_column"); !ok { + scope.CallMethodWithErrorCheck("AfterUpdate") + scope.CallMethodWithErrorCheck("AfterSave") + } +} + +func init() { + DefaultCallback.Update().Register("gorm:assign_update_attributes", AssignUpdateAttributes) + DefaultCallback.Update().Register("gorm:begin_transaction", BeginTransaction) + DefaultCallback.Update().Register("gorm:before_update", BeforeUpdate) + DefaultCallback.Update().Register("gorm:save_before_associations", SaveBeforeAssociations) + DefaultCallback.Update().Register("gorm:update_time_stamp_when_update", UpdateTimeStampWhenUpdate) + DefaultCallback.Update().Register("gorm:update", Update) + DefaultCallback.Update().Register("gorm:save_after_associations", SaveAfterAssociations) + DefaultCallback.Update().Register("gorm:after_update", AfterUpdate) + DefaultCallback.Update().Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction) +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/callbacks_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/callbacks_test.go new file mode 100644 index 0000000..a58913d --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/callbacks_test.go @@ -0,0 +1,177 @@ +package gorm_test + +import ( + "errors" + + "github.com/jinzhu/gorm" + + "reflect" + "testing" +) + +func (s *Product) BeforeCreate() (err error) { + if s.Code == "Invalid" { + err = errors.New("invalid product") + } + s.BeforeCreateCallTimes = s.BeforeCreateCallTimes + 1 + return +} + +func (s *Product) BeforeUpdate() (err error) { + if s.Code == "dont_update" { + err = errors.New("can't update") + } + s.BeforeUpdateCallTimes = s.BeforeUpdateCallTimes + 1 + return +} + +func (s *Product) BeforeSave() (err error) { + if s.Code == "dont_save" { + err = errors.New("can't save") + } + s.BeforeSaveCallTimes = s.BeforeSaveCallTimes + 1 + return +} + +func (s *Product) AfterFind() { + s.AfterFindCallTimes = s.AfterFindCallTimes + 1 +} + +func (s *Product) AfterCreate(tx *gorm.DB) { + tx.Model(s).UpdateColumn(Product{AfterCreateCallTimes: s.AfterCreateCallTimes + 1}) +} + +func (s *Product) AfterUpdate() { + s.AfterUpdateCallTimes = s.AfterUpdateCallTimes + 1 +} + +func (s *Product) AfterSave() (err error) { + if s.Code == "after_save_error" { + err = errors.New("can't save") + } + s.AfterSaveCallTimes = s.AfterSaveCallTimes + 1 + return +} + +func (s *Product) BeforeDelete() (err error) { + if s.Code == "dont_delete" { + err = errors.New("can't delete") + } + s.BeforeDeleteCallTimes = s.BeforeDeleteCallTimes + 1 + return +} + +func (s *Product) AfterDelete() (err error) { + if s.Code == "after_delete_error" { + err = errors.New("can't delete") + } + s.AfterDeleteCallTimes = s.AfterDeleteCallTimes + 1 + return +} + +func (s *Product) GetCallTimes() []int64 { + return []int64{s.BeforeCreateCallTimes, s.BeforeSaveCallTimes, s.BeforeUpdateCallTimes, s.AfterCreateCallTimes, s.AfterSaveCallTimes, s.AfterUpdateCallTimes, s.BeforeDeleteCallTimes, s.AfterDeleteCallTimes, s.AfterFindCallTimes} +} + +func TestRunCallbacks(t *testing.T) { + p := Product{Code: "unique_code", Price: 100} + DB.Save(&p) + + if !reflect.DeepEqual(p.GetCallTimes(), []int64{1, 1, 0, 1, 1, 0, 0, 0, 0}) { + t.Errorf("Callbacks should be invoked successfully, %v", p.GetCallTimes()) + } + + DB.Where("Code = ?", "unique_code").First(&p) + if !reflect.DeepEqual(p.GetCallTimes(), []int64{1, 1, 0, 1, 0, 0, 0, 0, 1}) { + t.Errorf("After callbacks values are not saved, %v", p.GetCallTimes()) + } + + p.Price = 200 + DB.Save(&p) + if !reflect.DeepEqual(p.GetCallTimes(), []int64{1, 2, 1, 1, 1, 1, 0, 0, 1}) { + t.Errorf("After update callbacks should be invoked successfully, %v", p.GetCallTimes()) + } + + var products []Product + DB.Find(&products, "code = ?", "unique_code") + if products[0].AfterFindCallTimes != 2 { + t.Errorf("AfterFind callbacks should work with slice") + } + + DB.Where("Code = ?", "unique_code").First(&p) + if !reflect.DeepEqual(p.GetCallTimes(), []int64{1, 2, 1, 1, 0, 0, 0, 0, 2}) { + t.Errorf("After update callbacks values are not saved, %v", p.GetCallTimes()) + } + + DB.Delete(&p) + if !reflect.DeepEqual(p.GetCallTimes(), []int64{1, 2, 1, 1, 0, 0, 1, 1, 2}) { + t.Errorf("After delete callbacks should be invoked successfully, %v", p.GetCallTimes()) + } + + if DB.Where("Code = ?", "unique_code").First(&p).Error == nil { + t.Errorf("Can't find a deleted record") + } +} + +func TestCallbacksWithErrors(t *testing.T) { + p := Product{Code: "Invalid", Price: 100} + if DB.Save(&p).Error == nil { + t.Errorf("An error from before create callbacks happened when create with invalid value") + } + + if DB.Where("code = ?", "Invalid").First(&Product{}).Error == nil { + t.Errorf("Should not save record that have errors") + } + + if DB.Save(&Product{Code: "dont_save", Price: 100}).Error == nil { + t.Errorf("An error from after create callbacks happened when create with invalid value") + } + + p2 := Product{Code: "update_callback", Price: 100} + DB.Save(&p2) + + p2.Code = "dont_update" + if DB.Save(&p2).Error == nil { + t.Errorf("An error from before update callbacks happened when update with invalid value") + } + + if DB.Where("code = ?", "update_callback").First(&Product{}).Error != nil { + t.Errorf("Record Should not be updated due to errors happened in before update callback") + } + + if DB.Where("code = ?", "dont_update").First(&Product{}).Error == nil { + t.Errorf("Record Should not be updated due to errors happened in before update callback") + } + + p2.Code = "dont_save" + if DB.Save(&p2).Error == nil { + t.Errorf("An error from before save callbacks happened when update with invalid value") + } + + p3 := Product{Code: "dont_delete", Price: 100} + DB.Save(&p3) + if DB.Delete(&p3).Error == nil { + t.Errorf("An error from before delete callbacks happened when delete") + } + + if DB.Where("Code = ?", "dont_delete").First(&p3).Error != nil { + t.Errorf("An error from before delete callbacks happened") + } + + p4 := Product{Code: "after_save_error", Price: 100} + DB.Save(&p4) + if err := DB.First(&Product{}, "code = ?", "after_save_error").Error; err == nil { + t.Errorf("Record should be reverted if get an error in after save callback") + } + + p5 := Product{Code: "after_delete_error", Price: 100} + DB.Save(&p5) + if err := DB.First(&Product{}, "code = ?", "after_delete_error").Error; err != nil { + t.Errorf("Record should be found") + } + + DB.Delete(&p5) + if err := DB.First(&Product{}, "code = ?", "after_delete_error").Error; err != nil { + t.Errorf("Record shouldn't be deleted because of an error happened in after delete callback") + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/common_dialect.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/common_dialect.go new file mode 100644 index 0000000..19708e5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/common_dialect.go @@ -0,0 +1,114 @@ +package gorm + +import ( + "fmt" + "reflect" + "time" +) + +type commonDialect struct{} + +func (commonDialect) BinVar(i int) string { + return "$$" // ? +} + +func (commonDialect) SupportLastInsertId() bool { + return true +} + +func (commonDialect) HasTop() bool { + return false +} + +func (commonDialect) SqlTag(value reflect.Value, size int, autoIncrease bool) string { + switch value.Kind() { + case reflect.Bool: + return "BOOLEAN" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + if autoIncrease { + return "INTEGER AUTO_INCREMENT" + } + return "INTEGER" + case reflect.Int64, reflect.Uint64: + if autoIncrease { + return "BIGINT AUTO_INCREMENT" + } + return "BIGINT" + case reflect.Float32, reflect.Float64: + return "FLOAT" + case reflect.String: + if size > 0 && size < 65532 { + return fmt.Sprintf("VARCHAR(%d)", size) + } + return "VARCHAR(65532)" + case reflect.Struct: + if _, ok := value.Interface().(time.Time); ok { + return "TIMESTAMP" + } + default: + if _, ok := value.Interface().([]byte); ok { + if size > 0 && size < 65532 { + return fmt.Sprintf("BINARY(%d)", size) + } + return "BINARY(65532)" + } + } + panic(fmt.Sprintf("invalid sql type %s (%s) for commonDialect", value.Type().Name(), value.Kind().String())) +} + +func (commonDialect) ReturningStr(tableName, key string) string { + return "" +} + +func (commonDialect) SelectFromDummyTable() string { + return "" +} + +func (commonDialect) Quote(key string) string { + return fmt.Sprintf(`"%s"`, key) +} + +func (c commonDialect) HasTable(scope *Scope, tableName string) bool { + var ( + count int + databaseName = c.CurrentDatabase(scope) + ) + c.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.TABLES WHERE table_name = ? AND table_schema = ?", tableName, databaseName) + return count > 0 +} + +func (c commonDialect) HasColumn(scope *Scope, tableName string, columnName string) bool { + var ( + count int + databaseName = c.CurrentDatabase(scope) + ) + c.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = ? AND table_name = ? AND column_name = ?", databaseName, tableName, columnName) + return count > 0 +} + +func (c commonDialect) HasIndex(scope *Scope, tableName string, indexName string) bool { + var count int + c.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.STATISTICS where table_name = ? AND index_name = ?", tableName, indexName) + return count > 0 +} + +func (commonDialect) RemoveIndex(scope *Scope, indexName string) { + scope.Err(scope.NewDB().Exec(fmt.Sprintf("DROP INDEX %v ON %v", indexName, scope.QuotedTableName())).Error) +} + +// RawScanInt scans the first column of the first row into the `scan' int pointer. +// This function captures raw query errors and propagates them to the original scope. +func (commonDialect) RawScanInt(scope *Scope, scanPtr *int, query string, args ...interface{}) { + scope.Err(scope.NewDB().Raw(query, args...).Row().Scan(scanPtr)) +} + +// RawScanString scans the first column of the first row into the `scan' string pointer. +// This function captures raw query errors and propagates them to the original scope. +func (commonDialect) RawScanString(scope *Scope, scanPtr *string, query string, args ...interface{}) { + scope.Err(scope.NewDB().Raw(query, args...).Row().Scan(scanPtr)) +} + +func (commonDialect) CurrentDatabase(scope *Scope) (name string) { + scope.Err(scope.NewDB().Raw("SELECT DATABASE()").Row().Scan(&name)) + return +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/create_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/create_test.go new file mode 100644 index 0000000..9717598 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/create_test.go @@ -0,0 +1,159 @@ +package gorm_test + +import ( + "reflect" + "testing" + "time" +) + +func TestCreate(t *testing.T) { + float := 35.03554004971999 + user := User{Name: "CreateUser", Age: 18, Birthday: time.Now(), UserNum: Num(111), PasswordHash: []byte{'f', 'a', 'k', '4'}, Latitude: float} + + if !DB.NewRecord(user) || !DB.NewRecord(&user) { + t.Error("User should be new record before create") + } + + if count := DB.Save(&user).RowsAffected; count != 1 { + t.Error("There should be one record be affected when create record") + } + + if DB.NewRecord(user) || DB.NewRecord(&user) { + t.Error("User should not new record after save") + } + + var newUser User + DB.First(&newUser, user.Id) + + if !reflect.DeepEqual(newUser.PasswordHash, []byte{'f', 'a', 'k', '4'}) { + t.Errorf("User's PasswordHash should be saved ([]byte)") + } + + if newUser.Age != 18 { + t.Errorf("User's Age should be saved (int)") + } + + if newUser.UserNum != Num(111) { + t.Errorf("User's UserNum should be saved (custom type)") + } + + if newUser.Latitude != float { + t.Errorf("Float64 should not be changed after save") + } + + if user.CreatedAt.IsZero() { + t.Errorf("Should have created_at after create") + } + + if newUser.CreatedAt.IsZero() { + t.Errorf("Should have created_at after create") + } + + DB.Model(user).Update("name", "create_user_new_name") + DB.First(&user, user.Id) + if user.CreatedAt != newUser.CreatedAt { + t.Errorf("CreatedAt should not be changed after update") + } +} + +func TestCreateWithNoGORMPrimayKey(t *testing.T) { + jt := JoinTable{From: 1, To: 2} + err := DB.Create(&jt).Error + if err != nil { + t.Errorf("No error should happen when create a record without a GORM primary key. But in the database this primary key exists and is the union of 2 or more fields\n But got: %s", err) + } +} + +func TestCreateWithNoStdPrimaryKeyAndDefaultValues(t *testing.T) { + animal := Animal{Name: "Ferdinand"} + if DB.Save(&animal).Error != nil { + t.Errorf("No error should happen when create a record without std primary key") + } + + if animal.Counter == 0 { + t.Errorf("No std primary key should be filled value after create") + } + + if animal.Name != "Ferdinand" { + t.Errorf("Default value should be overrided") + } + + // Test create with default value not overrided + an := Animal{From: "nerdz"} + + if DB.Save(&an).Error != nil { + t.Errorf("No error should happen when create an record without std primary key") + } + + // We must fetch the value again, to have the default fields updated + // (We can't do this in the update statements, since sql default can be expressions + // And be different from the fields' type (eg. a time.Time fiels has a default value of "now()" + DB.Model(Animal{}).Where(&Animal{Counter: an.Counter}).First(&an) + + if an.Name != "galeone" { + t.Errorf("Default value should fill the field. But got %v", an.Name) + } +} + +func TestAnonymousScanner(t *testing.T) { + user := User{Name: "anonymous_scanner", Role: Role{Name: "admin"}} + DB.Save(&user) + + var user2 User + DB.First(&user2, "name = ?", "anonymous_scanner") + if user2.Role.Name != "admin" { + t.Errorf("Should be able to get anonymous scanner") + } + + if !user2.IsAdmin() { + t.Errorf("Should be able to get anonymous scanner") + } +} + +func TestAnonymousField(t *testing.T) { + user := User{Name: "anonymous_field", Company: Company{Name: "company"}} + DB.Save(&user) + + var user2 User + DB.First(&user2, "name = ?", "anonymous_field") + DB.Model(&user2).Related(&user2.Company) + if user2.Company.Name != "company" { + t.Errorf("Should be able to get anonymous field") + } +} + +func TestSelectWithCreate(t *testing.T) { + user := getPreparedUser("select_user", "select_with_create") + DB.Select("Name", "BillingAddress", "CreditCard", "Company", "Emails").Create(user) + + var queryuser User + DB.Preload("BillingAddress").Preload("ShippingAddress"). + Preload("CreditCard").Preload("Emails").Preload("Company").First(&queryuser, user.Id) + + if queryuser.Name != user.Name || queryuser.Age == user.Age { + t.Errorf("Should only create users with name column") + } + + if queryuser.BillingAddressID.Int64 == 0 || queryuser.ShippingAddressId != 0 || + queryuser.CreditCard.ID == 0 || len(queryuser.Emails) == 0 { + t.Errorf("Should only create selected relationships") + } +} + +func TestOmitWithCreate(t *testing.T) { + user := getPreparedUser("omit_user", "omit_with_create") + DB.Omit("Name", "BillingAddress", "CreditCard", "Company", "Emails").Create(user) + + var queryuser User + DB.Preload("BillingAddress").Preload("ShippingAddress"). + Preload("CreditCard").Preload("Emails").Preload("Company").First(&queryuser, user.Id) + + if queryuser.Name == user.Name || queryuser.Age != user.Age { + t.Errorf("Should only create users with age column") + } + + if queryuser.BillingAddressID.Int64 != 0 || queryuser.ShippingAddressId == 0 || + queryuser.CreditCard.ID != 0 || len(queryuser.Emails) != 0 { + t.Errorf("Should not create omited relationships") + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/customize_column_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/customize_column_test.go new file mode 100644 index 0000000..cf4f1d1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/customize_column_test.go @@ -0,0 +1,65 @@ +package gorm_test + +import ( + "testing" + "time" +) + +type CustomizeColumn struct { + ID int64 `gorm:"column:mapped_id; primary_key:yes"` + Name string `gorm:"column:mapped_name"` + Date time.Time `gorm:"column:mapped_time"` +} + +// Make sure an ignored field does not interfere with another field's custom +// column name that matches the ignored field. +type CustomColumnAndIgnoredFieldClash struct { + Body string `sql:"-"` + RawBody string `gorm:"column:body"` +} + +func TestCustomizeColumn(t *testing.T) { + col := "mapped_name" + DB.DropTable(&CustomizeColumn{}) + DB.AutoMigrate(&CustomizeColumn{}) + + scope := DB.NewScope(&CustomizeColumn{}) + if !scope.Dialect().HasColumn(scope, scope.TableName(), col) { + t.Errorf("CustomizeColumn should have column %s", col) + } + + col = "mapped_id" + if scope.PrimaryKey() != col { + t.Errorf("CustomizeColumn should have primary key %s, but got %q", col, scope.PrimaryKey()) + } + + expected := "foo" + cc := CustomizeColumn{ID: 666, Name: expected, Date: time.Now()} + + if count := DB.Create(&cc).RowsAffected; count != 1 { + t.Error("There should be one record be affected when create record") + } + + var cc1 CustomizeColumn + DB.First(&cc1, 666) + + if cc1.Name != expected { + t.Errorf("Failed to query CustomizeColumn") + } + + cc.Name = "bar" + DB.Save(&cc) + + var cc2 CustomizeColumn + DB.First(&cc2, 666) + if cc2.Name != "bar" { + t.Errorf("Failed to query CustomizeColumn") + } +} + +func TestCustomColumnAndIgnoredFieldClash(t *testing.T) { + DB.DropTable(&CustomColumnAndIgnoredFieldClash{}) + if err := DB.AutoMigrate(&CustomColumnAndIgnoredFieldClash{}).Error; err != nil { + t.Errorf("Should not raise error: %s", err) + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/ddl_errors_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/ddl_errors_test.go new file mode 100644 index 0000000..aca5955 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/ddl_errors_test.go @@ -0,0 +1,24 @@ +package gorm_test + +import ( + "testing" +) + +func TestDdlErrors(t *testing.T) { + var err error + + if err = DB.Close(); err != nil { + t.Errorf("Closing DDL test db connection err=%s", err) + } + defer func() { + // Reopen DB connection. + if DB, err = OpenTestConnection(); err != nil { + t.Fatalf("Failed re-opening db connection: %s", err) + } + }() + + DB.HasTable("foobarbaz") + if DB.Error == nil { + t.Errorf("Expected operation on closed db to produce an error, but err was nil") + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/delete_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/delete_test.go new file mode 100644 index 0000000..e0c7166 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/delete_test.go @@ -0,0 +1,68 @@ +package gorm_test + +import ( + "testing" + "time" +) + +func TestDelete(t *testing.T) { + user1, user2 := User{Name: "delete1"}, User{Name: "delete2"} + DB.Save(&user1) + DB.Save(&user2) + + if err := DB.Delete(&user1).Error; err != nil { + t.Errorf("No error should happen when delete a record, err=%s", err) + } + + if !DB.Where("name = ?", user1.Name).First(&User{}).RecordNotFound() { + t.Errorf("User can't be found after delete") + } + + if DB.Where("name = ?", user2.Name).First(&User{}).RecordNotFound() { + t.Errorf("Other users that not deleted should be found-able") + } +} + +func TestInlineDelete(t *testing.T) { + user1, user2 := User{Name: "inline_delete1"}, User{Name: "inline_delete2"} + DB.Save(&user1) + DB.Save(&user2) + + if DB.Delete(&User{}, user1.Id).Error != nil { + t.Errorf("No error should happen when delete a record") + } else if !DB.Where("name = ?", user1.Name).First(&User{}).RecordNotFound() { + t.Errorf("User can't be found after delete") + } + + if err := DB.Delete(&User{}, "name = ?", user2.Name).Error; err != nil { + t.Errorf("No error should happen when delete a record, err=%s", err) + } else if !DB.Where("name = ?", user2.Name).First(&User{}).RecordNotFound() { + t.Errorf("User can't be found after delete") + } +} + +func TestSoftDelete(t *testing.T) { + type User struct { + Id int64 + Name string + DeletedAt time.Time + } + DB.AutoMigrate(&User{}) + + user := User{Name: "soft_delete"} + DB.Save(&user) + DB.Delete(&user) + + if DB.First(&User{}, "name = ?", user.Name).Error == nil { + t.Errorf("Can't find a soft deleted record") + } + + if err := DB.Unscoped().First(&User{}, "name = ?", user.Name).Error; err != nil { + t.Errorf("Should be able to find soft deleted record with Unscoped, but err=%s", err) + } + + DB.Unscoped().Delete(&user) + if !DB.Unscoped().First(&User{}, "name = ?", user.Name).RecordNotFound() { + t.Errorf("Can't find permanently deleted record") + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/dialect.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/dialect.go new file mode 100644 index 0000000..926f8a1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/dialect.go @@ -0,0 +1,41 @@ +package gorm + +import ( + "fmt" + "reflect" +) + +type Dialect interface { + BinVar(i int) string + SupportLastInsertId() bool + HasTop() bool + SqlTag(value reflect.Value, size int, autoIncrease bool) string + ReturningStr(tableName, key string) string + SelectFromDummyTable() string + Quote(key string) string + HasTable(scope *Scope, tableName string) bool + HasColumn(scope *Scope, tableName string, columnName string) bool + HasIndex(scope *Scope, tableName string, indexName string) bool + RemoveIndex(scope *Scope, indexName string) + CurrentDatabase(scope *Scope) string +} + +func NewDialect(driver string) Dialect { + var d Dialect + switch driver { + case "postgres": + d = &postgres{} + case "foundation": + d = &foundation{} + case "mysql": + d = &mysql{} + case "sqlite3": + d = &sqlite3{} + case "mssql": + d = &mssql{} + default: + fmt.Printf("`%v` is not officially supported, running under compatibility mode.\n", driver) + d = &commonDialect{} + } + return d +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/doc/development.md b/Godeps/_workspace/src/github.com/jinzhu/gorm/doc/development.md new file mode 100644 index 0000000..0816666 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/doc/development.md @@ -0,0 +1,68 @@ +# Gorm Development + +## Architecture + +The most notable component of Gorm is`gorm.DB`, which hold database connection. It could be initialized like this: + + db, err := gorm.Open("postgres", "user=gorm dbname=gorm sslmode=disable") + +Gorm has chainable API, `gorm.DB` is the bridge of chains, it save related information and pass it to the next chain. + +Lets use below code to explain how it works: + + db.Where("name = ?", "jinzhu").Find(&users) + + // equivalent code + newdb := db.Where("name =?", "jinzhu") + newdb.Find(&user) + +`newdb` is `db`'s clone, in addition, it contains search conditions from the `Where` method. +`Find` is a query method, it creates a `Scope` instance, and pass it as argument to query callbacks. + +There are four kinds of callbacks corresponds to sql's CURD: create callbacks, update callbacks, query callbacks, delete callbacks. + +## Callbacks + +### Register a new callback + + func updateCreated(scope *Scope) { + if scope.HasColumn("Created") { + scope.SetColumn("Created", NowFunc()) + } + } + + db.Callback().Create().Register("update_created_at", updateCreated) + // register a callback for Create process + +### Delete an existing callback + + db.Callback().Create().Remove("gorm:create") + // delete callback `gorm:create` from Create callbacks + +### Replace an existing callback + + db.Callback().Create().Replace("gorm:create", newCreateFunction) + // replace callback `gorm:create` with new function `newCreateFunction` for Create process + +### Register callback orders + + db.Callback().Create().Before("gorm:create").Register("update_created_at", updateCreated) + db.Callback().Create().After("gorm:create").Register("update_created_at", updateCreated) + db.Callback().Query().After("gorm:query").Register("my_plugin:after_query", afterQuery) + db.Callback().Delete().After("gorm:delete").Register("my_plugin:after_delete", afterDelete) + db.Callback().Update().Before("gorm:update").Register("my_plugin:before_update", beforeUpdate) + db.Callback().Create().Before("gorm:create").After("gorm:before_create").Register("my_plugin:before_create", beforeCreate) + +### Callback API + +Gorm is powered by callbacks, so you could refer below links to learn how to write callbacks + +[Create callbacks](https://github.com/jinzhu/gorm/blob/master/callback_create.go) + +[Update callbacks](https://github.com/jinzhu/gorm/blob/master/callback_update.go) + +[Query callbacks](https://github.com/jinzhu/gorm/blob/master/callback_query.go) + +[Delete callbacks](https://github.com/jinzhu/gorm/blob/master/callback_delete.go) + +View [https://github.com/jinzhu/gorm/blob/master/scope.go](https://github.com/jinzhu/gorm/blob/master/scope.go) for all available API diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/embedded_struct_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/embedded_struct_test.go new file mode 100644 index 0000000..7be75d9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/embedded_struct_test.go @@ -0,0 +1,48 @@ +package gorm_test + +import "testing" + +type BasePost struct { + Id int64 + Title string + URL string +} + +type HNPost struct { + BasePost + Upvotes int32 +} + +type EngadgetPost struct { + BasePost BasePost `gorm:"embedded"` + ImageUrl string +} + +func TestSaveAndQueryEmbeddedStruct(t *testing.T) { + DB.Save(&HNPost{BasePost: BasePost{Title: "news"}}) + DB.Save(&HNPost{BasePost: BasePost{Title: "hn_news"}}) + var news HNPost + if err := DB.First(&news, "title = ?", "hn_news").Error; err != nil { + t.Errorf("no error should happen when query with embedded struct, but got %v", err) + } else if news.Title != "hn_news" { + t.Errorf("embedded struct's value should be scanned correctly") + } + + DB.Save(&EngadgetPost{BasePost: BasePost{Title: "engadget_news"}}) + var egNews EngadgetPost + if err := DB.First(&egNews, "title = ?", "engadget_news").Error; err != nil { + t.Errorf("no error should happen when query with embedded struct, but got %v", err) + } else if egNews.BasePost.Title != "engadget_news" { + t.Errorf("embedded struct's value should be scanned correctly") + } + + if DB.NewScope(&HNPost{}).PrimaryField() == nil { + t.Errorf("primary key with embedded struct should works") + } + + for _, field := range DB.NewScope(&HNPost{}).Fields() { + if field.Name == "BasePost" { + t.Errorf("scope Fields should not contain embedded struct") + } + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/errors.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/errors.go new file mode 100644 index 0000000..23fedd7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/errors.go @@ -0,0 +1,38 @@ +package gorm + +import ( + "errors" + "strings" +) + +var ( + RecordNotFound = errors.New("record not found") + InvalidSql = errors.New("invalid sql") + NoNewAttrs = errors.New("no new attributes") + NoValidTransaction = errors.New("no valid transaction") + CantStartTransaction = errors.New("can't start transaction") +) + +type errorsInterface interface { + GetErrors() []error +} + +type Errors struct { + errors []error +} + +func (errs Errors) GetErrors() []error { + return errs.errors +} + +func (errs *Errors) Add(err error) { + errs.errors = append(errs.errors, err) +} + +func (errs Errors) Error() string { + var errors = []string{} + for _, e := range errs.errors { + errors = append(errors, e.Error()) + } + return strings.Join(errors, "; ") +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/field.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/field.go new file mode 100644 index 0000000..8f5efa6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/field.go @@ -0,0 +1,84 @@ +package gorm + +import ( + "database/sql" + "errors" + "reflect" +) + +type Field struct { + *StructField + IsBlank bool + Field reflect.Value +} + +func (field *Field) Set(value interface{}) error { + if !field.Field.IsValid() { + return errors.New("field value not valid") + } + + if !field.Field.CanAddr() { + return errors.New("unaddressable value") + } + + if rvalue, ok := value.(reflect.Value); ok { + value = rvalue.Interface() + } + + if scanner, ok := field.Field.Addr().Interface().(sql.Scanner); ok { + if v, ok := value.(reflect.Value); ok { + if err := scanner.Scan(v.Interface()); err != nil { + return err + } + } else { + if err := scanner.Scan(value); err != nil { + return err + } + } + } else { + reflectValue, ok := value.(reflect.Value) + if !ok { + reflectValue = reflect.ValueOf(value) + } + + if reflectValue.Type().ConvertibleTo(field.Field.Type()) { + field.Field.Set(reflectValue.Convert(field.Field.Type())) + } else { + return errors.New("could not convert argument") + } + } + + field.IsBlank = isBlank(field.Field) + return nil +} + +// Fields get value's fields +func (scope *Scope) Fields() map[string]*Field { + if scope.fields == nil { + fields := map[string]*Field{} + structFields := scope.GetStructFields() + + indirectValue := scope.IndirectValue() + isStruct := indirectValue.Kind() == reflect.Struct + for _, structField := range structFields { + if isStruct { + fields[structField.DBName] = getField(indirectValue, structField) + } else { + fields[structField.DBName] = &Field{StructField: structField, IsBlank: true} + } + } + + scope.fields = fields + } + return scope.fields +} + +func getField(indirectValue reflect.Value, structField *StructField) *Field { + field := &Field{StructField: structField} + for _, name := range structField.Names { + indirectValue = reflect.Indirect(indirectValue).FieldByName(name) + } + field.Field = indirectValue + field.IsBlank = isBlank(indirectValue) + return field +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/foundation.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/foundation.go new file mode 100644 index 0000000..07968ad --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/foundation.go @@ -0,0 +1,83 @@ +package gorm + +import ( + "fmt" + "reflect" + "time" +) + +type foundation struct { + commonDialect +} + +func (foundation) BinVar(i int) string { + return fmt.Sprintf("$%v", i) +} + +func (foundation) SupportLastInsertId() bool { + return false +} + +func (foundation) SqlTag(value reflect.Value, size int, autoIncrease bool) string { + switch value.Kind() { + case reflect.Bool: + return "boolean" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + if autoIncrease { + return "serial" + } + return "int" + case reflect.Int64, reflect.Uint64: + if autoIncrease { + return "bigserial" + } + return "bigint" + case reflect.Float32, reflect.Float64: + return "double" + case reflect.String: + if size > 0 && size < 65532 { + return fmt.Sprintf("varchar(%d)", size) + } + return "clob" + case reflect.Struct: + if _, ok := value.Interface().(time.Time); ok { + return "datetime" + } + default: + if _, ok := value.Interface().([]byte); ok { + return "blob" + } + } + panic(fmt.Sprintf("invalid sql type %s (%s) for foundation", value.Type().Name(), value.Kind().String())) +} + +func (s foundation) ReturningStr(tableName, key string) string { + return fmt.Sprintf("RETURNING %v.%v", s.Quote(tableName), key) +} + +func (s foundation) HasTable(scope *Scope, tableName string) bool { + var count int + s.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.tables WHERE table_schema = current_schema AND table_type = 'TABLE' AND table_name = ?", tableName) + return count > 0 +} + +func (s foundation) HasColumn(scope *Scope, tableName string, columnName string) bool { + var count int + s.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.columns WHERE table_schema = current_schema AND table_name = ? AND column_name = ?", tableName, columnName) + return count > 0 +} + +func (s foundation) RemoveIndex(scope *Scope, indexName string) { + scope.NewDB().Exec(fmt.Sprintf("DROP INDEX %v", s.Quote(indexName))) +} + +func (s foundation) HasIndex(scope *Scope, tableName string, indexName string) bool { + var count int + s.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.indexes WHERE table_schema = current_schema AND table_name = ? AND index_name = ?", tableName, indexName) + return count > 0 +} + +func (s foundation) CurrentDatabase(scope *Scope) (name string) { + s.RawScanString(scope, &name, "SELECT CURRENT_SCHEMA") + return +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/images/logger.png b/Godeps/_workspace/src/github.com/jinzhu/gorm/images/logger.png new file mode 100644 index 0000000..8c46588 Binary files /dev/null and b/Godeps/_workspace/src/github.com/jinzhu/gorm/images/logger.png differ diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/interface.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/interface.go new file mode 100644 index 0000000..7b02aa6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/interface.go @@ -0,0 +1,19 @@ +package gorm + +import "database/sql" + +type sqlCommon interface { + Exec(query string, args ...interface{}) (sql.Result, error) + Prepare(query string) (*sql.Stmt, error) + Query(query string, args ...interface{}) (*sql.Rows, error) + QueryRow(query string, args ...interface{}) *sql.Row +} + +type sqlDb interface { + Begin() (*sql.Tx, error) +} + +type sqlTx interface { + Commit() error + Rollback() error +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/join_table_handler.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/join_table_handler.go new file mode 100644 index 0000000..10e1e84 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/join_table_handler.go @@ -0,0 +1,155 @@ +package gorm + +import ( + "errors" + "fmt" + "reflect" + "strings" +) + +type JoinTableHandlerInterface interface { + Setup(relationship *Relationship, tableName string, source reflect.Type, destination reflect.Type) + Table(db *DB) string + Add(handler JoinTableHandlerInterface, db *DB, source interface{}, destination interface{}) error + Delete(handler JoinTableHandlerInterface, db *DB, sources ...interface{}) error + JoinWith(handler JoinTableHandlerInterface, db *DB, source interface{}) *DB + SourceForeignKeys() []JoinTableForeignKey + DestinationForeignKeys() []JoinTableForeignKey +} + +type JoinTableForeignKey struct { + DBName string + AssociationDBName string +} + +type JoinTableSource struct { + ModelType reflect.Type + ForeignKeys []JoinTableForeignKey +} + +type JoinTableHandler struct { + TableName string `sql:"-"` + Source JoinTableSource `sql:"-"` + Destination JoinTableSource `sql:"-"` +} + +func (s *JoinTableHandler) SourceForeignKeys() []JoinTableForeignKey { + return s.Source.ForeignKeys +} + +func (s *JoinTableHandler) DestinationForeignKeys() []JoinTableForeignKey { + return s.Destination.ForeignKeys +} + +func (s *JoinTableHandler) Setup(relationship *Relationship, tableName string, source reflect.Type, destination reflect.Type) { + s.TableName = tableName + + s.Source = JoinTableSource{ModelType: source} + for idx, dbName := range relationship.ForeignFieldNames { + s.Source.ForeignKeys = append(s.Source.ForeignKeys, JoinTableForeignKey{ + DBName: relationship.ForeignDBNames[idx], + AssociationDBName: dbName, + }) + } + + s.Destination = JoinTableSource{ModelType: destination} + for idx, dbName := range relationship.AssociationForeignFieldNames { + s.Destination.ForeignKeys = append(s.Destination.ForeignKeys, JoinTableForeignKey{ + DBName: relationship.AssociationForeignDBNames[idx], + AssociationDBName: dbName, + }) + } +} + +func (s JoinTableHandler) Table(db *DB) string { + return s.TableName +} + +func (s JoinTableHandler) GetSearchMap(db *DB, sources ...interface{}) map[string]interface{} { + values := map[string]interface{}{} + + for _, source := range sources { + scope := db.NewScope(source) + modelType := scope.GetModelStruct().ModelType + + if s.Source.ModelType == modelType { + for _, foreignKey := range s.Source.ForeignKeys { + values[foreignKey.DBName] = scope.Fields()[foreignKey.AssociationDBName].Field.Interface() + } + } else if s.Destination.ModelType == modelType { + for _, foreignKey := range s.Destination.ForeignKeys { + values[foreignKey.DBName] = scope.Fields()[foreignKey.AssociationDBName].Field.Interface() + } + } + } + return values +} + +func (s JoinTableHandler) Add(handler JoinTableHandlerInterface, db *DB, source1 interface{}, source2 interface{}) error { + scope := db.NewScope("") + searchMap := s.GetSearchMap(db, source1, source2) + + var assignColumns, binVars, conditions []string + var values []interface{} + for key, value := range searchMap { + assignColumns = append(assignColumns, key) + binVars = append(binVars, `?`) + conditions = append(conditions, fmt.Sprintf("%v = ?", scope.Quote(key))) + values = append(values, value) + } + + for _, value := range values { + values = append(values, value) + } + + quotedTable := handler.Table(db) + sql := fmt.Sprintf( + "INSERT INTO %v (%v) SELECT %v %v WHERE NOT EXISTS (SELECT * FROM %v WHERE %v)", + quotedTable, + strings.Join(assignColumns, ","), + strings.Join(binVars, ","), + scope.Dialect().SelectFromDummyTable(), + quotedTable, + strings.Join(conditions, " AND "), + ) + + return db.Exec(sql, values...).Error +} + +func (s JoinTableHandler) Delete(handler JoinTableHandlerInterface, db *DB, sources ...interface{}) error { + var conditions []string + var values []interface{} + + for key, value := range s.GetSearchMap(db, sources...) { + conditions = append(conditions, fmt.Sprintf("%v = ?", key)) + values = append(values, value) + } + + return db.Table(handler.Table(db)).Where(strings.Join(conditions, " AND "), values...).Delete("").Error +} + +func (s JoinTableHandler) JoinWith(handler JoinTableHandlerInterface, db *DB, source interface{}) *DB { + quotedTable := handler.Table(db) + + scope := db.NewScope(source) + modelType := scope.GetModelStruct().ModelType + var joinConditions []string + var queryConditions []string + var values []interface{} + if s.Source.ModelType == modelType { + for _, foreignKey := range s.Destination.ForeignKeys { + destinationTableName := db.NewScope(reflect.New(s.Destination.ModelType).Interface()).QuotedTableName() + joinConditions = append(joinConditions, fmt.Sprintf("%v.%v = %v.%v", quotedTable, scope.Quote(foreignKey.DBName), destinationTableName, scope.Quote(foreignKey.AssociationDBName))) + } + + for _, foreignKey := range s.Source.ForeignKeys { + queryConditions = append(queryConditions, fmt.Sprintf("%v.%v = ?", quotedTable, scope.Quote(foreignKey.DBName))) + values = append(values, scope.Fields()[foreignKey.AssociationDBName].Field.Interface()) + } + return db.Joins(fmt.Sprintf("INNER JOIN %v ON %v", quotedTable, strings.Join(joinConditions, " AND "))). + Where(strings.Join(queryConditions, " AND "), values...) + } else { + db.Error = errors.New("wrong source type for join table handler") + return db + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/join_table_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/join_table_test.go new file mode 100644 index 0000000..3353aee --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/join_table_test.go @@ -0,0 +1,72 @@ +package gorm_test + +import ( + "fmt" + "testing" + "time" + + "github.com/jinzhu/gorm" +) + +type Person struct { + Id int + Name string + Addresses []*Address `gorm:"many2many:person_addresses;"` +} + +type PersonAddress struct { + gorm.JoinTableHandler + PersonID int + AddressID int + DeletedAt time.Time + CreatedAt time.Time +} + +func (*PersonAddress) Add(handler gorm.JoinTableHandlerInterface, db *gorm.DB, foreignValue interface{}, associationValue interface{}) error { + return db.Where(map[string]interface{}{ + "person_id": db.NewScope(foreignValue).PrimaryKeyValue(), + "address_id": db.NewScope(associationValue).PrimaryKeyValue(), + }).Assign(map[string]interface{}{ + "person_id": foreignValue, + "address_id": associationValue, + "deleted_at": gorm.Expr("NULL"), + }).FirstOrCreate(&PersonAddress{}).Error +} + +func (*PersonAddress) Delete(handler gorm.JoinTableHandlerInterface, db *gorm.DB, sources ...interface{}) error { + return db.Delete(&PersonAddress{}).Error +} + +func (pa *PersonAddress) JoinWith(handler gorm.JoinTableHandlerInterface, db *gorm.DB, source interface{}) *gorm.DB { + table := pa.Table(db) + return db.Table(table).Joins("INNER JOIN person_addresses ON person_addresses.address_id = addresses.id").Where(fmt.Sprintf("%v.deleted_at IS NULL OR %v.deleted_at <= '0001-01-02'", table, table)) +} + +func TestJoinTable(t *testing.T) { + DB.Exec("drop table person_addresses;") + DB.AutoMigrate(&Person{}) + DB.SetJoinTableHandler(&Person{}, "Addresses", &PersonAddress{}) + + address1 := &Address{Address1: "address 1"} + address2 := &Address{Address1: "address 2"} + person := &Person{Name: "person", Addresses: []*Address{address1, address2}} + DB.Save(person) + + DB.Model(person).Association("Addresses").Delete(address1) + + if DB.Find(&[]PersonAddress{}, "person_id = ?", person.Id).RowsAffected != 1 { + t.Errorf("Should found one address") + } + + if DB.Model(person).Association("Addresses").Count() != 1 { + t.Errorf("Should found one address") + } + + if DB.Unscoped().Find(&[]PersonAddress{}, "person_id = ?", person.Id).RowsAffected != 2 { + t.Errorf("Found two addresses with Unscoped") + } + + if DB.Model(person).Association("Addresses").Clear(); DB.Model(person).Association("Addresses").Count() != 0 { + t.Errorf("Should deleted all addresses") + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/logger.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/logger.go new file mode 100644 index 0000000..907faa8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/logger.go @@ -0,0 +1,67 @@ +package gorm + +import ( + "database/sql/driver" + "fmt" + "log" + "os" + "reflect" + "regexp" + "time" +) + +type logger interface { + Print(v ...interface{}) +} + +type Logger struct { + *log.Logger +} + +var defaultLogger = Logger{log.New(os.Stdout, "\r\n", 0)} + +// Format log +var sqlRegexp = regexp.MustCompile(`(\$\d+)|\?`) + +func (logger Logger) Print(values ...interface{}) { + if len(values) > 1 { + level := values[0] + currentTime := "\n\033[33m[" + NowFunc().Format("2006-01-02 15:04:05") + "]\033[0m" + source := fmt.Sprintf("\033[35m(%v)\033[0m", values[1]) + messages := []interface{}{source, currentTime} + + if level == "sql" { + // duration + messages = append(messages, fmt.Sprintf(" \033[36;1m[%.2fms]\033[0m ", float64(values[2].(time.Duration).Nanoseconds()/1e4)/100.0)) + // sql + var formatedValues []interface{} + for _, value := range values[4].([]interface{}) { + indirectValue := reflect.Indirect(reflect.ValueOf(value)) + if indirectValue.IsValid() { + value = indirectValue.Interface() + if t, ok := value.(time.Time); ok { + formatedValues = append(formatedValues, fmt.Sprintf("'%v'", t.Format(time.RFC3339))) + } else if b, ok := value.([]byte); ok { + formatedValues = append(formatedValues, fmt.Sprintf("'%v'", string(b))) + } else if r, ok := value.(driver.Valuer); ok { + if value, err := r.Value(); err == nil && value != nil { + formatedValues = append(formatedValues, fmt.Sprintf("'%v'", value)) + } else { + formatedValues = append(formatedValues, "NULL") + } + } else { + formatedValues = append(formatedValues, fmt.Sprintf("'%v'", value)) + } + } else { + formatedValues = append(formatedValues, fmt.Sprintf("'%v'", value)) + } + } + messages = append(messages, fmt.Sprintf(sqlRegexp.ReplaceAllString(values[3].(string), "%v"), formatedValues...)) + } else { + messages = append(messages, "\033[31;1m") + messages = append(messages, values[2:]...) + messages = append(messages, "\033[0m") + } + logger.Println(messages...) + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/main.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/main.go new file mode 100644 index 0000000..d189814 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/main.go @@ -0,0 +1,540 @@ +package gorm + +import ( + "database/sql" + "errors" + "fmt" + "reflect" + "strings" + "time" +) + +// NowFunc returns current time, this function is exported in order to be able +// to give the flexibility to the developer to customize it according to their +// needs +// +// e.g: return time.Now().UTC() +// +var NowFunc = func() time.Time { + return time.Now() +} + +type DB struct { + Value interface{} + Error error + RowsAffected int64 + callback *callback + db sqlCommon + parent *DB + search *search + logMode int + logger logger + dialect Dialect + singularTable bool + source string + values map[string]interface{} + joinTableHandlers map[string]JoinTableHandler +} + +func Open(dialect string, args ...interface{}) (DB, error) { + var db DB + var err error + + if len(args) == 0 { + err = errors.New("invalid database source") + } else { + var source string + var dbSql sqlCommon + + switch value := args[0].(type) { + case string: + var driver = dialect + if len(args) == 1 { + source = value + } else if len(args) >= 2 { + driver = value + source = args[1].(string) + } + if driver == "foundation" { + driver = "postgres" // FoundationDB speaks a postgres-compatible protocol. + } + dbSql, err = sql.Open(driver, source) + case sqlCommon: + source = reflect.Indirect(reflect.ValueOf(value)).FieldByName("dsn").String() + dbSql = value + } + + db = DB{ + dialect: NewDialect(dialect), + logger: defaultLogger, + callback: DefaultCallback, + source: source, + values: map[string]interface{}{}, + db: dbSql, + } + db.parent = &db + + if err == nil { + err = db.DB().Ping() // Send a ping to make sure the database connection is alive. + } + } + + return db, err +} + +func (s *DB) Close() error { + return s.parent.db.(*sql.DB).Close() +} + +func (s *DB) DB() *sql.DB { + return s.db.(*sql.DB) +} + +func (s *DB) New() *DB { + clone := s.clone() + clone.search = nil + clone.Value = nil + return clone +} + +// NewScope create scope for callbacks, including DB's search information +func (db *DB) NewScope(value interface{}) *Scope { + dbClone := db.clone() + dbClone.Value = value + return &Scope{db: dbClone, Search: dbClone.search.clone(), Value: value} +} + +// CommonDB Return the underlying sql.DB or sql.Tx instance. +// Use of this method is discouraged. It's mainly intended to allow +// coexistence with legacy non-GORM code. +func (s *DB) CommonDB() sqlCommon { + return s.db +} + +func (s *DB) Callback() *callback { + s.parent.callback = s.parent.callback.clone() + return s.parent.callback +} + +func (s *DB) SetLogger(l logger) { + s.logger = l +} + +func (s *DB) LogMode(enable bool) *DB { + if enable { + s.logMode = 2 + } else { + s.logMode = 1 + } + return s +} + +func (s *DB) SingularTable(enable bool) { + modelStructs = map[reflect.Type]*ModelStruct{} + s.parent.singularTable = enable +} + +func (s *DB) Where(query interface{}, args ...interface{}) *DB { + return s.clone().search.Where(query, args...).db +} + +func (s *DB) Or(query interface{}, args ...interface{}) *DB { + return s.clone().search.Or(query, args...).db +} + +func (s *DB) Not(query interface{}, args ...interface{}) *DB { + return s.clone().search.Not(query, args...).db +} + +func (s *DB) Limit(value interface{}) *DB { + return s.clone().search.Limit(value).db +} + +func (s *DB) Offset(value interface{}) *DB { + return s.clone().search.Offset(value).db +} + +func (s *DB) Order(value string, reorder ...bool) *DB { + return s.clone().search.Order(value, reorder...).db +} + +func (s *DB) Select(query interface{}, args ...interface{}) *DB { + return s.clone().search.Select(query, args...).db +} + +func (s *DB) Omit(columns ...string) *DB { + return s.clone().search.Omit(columns...).db +} + +func (s *DB) Group(query string) *DB { + return s.clone().search.Group(query).db +} + +func (s *DB) Having(query string, values ...interface{}) *DB { + return s.clone().search.Having(query, values...).db +} + +func (s *DB) Joins(query string) *DB { + return s.clone().search.Joins(query).db +} + +func (s *DB) Scopes(funcs ...func(*DB) *DB) *DB { + for _, f := range funcs { + s = f(s) + } + return s +} + +func (s *DB) Unscoped() *DB { + return s.clone().search.unscoped().db +} + +func (s *DB) Attrs(attrs ...interface{}) *DB { + return s.clone().search.Attrs(attrs...).db +} + +func (s *DB) Assign(attrs ...interface{}) *DB { + return s.clone().search.Assign(attrs...).db +} + +func (s *DB) First(out interface{}, where ...interface{}) *DB { + newScope := s.clone().NewScope(out) + newScope.Search.Limit(1) + return newScope.Set("gorm:order_by_primary_key", "ASC"). + inlineCondition(where...).callCallbacks(s.parent.callback.queries).db +} + +func (s *DB) Last(out interface{}, where ...interface{}) *DB { + newScope := s.clone().NewScope(out) + newScope.Search.Limit(1) + return newScope.Set("gorm:order_by_primary_key", "DESC"). + inlineCondition(where...).callCallbacks(s.parent.callback.queries).db +} + +func (s *DB) Find(out interface{}, where ...interface{}) *DB { + return s.clone().NewScope(out).inlineCondition(where...).callCallbacks(s.parent.callback.queries).db +} + +func (s *DB) Scan(dest interface{}) *DB { + return s.clone().NewScope(s.Value).Set("gorm:query_destination", dest).callCallbacks(s.parent.callback.queries).db +} + +func (s *DB) Row() *sql.Row { + return s.NewScope(s.Value).row() +} + +func (s *DB) Rows() (*sql.Rows, error) { + return s.NewScope(s.Value).rows() +} + +func (s *DB) Pluck(column string, value interface{}) *DB { + return s.NewScope(s.Value).pluck(column, value).db +} + +func (s *DB) Count(value interface{}) *DB { + return s.NewScope(s.Value).count(value).db +} + +func (s *DB) Related(value interface{}, foreignKeys ...string) *DB { + return s.clone().NewScope(s.Value).related(value, foreignKeys...).db +} + +func (s *DB) FirstOrInit(out interface{}, where ...interface{}) *DB { + c := s.clone() + if result := c.First(out, where...); result.Error != nil { + if !result.RecordNotFound() { + return result + } + c.NewScope(out).inlineCondition(where...).initialize() + } else { + c.NewScope(out).updatedAttrsWithValues(convertInterfaceToMap(s.search.assignAttrs), false) + } + return c +} + +func (s *DB) FirstOrCreate(out interface{}, where ...interface{}) *DB { + c := s.clone() + if result := c.First(out, where...); result.Error != nil { + if !result.RecordNotFound() { + return result + } + c.AddError(c.NewScope(out).inlineCondition(where...).initialize().callCallbacks(s.parent.callback.creates).db.Error) + } else if len(c.search.assignAttrs) > 0 { + c.AddError(c.NewScope(out).InstanceSet("gorm:update_interface", s.search.assignAttrs).callCallbacks(s.parent.callback.updates).db.Error) + } + return c +} + +func (s *DB) Update(attrs ...interface{}) *DB { + return s.Updates(toSearchableMap(attrs...), true) +} + +func (s *DB) Updates(values interface{}, ignoreProtectedAttrs ...bool) *DB { + return s.clone().NewScope(s.Value). + Set("gorm:ignore_protected_attrs", len(ignoreProtectedAttrs) > 0). + InstanceSet("gorm:update_interface", values). + callCallbacks(s.parent.callback.updates).db +} + +func (s *DB) UpdateColumn(attrs ...interface{}) *DB { + return s.UpdateColumns(toSearchableMap(attrs...)) +} + +func (s *DB) UpdateColumns(values interface{}) *DB { + return s.clone().NewScope(s.Value). + Set("gorm:update_column", true). + Set("gorm:save_associations", false). + InstanceSet("gorm:update_interface", values). + callCallbacks(s.parent.callback.updates).db +} + +func (s *DB) Save(value interface{}) *DB { + scope := s.clone().NewScope(value) + if scope.PrimaryKeyZero() { + return scope.callCallbacks(s.parent.callback.creates).db + } + return scope.callCallbacks(s.parent.callback.updates).db +} + +func (s *DB) Create(value interface{}) *DB { + scope := s.clone().NewScope(value) + return scope.callCallbacks(s.parent.callback.creates).db +} + +func (s *DB) Delete(value interface{}, where ...interface{}) *DB { + return s.clone().NewScope(value).inlineCondition(where...).callCallbacks(s.parent.callback.deletes).db +} + +func (s *DB) Raw(sql string, values ...interface{}) *DB { + return s.clone().search.Raw(true).Where(sql, values...).db +} + +func (s *DB) Exec(sql string, values ...interface{}) *DB { + scope := s.clone().NewScope(nil) + generatedSql := scope.buildWhereCondition(map[string]interface{}{"query": sql, "args": values}) + generatedSql = strings.TrimSuffix(strings.TrimPrefix(generatedSql, "("), ")") + scope.Raw(generatedSql) + return scope.Exec().db +} + +func (s *DB) Model(value interface{}) *DB { + c := s.clone() + c.Value = value + return c +} + +func (s *DB) Table(name string) *DB { + clone := s.clone() + clone.search.Table(name) + clone.Value = nil + return clone +} + +func (s *DB) Debug() *DB { + return s.clone().LogMode(true) +} + +func (s *DB) Begin() *DB { + c := s.clone() + if db, ok := c.db.(sqlDb); ok { + tx, err := db.Begin() + c.db = interface{}(tx).(sqlCommon) + c.AddError(err) + } else { + c.AddError(CantStartTransaction) + } + return c +} + +func (s *DB) Commit() *DB { + if db, ok := s.db.(sqlTx); ok { + s.AddError(db.Commit()) + } else { + s.AddError(NoValidTransaction) + } + return s +} + +func (s *DB) Rollback() *DB { + if db, ok := s.db.(sqlTx); ok { + s.AddError(db.Rollback()) + } else { + s.AddError(NoValidTransaction) + } + return s +} + +func (s *DB) NewRecord(value interface{}) bool { + return s.clone().NewScope(value).PrimaryKeyZero() +} + +func (s *DB) RecordNotFound() bool { + return s.Error == RecordNotFound +} + +// Migrations +func (s *DB) CreateTable(value interface{}) *DB { + return s.clone().NewScope(value).createTable().db +} + +func (s *DB) DropTable(value interface{}) *DB { + return s.clone().NewScope(value).dropTable().db +} + +func (s *DB) DropTableIfExists(value interface{}) *DB { + return s.clone().NewScope(value).dropTableIfExists().db +} + +func (s *DB) HasTable(value interface{}) bool { + scope := s.clone().NewScope(value) + tableName := scope.TableName() + has := scope.Dialect().HasTable(scope, tableName) + s.AddError(scope.db.Error) + return has +} + +func (s *DB) AutoMigrate(values ...interface{}) *DB { + db := s.clone() + for _, value := range values { + db = db.NewScope(value).NeedPtr().autoMigrate().db + } + return db +} + +func (s *DB) ModifyColumn(column string, typ string) *DB { + scope := s.clone().NewScope(s.Value) + scope.modifyColumn(column, typ) + return scope.db +} + +func (s *DB) DropColumn(column string) *DB { + scope := s.clone().NewScope(s.Value) + scope.dropColumn(column) + return scope.db +} + +func (s *DB) AddIndex(indexName string, column ...string) *DB { + scope := s.clone().NewScope(s.Value) + scope.addIndex(false, indexName, column...) + return scope.db +} + +func (s *DB) AddUniqueIndex(indexName string, column ...string) *DB { + scope := s.clone().NewScope(s.Value) + scope.addIndex(true, indexName, column...) + return scope.db +} + +func (s *DB) RemoveIndex(indexName string) *DB { + scope := s.clone().NewScope(s.Value) + scope.removeIndex(indexName) + return scope.db +} + +func (s *DB) CurrentDatabase() string { + var ( + scope = s.clone().NewScope(s.Value) + name = s.dialect.CurrentDatabase(scope) + ) + return name +} + +/* +Add foreign key to the given scope + +Example: + db.Model(&User{}).AddForeignKey("city_id", "cities(id)", "RESTRICT", "RESTRICT") +*/ +func (s *DB) AddForeignKey(field string, dest string, onDelete string, onUpdate string) *DB { + scope := s.clone().NewScope(s.Value) + scope.addForeignKey(field, dest, onDelete, onUpdate) + return scope.db +} + +func (s *DB) Association(column string) *Association { + var err error + scope := s.clone().NewScope(s.Value) + + if primaryField := scope.PrimaryField(); primaryField.IsBlank { + err = errors.New("primary key can't be nil") + } else { + if field, ok := scope.FieldByName(column); ok { + if field.Relationship == nil || len(field.Relationship.ForeignFieldNames) == 0 { + err = fmt.Errorf("invalid association %v for %v", column, scope.IndirectValue().Type()) + } else { + return &Association{Scope: scope, Column: column, Field: field} + } + } else { + err = fmt.Errorf("%v doesn't have column %v", scope.IndirectValue().Type(), column) + } + } + + return &Association{Error: err} +} + +func (s *DB) Preload(column string, conditions ...interface{}) *DB { + return s.clone().search.Preload(column, conditions...).db +} + +// Set set value by name +func (s *DB) Set(name string, value interface{}) *DB { + return s.clone().InstantSet(name, value) +} + +func (s *DB) InstantSet(name string, value interface{}) *DB { + s.values[name] = value + return s +} + +// Get get value by name +func (s *DB) Get(name string) (value interface{}, ok bool) { + value, ok = s.values[name] + return +} + +func (s *DB) SetJoinTableHandler(source interface{}, column string, handler JoinTableHandlerInterface) { + scope := s.NewScope(source) + for _, field := range scope.GetModelStruct().StructFields { + if field.Name == column || field.DBName == column { + if many2many := parseTagSetting(field.Tag.Get("gorm"))["MANY2MANY"]; many2many != "" { + source := (&Scope{Value: source}).GetModelStruct().ModelType + destination := (&Scope{Value: reflect.New(field.Struct.Type).Interface()}).GetModelStruct().ModelType + handler.Setup(field.Relationship, many2many, source, destination) + field.Relationship.JoinTableHandler = handler + if table := handler.Table(s); scope.Dialect().HasTable(scope, table) { + s.Table(table).AutoMigrate(handler) + } + } + } + } +} + +func (s *DB) AddError(err error) error { + if err != nil { + if err != RecordNotFound { + if s.logMode == 0 { + go s.print(fileWithLineNum(), err) + } else { + s.log(err) + } + + if e, ok := err.(errorsInterface); ok { + err = Errors{errors: append(s.GetErrors(), e.GetErrors()...)} + } else { + err = Errors{errors: append(s.GetErrors(), err)} + } + } + + s.Error = err + } + return err +} + +func (s *DB) GetErrors() (errors []error) { + if errs, ok := s.Error.(errorsInterface); ok { + return errs.GetErrors() + } else if s.Error != nil { + return []error{s.Error} + } + return +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/main_private.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/main_private.go new file mode 100644 index 0000000..bd097ce --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/main_private.go @@ -0,0 +1,36 @@ +package gorm + +import "time" + +func (s *DB) clone() *DB { + db := DB{db: s.db, parent: s.parent, logger: s.logger, logMode: s.logMode, values: map[string]interface{}{}, Value: s.Value, Error: s.Error} + + for key, value := range s.values { + db.values[key] = value + } + + if s.search == nil { + db.search = &search{} + } else { + db.search = s.search.clone() + } + + db.search.db = &db + return &db +} + +func (s *DB) print(v ...interface{}) { + s.logger.(logger).Print(v...) +} + +func (s *DB) log(v ...interface{}) { + if s != nil && s.logMode == 2 { + s.print(append([]interface{}{"log", fileWithLineNum()}, v...)...) + } +} + +func (s *DB) slog(sql string, t time.Time, vars ...interface{}) { + if s.logMode == 2 { + s.print("sql", fileWithLineNum(), NowFunc().Sub(t), sql, vars) + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/main_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/main_test.go new file mode 100644 index 0000000..2683ac6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/main_test.go @@ -0,0 +1,649 @@ +package gorm_test + +import ( + "database/sql" + "database/sql/driver" + "fmt" + "strconv" + + _ "github.com/denisenkom/go-mssqldb" + testdb "github.com/erikstmartin/go-testdb" + _ "github.com/go-sql-driver/mysql" + "github.com/jinzhu/gorm" + "github.com/jinzhu/now" + _ "github.com/lib/pq" + _ "github.com/mattn/go-sqlite3" + + "os" + "testing" + "time" +) + +var ( + DB gorm.DB + t1, t2, t3, t4, t5 time.Time +) + +func init() { + var err error + + if DB, err = OpenTestConnection(); err != nil { + panic(fmt.Sprintf("No error should happen when connecting to test database, but got err=%+v", err)) + } + + // DB.SetLogger(Logger{log.New(os.Stdout, "\r\n", 0)}) + // DB.SetLogger(log.New(os.Stdout, "\r\n", 0)) + // DB.LogMode(true) + DB.LogMode(false) + + DB.DB().SetMaxIdleConns(10) + + runMigration() +} + +func OpenTestConnection() (db gorm.DB, err error) { + switch os.Getenv("GORM_DIALECT") { + case "mysql": + // CREATE USER 'gorm'@'localhost' IDENTIFIED BY 'gorm'; + // CREATE DATABASE gorm; + // GRANT ALL ON gorm.* TO 'gorm'@'localhost'; + fmt.Println("testing mysql...") + db, err = gorm.Open("mysql", "gorm:gorm@/gorm?charset=utf8&parseTime=True") + case "postgres": + fmt.Println("testing postgres...") + db, err = gorm.Open("postgres", "user=gorm DB.name=gorm sslmode=disable") + case "foundation": + fmt.Println("testing foundation...") + db, err = gorm.Open("foundation", "dbname=gorm port=15432 sslmode=disable") + case "mssql": + fmt.Println("testing mssql...") + db, err = gorm.Open("mssql", "server=SERVER_HERE;database=rogue;user id=USER_HERE;password=PW_HERE;port=1433") + default: + fmt.Println("testing sqlite3...") + db, err = gorm.Open("sqlite3", "/tmp/gorm.db") + } + return +} + +func TestStringPrimaryKey(t *testing.T) { + type UUIDStruct struct { + ID string `gorm:"primary_key"` + Name string + } + DB.AutoMigrate(&UUIDStruct{}) + + data := UUIDStruct{ID: "uuid", Name: "hello"} + if err := DB.Save(&data).Error; err != nil || data.ID != "uuid" { + t.Errorf("string primary key should not be populated") + } +} + +func TestExceptionsWithInvalidSql(t *testing.T) { + var columns []string + if DB.Where("sdsd.zaaa = ?", "sd;;;aa").Pluck("aaa", &columns).Error == nil { + t.Errorf("Should got error with invalid SQL") + } + + if DB.Model(&User{}).Where("sdsd.zaaa = ?", "sd;;;aa").Pluck("aaa", &columns).Error == nil { + t.Errorf("Should got error with invalid SQL") + } + + if DB.Where("sdsd.zaaa = ?", "sd;;;aa").Find(&User{}).Error == nil { + t.Errorf("Should got error with invalid SQL") + } + + var count1, count2 int64 + DB.Model(&User{}).Count(&count1) + if count1 <= 0 { + t.Errorf("Should find some users") + } + + if DB.Where("name = ?", "jinzhu; delete * from users").First(&User{}).Error == nil { + t.Errorf("Should got error with invalid SQL") + } + + DB.Model(&User{}).Count(&count2) + if count1 != count2 { + t.Errorf("No user should not be deleted by invalid SQL") + } +} + +func TestSetTable(t *testing.T) { + DB.Create(getPreparedUser("pluck_user1", "pluck_user")) + DB.Create(getPreparedUser("pluck_user2", "pluck_user")) + DB.Create(getPreparedUser("pluck_user3", "pluck_user")) + + if err := DB.Table("users").Where("role = ?", "pluck_user").Pluck("age", &[]int{}).Error; err != nil { + t.Errorf("No errors should happen if set table for pluck", err.Error()) + } + + var users []User + if DB.Table("users").Find(&[]User{}).Error != nil { + t.Errorf("No errors should happen if set table for find") + } + + if DB.Table("invalid_table").Find(&users).Error == nil { + t.Errorf("Should got error when table is set to an invalid table") + } + + DB.Exec("drop table deleted_users;") + if DB.Table("deleted_users").CreateTable(&User{}).Error != nil { + t.Errorf("Create table with specified table") + } + + DB.Table("deleted_users").Save(&User{Name: "DeletedUser"}) + + var deletedUsers []User + DB.Table("deleted_users").Find(&deletedUsers) + if len(deletedUsers) != 1 { + t.Errorf("Query from specified table") + } + + DB.Save(getPreparedUser("normal_user", "reset_table")) + DB.Table("deleted_users").Save(getPreparedUser("deleted_user", "reset_table")) + var user1, user2, user3 User + DB.Where("role = ?", "reset_table").First(&user1).Table("deleted_users").First(&user2).Table("").First(&user3) + if (user1.Name != "normal_user") || (user2.Name != "deleted_user") || (user3.Name != "normal_user") { + t.Errorf("unset specified table with blank string") + } +} + +type Order struct { +} + +type Cart struct { +} + +func (c Cart) TableName() string { + return "shopping_cart" +} + +func TestHasTable(t *testing.T) { + type Foo struct { + Id int + Stuff string + } + DB.DropTable(&Foo{}) + if ok := DB.HasTable(&Foo{}); ok { + t.Errorf("Table should not exist, but does") + } + if err := DB.CreateTable(&Foo{}).Error; err != nil { + t.Errorf("Table should be created") + } + if ok := DB.HasTable(&Foo{}); !ok { + t.Errorf("Table should exist, but HasTable informs it does not") + } +} + +func TestTableName(t *testing.T) { + DB := DB.Model("") + if DB.NewScope(Order{}).TableName() != "orders" { + t.Errorf("Order's table name should be orders") + } + + if DB.NewScope(&Order{}).TableName() != "orders" { + t.Errorf("&Order's table name should be orders") + } + + if DB.NewScope([]Order{}).TableName() != "orders" { + t.Errorf("[]Order's table name should be orders") + } + + if DB.NewScope(&[]Order{}).TableName() != "orders" { + t.Errorf("&[]Order's table name should be orders") + } + + DB.SingularTable(true) + if DB.NewScope(Order{}).TableName() != "order" { + t.Errorf("Order's singular table name should be order") + } + + if DB.NewScope(&Order{}).TableName() != "order" { + t.Errorf("&Order's singular table name should be order") + } + + if DB.NewScope([]Order{}).TableName() != "order" { + t.Errorf("[]Order's singular table name should be order") + } + + if DB.NewScope(&[]Order{}).TableName() != "order" { + t.Errorf("&[]Order's singular table name should be order") + } + + if DB.NewScope(&Cart{}).TableName() != "shopping_cart" { + t.Errorf("&Cart's singular table name should be shopping_cart") + } + + if DB.NewScope(Cart{}).TableName() != "shopping_cart" { + t.Errorf("Cart's singular table name should be shopping_cart") + } + + if DB.NewScope(&[]Cart{}).TableName() != "shopping_cart" { + t.Errorf("&[]Cart's singular table name should be shopping_cart") + } + + if DB.NewScope([]Cart{}).TableName() != "shopping_cart" { + t.Errorf("[]Cart's singular table name should be shopping_cart") + } + DB.SingularTable(false) +} + +func TestSqlNullValue(t *testing.T) { + DB.DropTable(&NullValue{}) + DB.AutoMigrate(&NullValue{}) + + if err := DB.Save(&NullValue{Name: sql.NullString{String: "hello", Valid: true}, + Age: sql.NullInt64{Int64: 18, Valid: true}, + Male: sql.NullBool{Bool: true, Valid: true}, + Height: sql.NullFloat64{Float64: 100.11, Valid: true}, + AddedAt: NullTime{Time: time.Now(), Valid: true}, + }).Error; err != nil { + t.Errorf("Not error should raise when test null value") + } + + var nv NullValue + DB.First(&nv, "name = ?", "hello") + + if nv.Name.String != "hello" || nv.Age.Int64 != 18 || nv.Male.Bool != true || nv.Height.Float64 != 100.11 || nv.AddedAt.Valid != true { + t.Errorf("Should be able to fetch null value") + } + + if err := DB.Save(&NullValue{Name: sql.NullString{String: "hello-2", Valid: true}, + Age: sql.NullInt64{Int64: 18, Valid: false}, + Male: sql.NullBool{Bool: true, Valid: true}, + Height: sql.NullFloat64{Float64: 100.11, Valid: true}, + AddedAt: NullTime{Time: time.Now(), Valid: false}, + }).Error; err != nil { + t.Errorf("Not error should raise when test null value") + } + + var nv2 NullValue + DB.First(&nv2, "name = ?", "hello-2") + if nv2.Name.String != "hello-2" || nv2.Age.Int64 != 0 || nv2.Male.Bool != true || nv2.Height.Float64 != 100.11 || nv2.AddedAt.Valid != false { + t.Errorf("Should be able to fetch null value") + } + + if err := DB.Save(&NullValue{Name: sql.NullString{String: "hello-3", Valid: false}, + Age: sql.NullInt64{Int64: 18, Valid: false}, + Male: sql.NullBool{Bool: true, Valid: true}, + Height: sql.NullFloat64{Float64: 100.11, Valid: true}, + AddedAt: NullTime{Time: time.Now(), Valid: false}, + }).Error; err == nil { + t.Errorf("Can't save because of name can't be null") + } +} + +func TestTransaction(t *testing.T) { + tx := DB.Begin() + u := User{Name: "transcation"} + if err := tx.Save(&u).Error; err != nil { + t.Errorf("No error should raise") + } + + if err := tx.First(&User{}, "name = ?", "transcation").Error; err != nil { + t.Errorf("Should find saved record") + } + + if sqlTx, ok := tx.CommonDB().(*sql.Tx); !ok || sqlTx == nil { + t.Errorf("Should return the underlying sql.Tx") + } + + tx.Rollback() + + if err := tx.First(&User{}, "name = ?", "transcation").Error; err == nil { + t.Errorf("Should not find record after rollback") + } + + tx2 := DB.Begin() + u2 := User{Name: "transcation-2"} + if err := tx2.Save(&u2).Error; err != nil { + t.Errorf("No error should raise") + } + + if err := tx2.First(&User{}, "name = ?", "transcation-2").Error; err != nil { + t.Errorf("Should find saved record") + } + + tx2.Commit() + + if err := DB.First(&User{}, "name = ?", "transcation-2").Error; err != nil { + t.Errorf("Should be able to find committed record") + } +} + +func TestRow(t *testing.T) { + user1 := User{Name: "RowUser1", Age: 1, Birthday: now.MustParse("2000-1-1")} + user2 := User{Name: "RowUser2", Age: 10, Birthday: now.MustParse("2010-1-1")} + user3 := User{Name: "RowUser3", Age: 20, Birthday: now.MustParse("2020-1-1")} + DB.Save(&user1).Save(&user2).Save(&user3) + + row := DB.Table("users").Where("name = ?", user2.Name).Select("age").Row() + var age int64 + row.Scan(&age) + if age != 10 { + t.Errorf("Scan with Row") + } +} + +func TestRows(t *testing.T) { + user1 := User{Name: "RowsUser1", Age: 1, Birthday: now.MustParse("2000-1-1")} + user2 := User{Name: "RowsUser2", Age: 10, Birthday: now.MustParse("2010-1-1")} + user3 := User{Name: "RowsUser3", Age: 20, Birthday: now.MustParse("2020-1-1")} + DB.Save(&user1).Save(&user2).Save(&user3) + + rows, err := DB.Table("users").Where("name = ? or name = ?", user2.Name, user3.Name).Select("name, age").Rows() + if err != nil { + t.Errorf("Not error should happen, but got") + } + + count := 0 + for rows.Next() { + var name string + var age int64 + rows.Scan(&name, &age) + count++ + } + if count != 2 { + t.Errorf("Should found two records with name 3") + } +} + +func TestScan(t *testing.T) { + user1 := User{Name: "ScanUser1", Age: 1, Birthday: now.MustParse("2000-1-1")} + user2 := User{Name: "ScanUser2", Age: 10, Birthday: now.MustParse("2010-1-1")} + user3 := User{Name: "ScanUser3", Age: 20, Birthday: now.MustParse("2020-1-1")} + DB.Save(&user1).Save(&user2).Save(&user3) + + type result struct { + Name string + Age int + } + + var res result + DB.Table("users").Select("name, age").Where("name = ?", user3.Name).Scan(&res) + if res.Name != user3.Name { + t.Errorf("Scan into struct should work") + } + + var doubleAgeRes result + DB.Table("users").Select("age + age as age").Where("name = ?", user3.Name).Scan(&doubleAgeRes) + if doubleAgeRes.Age != res.Age*2 { + t.Errorf("Scan double age as age") + } + + var ress []result + DB.Table("users").Select("name, age").Where("name in (?)", []string{user2.Name, user3.Name}).Scan(&ress) + if len(ress) != 2 || ress[0].Name != user2.Name || ress[1].Name != user3.Name { + t.Errorf("Scan into struct map") + } +} + +func TestRaw(t *testing.T) { + user1 := User{Name: "ExecRawSqlUser1", Age: 1, Birthday: now.MustParse("2000-1-1")} + user2 := User{Name: "ExecRawSqlUser2", Age: 10, Birthday: now.MustParse("2010-1-1")} + user3 := User{Name: "ExecRawSqlUser3", Age: 20, Birthday: now.MustParse("2020-1-1")} + DB.Save(&user1).Save(&user2).Save(&user3) + + type result struct { + Name string + Email string + } + + var ress []result + DB.Raw("SELECT name, age FROM users WHERE name = ? or name = ?", user2.Name, user3.Name).Scan(&ress) + if len(ress) != 2 || ress[0].Name != user2.Name || ress[1].Name != user3.Name { + t.Errorf("Raw with scan") + } + + rows, _ := DB.Raw("select name, age from users where name = ?", user3.Name).Rows() + count := 0 + for rows.Next() { + count++ + } + if count != 1 { + t.Errorf("Raw with Rows should find one record with name 3") + } + + DB.Exec("update users set name=? where name in (?)", "jinzhu", []string{user1.Name, user2.Name, user3.Name}) + if DB.Where("name in (?)", []string{user1.Name, user2.Name, user3.Name}).First(&User{}).Error != gorm.RecordNotFound { + t.Error("Raw sql to update records") + } +} + +func TestGroup(t *testing.T) { + rows, err := DB.Select("name").Table("users").Group("name").Rows() + + if err == nil { + defer rows.Close() + for rows.Next() { + var name string + rows.Scan(&name) + } + } else { + t.Errorf("Should not raise any error") + } +} + +func TestJoins(t *testing.T) { + type result struct { + Name string + Email string + } + + user := User{ + Name: "joins", + Emails: []Email{{Email: "join1@example.com"}, {Email: "join2@example.com"}}, + } + DB.Save(&user) + + var results []result + DB.Table("users").Select("name, email").Joins("left join emails on emails.user_id = users.id").Where("name = ?", "joins").Scan(&results) + if len(results) != 2 || results[0].Email != "join1@example.com" || results[1].Email != "join2@example.com" { + t.Errorf("Should find all two emails with Join") + } +} + +func TestHaving(t *testing.T) { + rows, err := DB.Select("name, count(*) as total").Table("users").Group("name").Having("name IN (?)", []string{"2", "3"}).Rows() + + if err == nil { + defer rows.Close() + for rows.Next() { + var name string + var total int64 + rows.Scan(&name, &total) + + if name == "2" && total != 1 { + t.Errorf("Should have one user having name 2") + } + if name == "3" && total != 2 { + t.Errorf("Should have two users having name 3") + } + } + } else { + t.Errorf("Should not raise any error") + } +} + +func DialectHasTzSupport() bool { + // NB: mssql and FoundationDB do not support time zones. + if dialect := os.Getenv("GORM_DIALECT"); dialect == "mssql" || dialect == "foundation" { + return false + } + return true +} + +func TestTimeWithZone(t *testing.T) { + var format = "2006-01-02 15:04:05 -0700" + var times []time.Time + GMT8, _ := time.LoadLocation("Asia/Shanghai") + times = append(times, time.Date(2013, 02, 19, 1, 51, 49, 123456789, GMT8)) + times = append(times, time.Date(2013, 02, 18, 17, 51, 49, 123456789, time.UTC)) + + for index, vtime := range times { + name := "time_with_zone_" + strconv.Itoa(index) + user := User{Name: name, Birthday: vtime} + + if !DialectHasTzSupport() { + // If our driver dialect doesn't support TZ's, just use UTC for everything here. + user.Birthday = vtime.UTC() + } + + DB.Save(&user) + expectedBirthday := "2013-02-18 17:51:49 +0000" + foundBirthday := user.Birthday.UTC().Format(format) + if foundBirthday != expectedBirthday { + t.Errorf("User's birthday should not be changed after save for name=%s, expected bday=%+v but actual value=%+v", name, expectedBirthday, foundBirthday) + } + + var findUser, findUser2, findUser3 User + DB.First(&findUser, "name = ?", name) + foundBirthday = findUser.Birthday.UTC().Format(format) + if foundBirthday != expectedBirthday { + t.Errorf("User's birthday should not be changed after find for name=%s, expected bday=%+v but actual value=%+v or %+v", name, expectedBirthday, foundBirthday) + } + + if DB.Where("id = ? AND birthday >= ?", findUser.Id, user.Birthday.Add(-time.Minute)).First(&findUser2).RecordNotFound() { + t.Errorf("User should be found") + } + + if !DB.Where("id = ? AND birthday >= ?", findUser.Id, user.Birthday.Add(time.Minute)).First(&findUser3).RecordNotFound() { + t.Errorf("User should not be found") + } + } +} + +func TestHstore(t *testing.T) { + type Details struct { + Id int64 + Bulk gorm.Hstore + } + + if dialect := os.Getenv("GORM_DIALECT"); dialect != "postgres" { + t.Skip() + } + + if err := DB.Exec("CREATE EXTENSION IF NOT EXISTS hstore").Error; err != nil { + fmt.Println("\033[31mHINT: Must be superuser to create hstore extension (ALTER USER gorm WITH SUPERUSER;)\033[0m") + panic(fmt.Sprintf("No error should happen when create hstore extension, but got %+v", err)) + } + + DB.Exec("drop table details") + + if err := DB.CreateTable(&Details{}).Error; err != nil { + panic(fmt.Sprintf("No error should happen when create table, but got %+v", err)) + } + + bankAccountId, phoneNumber, opinion := "123456", "14151321232", "sharkbait" + bulk := map[string]*string{ + "bankAccountId": &bankAccountId, + "phoneNumber": &phoneNumber, + "opinion": &opinion, + } + d := Details{Bulk: bulk} + DB.Save(&d) + + var d2 Details + if err := DB.First(&d2).Error; err != nil { + t.Errorf("Got error when tried to fetch details: %+v", err) + } + + for k := range bulk { + if r, ok := d2.Bulk[k]; ok { + if res, _ := bulk[k]; *res != *r { + t.Errorf("Details should be equal") + } + } else { + t.Errorf("Details should be existed") + } + } +} + +func TestSetAndGet(t *testing.T) { + if value, ok := DB.Set("hello", "world").Get("hello"); !ok { + t.Errorf("Should be able to get setting after set") + } else { + if value.(string) != "world" { + t.Errorf("Setted value should not be changed") + } + } + + if _, ok := DB.Get("non_existing"); ok { + t.Errorf("Get non existing key should return error") + } +} + +func TestCompatibilityMode(t *testing.T) { + DB, _ := gorm.Open("testdb", "") + testdb.SetQueryFunc(func(query string) (driver.Rows, error) { + columns := []string{"id", "name", "age"} + result := ` + 1,Tim,20 + 2,Joe,25 + 3,Bob,30 + ` + return testdb.RowsFromCSVString(columns, result), nil + }) + + var users []User + DB.Find(&users) + if (users[0].Name != "Tim") || len(users) != 3 { + t.Errorf("Unexcepted result returned") + } +} + +func TestOpenExistingDB(t *testing.T) { + DB.Save(&User{Name: "jnfeinstein"}) + dialect := os.Getenv("GORM_DIALECT") + + db, err := gorm.Open(dialect, DB.DB()) + if err != nil { + t.Errorf("Should have wrapped the existing DB connection") + } + + var user User + if db.Where("name = ?", "jnfeinstein").First(&user).Error == gorm.RecordNotFound { + t.Errorf("Should have found existing record") + } +} + +func BenchmarkGorm(b *testing.B) { + b.N = 2000 + for x := 0; x < b.N; x++ { + e := strconv.Itoa(x) + "benchmark@example.org" + email := BigEmail{Email: e, UserAgent: "pc", RegisteredAt: time.Now()} + // Insert + DB.Save(&email) + // Query + DB.First(&BigEmail{}, "email = ?", e) + // Update + DB.Model(&email).UpdateColumn("email", "new-"+e) + // Delete + DB.Delete(&email) + } +} + +func BenchmarkRawSql(b *testing.B) { + DB, _ := sql.Open("postgres", "user=gorm DB.ame=gorm sslmode=disable") + DB.SetMaxIdleConns(10) + insertSql := "INSERT INTO emails (user_id,email,user_agent,registered_at,created_at,updated_at) VALUES ($1,$2,$3,$4,$5,$6) RETURNING id" + querySql := "SELECT * FROM emails WHERE email = $1 ORDER BY id LIMIT 1" + updateSql := "UPDATE emails SET email = $1, updated_at = $2 WHERE id = $3" + deleteSql := "DELETE FROM orders WHERE id = $1" + + b.N = 2000 + for x := 0; x < b.N; x++ { + var id int64 + e := strconv.Itoa(x) + "benchmark@example.org" + email := BigEmail{Email: e, UserAgent: "pc", RegisteredAt: time.Now()} + // Insert + DB.QueryRow(insertSql, email.UserId, email.Email, email.UserAgent, email.RegisteredAt, time.Now(), time.Now()).Scan(&id) + // Query + rows, _ := DB.Query(querySql, email.Email) + rows.Close() + // Update + DB.Exec(updateSql, "new-"+e, time.Now(), id) + // Delete + DB.Exec(deleteSql, id) + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/migration_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/migration_test.go new file mode 100644 index 0000000..74c8b94 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/migration_test.go @@ -0,0 +1,123 @@ +package gorm_test + +import ( + "fmt" + "testing" + "time" +) + +func runMigration() { + if err := DB.DropTableIfExists(&User{}).Error; err != nil { + fmt.Printf("Got error when try to delete table users, %+v\n", err) + } + + for _, table := range []string{"animals", "user_languages"} { + DB.Exec(fmt.Sprintf("drop table %v;", table)) + } + + values := []interface{}{&Product{}, &Email{}, &Address{}, &CreditCard{}, &Company{}, &Role{}, &Language{}, &HNPost{}, &EngadgetPost{}, &Animal{}, &User{}, &JoinTable{}} + for _, value := range values { + DB.DropTable(value) + } + + if err := DB.AutoMigrate(values...).Error; err != nil { + panic(fmt.Sprintf("No error should happen when create table, but got %+v", err)) + } +} + +func TestIndexes(t *testing.T) { + if err := DB.Model(&Email{}).AddIndex("idx_email_email", "email").Error; err != nil { + t.Errorf("Got error when tried to create index: %+v", err) + } + + scope := DB.NewScope(&Email{}) + if !scope.Dialect().HasIndex(scope, scope.TableName(), "idx_email_email") { + t.Errorf("Email should have index idx_email_email") + } + + if err := DB.Model(&Email{}).RemoveIndex("idx_email_email").Error; err != nil { + t.Errorf("Got error when tried to remove index: %+v", err) + } + + if scope.Dialect().HasIndex(scope, scope.TableName(), "idx_email_email") { + t.Errorf("Email's index idx_email_email should be deleted") + } + + if err := DB.Model(&Email{}).AddIndex("idx_email_email_and_user_id", "user_id", "email").Error; err != nil { + t.Errorf("Got error when tried to create index: %+v", err) + } + + if !scope.Dialect().HasIndex(scope, scope.TableName(), "idx_email_email_and_user_id") { + t.Errorf("Email should have index idx_email_email_and_user_id") + } + + if err := DB.Model(&Email{}).RemoveIndex("idx_email_email_and_user_id").Error; err != nil { + t.Errorf("Got error when tried to remove index: %+v", err) + } + + if scope.Dialect().HasIndex(scope, scope.TableName(), "idx_email_email_and_user_id") { + t.Errorf("Email's index idx_email_email_and_user_id should be deleted") + } + + if err := DB.Model(&Email{}).AddUniqueIndex("idx_email_email_and_user_id", "user_id", "email").Error; err != nil { + t.Errorf("Got error when tried to create index: %+v", err) + } + + if !scope.Dialect().HasIndex(scope, scope.TableName(), "idx_email_email_and_user_id") { + t.Errorf("Email should have index idx_email_email_and_user_id") + } + + if DB.Save(&User{Name: "unique_indexes", Emails: []Email{{Email: "user1@example.comiii"}, {Email: "user1@example.com"}, {Email: "user1@example.com"}}}).Error == nil { + t.Errorf("Should get to create duplicate record when having unique index") + } + + if err := DB.Model(&Email{}).RemoveIndex("idx_email_email_and_user_id").Error; err != nil { + t.Errorf("Got error when tried to remove index: %+v", err) + } + + if scope.Dialect().HasIndex(scope, scope.TableName(), "idx_email_email_and_user_id") { + t.Errorf("Email's index idx_email_email_and_user_id should be deleted") + } + + if DB.Save(&User{Name: "unique_indexes", Emails: []Email{{Email: "user1@example.com"}, {Email: "user1@example.com"}}}).Error != nil { + t.Errorf("Should be able to create duplicated emails after remove unique index") + } +} + +type BigEmail struct { + Id int64 + UserId int64 + Email string `sql:"index:idx_email_agent"` + UserAgent string `sql:"index:idx_email_agent"` + RegisteredAt time.Time `sql:"unique_index"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (b BigEmail) TableName() string { + return "emails" +} + +func TestAutoMigration(t *testing.T) { + DB.AutoMigrate(&Address{}) + if err := DB.Table("emails").AutoMigrate(&BigEmail{}).Error; err != nil { + t.Errorf("Auto Migrate should not raise any error") + } + + DB.Save(&BigEmail{Email: "jinzhu@example.org", UserAgent: "pc", RegisteredAt: time.Now()}) + + scope := DB.NewScope(&BigEmail{}) + if !scope.Dialect().HasIndex(scope, scope.TableName(), "idx_email_agent") { + t.Errorf("Failed to create index") + } + + if !scope.Dialect().HasIndex(scope, scope.TableName(), "uix_emails_registered_at") { + t.Errorf("Failed to create index") + } + + var bigemail BigEmail + DB.First(&bigemail, "user_agent = ?", "pc") + if bigemail.Email != "jinzhu@example.org" || bigemail.UserAgent != "pc" || bigemail.RegisteredAt.IsZero() { + t.Error("Big Emails should be saved and fetched correctly") + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/model.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/model.go new file mode 100644 index 0000000..50fa52e --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/model.go @@ -0,0 +1,10 @@ +package gorm + +import "time" + +type Model struct { + ID uint `gorm:"primary_key"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt *time.Time +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/model_struct.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/model_struct.go new file mode 100644 index 0000000..26c58fc --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/model_struct.go @@ -0,0 +1,441 @@ +package gorm + +import ( + "database/sql" + "fmt" + "go/ast" + "reflect" + "strconv" + "strings" + "time" + + "github.com/qor/inflection" +) + +var modelStructs = map[reflect.Type]*ModelStruct{} + +var DefaultTableNameHandler = func(db *DB, defaultTableName string) string { + return defaultTableName +} + +type ModelStruct struct { + PrimaryFields []*StructField + StructFields []*StructField + ModelType reflect.Type + defaultTableName string +} + +func (s ModelStruct) TableName(db *DB) string { + return DefaultTableNameHandler(db, s.defaultTableName) +} + +type StructField struct { + DBName string + Name string + Names []string + IsPrimaryKey bool + IsNormal bool + IsIgnored bool + IsScanner bool + HasDefaultValue bool + Tag reflect.StructTag + Struct reflect.StructField + IsForeignKey bool + Relationship *Relationship +} + +func (structField *StructField) clone() *StructField { + return &StructField{ + DBName: structField.DBName, + Name: structField.Name, + Names: structField.Names, + IsPrimaryKey: structField.IsPrimaryKey, + IsNormal: structField.IsNormal, + IsIgnored: structField.IsIgnored, + IsScanner: structField.IsScanner, + HasDefaultValue: structField.HasDefaultValue, + Tag: structField.Tag, + Struct: structField.Struct, + IsForeignKey: structField.IsForeignKey, + Relationship: structField.Relationship, + } +} + +type Relationship struct { + Kind string + PolymorphicType string + PolymorphicDBName string + ForeignFieldNames []string + ForeignDBNames []string + AssociationForeignFieldNames []string + AssociationForeignDBNames []string + JoinTableHandler JoinTableHandlerInterface +} + +func (scope *Scope) GetModelStruct() *ModelStruct { + var modelStruct ModelStruct + + reflectValue := reflect.Indirect(reflect.ValueOf(scope.Value)) + if !reflectValue.IsValid() { + return &modelStruct + } + + if reflectValue.Kind() == reflect.Slice { + reflectValue = reflect.Indirect(reflect.New(reflectValue.Type().Elem())) + } + + scopeType := reflectValue.Type() + + if scopeType.Kind() == reflect.Ptr { + scopeType = scopeType.Elem() + } + + if value, ok := modelStructs[scopeType]; ok { + return value + } + + modelStruct.ModelType = scopeType + if scopeType.Kind() != reflect.Struct { + return &modelStruct + } + + // Set tablename + type tabler interface { + TableName() string + } + + if tabler, ok := reflect.New(scopeType).Interface().(interface { + TableName() string + }); ok { + modelStruct.defaultTableName = tabler.TableName() + } else { + name := ToDBName(scopeType.Name()) + if scope.db == nil || !scope.db.parent.singularTable { + name = inflection.Plural(name) + } + + modelStruct.defaultTableName = name + } + + // Get all fields + fields := []*StructField{} + for i := 0; i < scopeType.NumField(); i++ { + if fieldStruct := scopeType.Field(i); ast.IsExported(fieldStruct.Name) { + field := &StructField{ + Struct: fieldStruct, + Name: fieldStruct.Name, + Names: []string{fieldStruct.Name}, + Tag: fieldStruct.Tag, + } + + if fieldStruct.Tag.Get("sql") == "-" { + field.IsIgnored = true + } else { + sqlSettings := parseTagSetting(field.Tag.Get("sql")) + gormSettings := parseTagSetting(field.Tag.Get("gorm")) + if _, ok := gormSettings["PRIMARY_KEY"]; ok { + field.IsPrimaryKey = true + modelStruct.PrimaryFields = append(modelStruct.PrimaryFields, field) + } + + if _, ok := sqlSettings["DEFAULT"]; ok { + field.HasDefaultValue = true + } + + if value, ok := gormSettings["COLUMN"]; ok { + field.DBName = value + } else { + field.DBName = ToDBName(fieldStruct.Name) + } + } + fields = append(fields, field) + } + } + + defer func() { + for _, field := range fields { + if !field.IsIgnored { + fieldStruct := field.Struct + indirectType := fieldStruct.Type + if indirectType.Kind() == reflect.Ptr { + indirectType = indirectType.Elem() + } + + if _, isScanner := reflect.New(indirectType).Interface().(sql.Scanner); isScanner { + field.IsScanner, field.IsNormal = true, true + } + + if _, isTime := reflect.New(indirectType).Interface().(*time.Time); isTime { + field.IsNormal = true + } + + if !field.IsNormal { + gormSettings := parseTagSetting(field.Tag.Get("gorm")) + toScope := scope.New(reflect.New(fieldStruct.Type).Interface()) + + getForeignField := func(column string, fields []*StructField) *StructField { + for _, field := range fields { + if field.Name == column || field.DBName == ToDBName(column) { + return field + } + } + return nil + } + + var relationship = &Relationship{} + + if polymorphic := gormSettings["POLYMORPHIC"]; polymorphic != "" { + if polymorphicField := getForeignField(polymorphic+"Id", toScope.GetStructFields()); polymorphicField != nil { + if polymorphicType := getForeignField(polymorphic+"Type", toScope.GetStructFields()); polymorphicType != nil { + relationship.ForeignFieldNames = []string{polymorphicField.Name} + relationship.ForeignDBNames = []string{polymorphicField.DBName} + relationship.AssociationForeignFieldNames = []string{scope.PrimaryField().Name} + relationship.AssociationForeignDBNames = []string{scope.PrimaryField().DBName} + relationship.PolymorphicType = polymorphicType.Name + relationship.PolymorphicDBName = polymorphicType.DBName + polymorphicType.IsForeignKey = true + polymorphicField.IsForeignKey = true + } + } + } + + var foreignKeys []string + if foreignKey, ok := gormSettings["FOREIGNKEY"]; ok { + foreignKeys = append(foreignKeys, foreignKey) + } + switch indirectType.Kind() { + case reflect.Slice: + elemType := indirectType.Elem() + if elemType.Kind() == reflect.Ptr { + elemType = elemType.Elem() + } + + if elemType.Kind() == reflect.Struct { + if many2many := gormSettings["MANY2MANY"]; many2many != "" { + relationship.Kind = "many_to_many" + + // foreign keys + if len(foreignKeys) == 0 { + for _, field := range scope.PrimaryFields() { + foreignKeys = append(foreignKeys, field.DBName) + } + } + + for _, foreignKey := range foreignKeys { + if field, ok := scope.FieldByName(foreignKey); ok { + relationship.ForeignFieldNames = append(relationship.ForeignFieldNames, field.DBName) + joinTableDBName := ToDBName(scopeType.Name()) + "_" + field.DBName + relationship.ForeignDBNames = append(relationship.ForeignDBNames, joinTableDBName) + } + } + + // association foreign keys + var associationForeignKeys []string + if foreignKey := gormSettings["ASSOCIATIONFOREIGNKEY"]; foreignKey != "" { + associationForeignKeys = []string{gormSettings["ASSOCIATIONFOREIGNKEY"]} + } else { + for _, field := range toScope.PrimaryFields() { + associationForeignKeys = append(associationForeignKeys, field.DBName) + } + } + + for _, name := range associationForeignKeys { + if field, ok := toScope.FieldByName(name); ok { + relationship.AssociationForeignFieldNames = append(relationship.AssociationForeignFieldNames, field.DBName) + joinTableDBName := ToDBName(elemType.Name()) + "_" + field.DBName + relationship.AssociationForeignDBNames = append(relationship.AssociationForeignDBNames, joinTableDBName) + } + } + + joinTableHandler := JoinTableHandler{} + joinTableHandler.Setup(relationship, many2many, scopeType, elemType) + relationship.JoinTableHandler = &joinTableHandler + field.Relationship = relationship + } else { + relationship.Kind = "has_many" + + if len(foreignKeys) == 0 { + for _, field := range scope.PrimaryFields() { + if foreignField := getForeignField(scopeType.Name()+field.Name, toScope.GetStructFields()); foreignField != nil { + relationship.AssociationForeignFieldNames = append(relationship.AssociationForeignFieldNames, field.Name) + relationship.AssociationForeignDBNames = append(relationship.AssociationForeignDBNames, field.DBName) + relationship.ForeignFieldNames = append(relationship.ForeignFieldNames, foreignField.Name) + relationship.ForeignDBNames = append(relationship.ForeignDBNames, foreignField.DBName) + foreignField.IsForeignKey = true + } + } + } else { + for _, foreignKey := range foreignKeys { + if foreignField := getForeignField(foreignKey, toScope.GetStructFields()); foreignField != nil { + relationship.AssociationForeignFieldNames = append(relationship.AssociationForeignFieldNames, scope.PrimaryField().Name) + relationship.AssociationForeignDBNames = append(relationship.AssociationForeignDBNames, scope.PrimaryField().DBName) + relationship.ForeignFieldNames = append(relationship.ForeignFieldNames, foreignField.Name) + relationship.ForeignDBNames = append(relationship.ForeignDBNames, foreignField.DBName) + foreignField.IsForeignKey = true + } + } + } + + if len(relationship.ForeignFieldNames) != 0 { + field.Relationship = relationship + } + } + } else { + field.IsNormal = true + } + case reflect.Struct: + if _, ok := gormSettings["EMBEDDED"]; ok || fieldStruct.Anonymous { + for _, toField := range toScope.GetStructFields() { + toField = toField.clone() + toField.Names = append([]string{fieldStruct.Name}, toField.Names...) + modelStruct.StructFields = append(modelStruct.StructFields, toField) + if toField.IsPrimaryKey { + modelStruct.PrimaryFields = append(modelStruct.PrimaryFields, toField) + } + } + continue + } else { + if len(foreignKeys) == 0 { + for _, f := range scope.PrimaryFields() { + if foreignField := getForeignField(modelStruct.ModelType.Name()+f.Name, toScope.GetStructFields()); foreignField != nil { + relationship.AssociationForeignFieldNames = append(relationship.AssociationForeignFieldNames, f.Name) + relationship.AssociationForeignDBNames = append(relationship.AssociationForeignDBNames, f.DBName) + relationship.ForeignFieldNames = append(relationship.ForeignFieldNames, foreignField.Name) + relationship.ForeignDBNames = append(relationship.ForeignDBNames, foreignField.DBName) + foreignField.IsForeignKey = true + } + } + } else { + for _, foreignKey := range foreignKeys { + if foreignField := getForeignField(foreignKey, toScope.GetStructFields()); foreignField != nil { + relationship.AssociationForeignFieldNames = append(relationship.AssociationForeignFieldNames, scope.PrimaryField().Name) + relationship.AssociationForeignDBNames = append(relationship.AssociationForeignDBNames, scope.PrimaryField().DBName) + relationship.ForeignFieldNames = append(relationship.ForeignFieldNames, foreignField.Name) + relationship.ForeignDBNames = append(relationship.ForeignDBNames, foreignField.DBName) + foreignField.IsForeignKey = true + } + } + } + + if len(relationship.ForeignFieldNames) != 0 { + relationship.Kind = "has_one" + field.Relationship = relationship + } else { + if len(foreignKeys) == 0 { + for _, f := range toScope.PrimaryFields() { + if foreignField := getForeignField(field.Name+f.Name, fields); foreignField != nil { + relationship.AssociationForeignFieldNames = append(relationship.AssociationForeignFieldNames, f.Name) + relationship.AssociationForeignDBNames = append(relationship.AssociationForeignDBNames, f.DBName) + relationship.ForeignFieldNames = append(relationship.ForeignFieldNames, foreignField.Name) + relationship.ForeignDBNames = append(relationship.ForeignDBNames, foreignField.DBName) + foreignField.IsForeignKey = true + } + } + } else { + for _, foreignKey := range foreignKeys { + if foreignField := getForeignField(foreignKey, fields); foreignField != nil { + relationship.AssociationForeignFieldNames = append(relationship.AssociationForeignFieldNames, toScope.PrimaryField().Name) + relationship.AssociationForeignDBNames = append(relationship.AssociationForeignDBNames, toScope.PrimaryField().DBName) + relationship.ForeignFieldNames = append(relationship.ForeignFieldNames, foreignField.Name) + relationship.ForeignDBNames = append(relationship.ForeignDBNames, foreignField.DBName) + foreignField.IsForeignKey = true + } + } + } + + if len(relationship.ForeignFieldNames) != 0 { + relationship.Kind = "belongs_to" + field.Relationship = relationship + } + } + } + default: + field.IsNormal = true + } + } + + if field.IsNormal { + if len(modelStruct.PrimaryFields) == 0 && field.DBName == "id" { + field.IsPrimaryKey = true + modelStruct.PrimaryFields = append(modelStruct.PrimaryFields, field) + } + } + } + modelStruct.StructFields = append(modelStruct.StructFields, field) + } + }() + + modelStructs[scopeType] = &modelStruct + + return &modelStruct +} + +func (scope *Scope) GetStructFields() (fields []*StructField) { + return scope.GetModelStruct().StructFields +} + +func (scope *Scope) generateSqlTag(field *StructField) string { + var sqlType string + structType := field.Struct.Type + if structType.Kind() == reflect.Ptr { + structType = structType.Elem() + } + reflectValue := reflect.Indirect(reflect.New(structType)) + sqlSettings := parseTagSetting(field.Tag.Get("sql")) + + if value, ok := sqlSettings["TYPE"]; ok { + sqlType = value + } + + additionalType := sqlSettings["NOT NULL"] + " " + sqlSettings["UNIQUE"] + if value, ok := sqlSettings["DEFAULT"]; ok { + additionalType = additionalType + " DEFAULT " + value + } + + if field.IsScanner { + var getScannerValue func(reflect.Value) + getScannerValue = func(value reflect.Value) { + reflectValue = value + if _, isScanner := reflect.New(reflectValue.Type()).Interface().(sql.Scanner); isScanner && reflectValue.Kind() == reflect.Struct { + getScannerValue(reflectValue.Field(0)) + } + } + getScannerValue(reflectValue) + } + + if sqlType == "" { + var size = 255 + + if value, ok := sqlSettings["SIZE"]; ok { + size, _ = strconv.Atoi(value) + } + + _, autoIncrease := sqlSettings["AUTO_INCREMENT"] + if field.IsPrimaryKey { + autoIncrease = true + } + + sqlType = scope.Dialect().SqlTag(reflectValue, size, autoIncrease) + } + + if strings.TrimSpace(additionalType) == "" { + return sqlType + } else { + return fmt.Sprintf("%v %v", sqlType, additionalType) + } +} + +func parseTagSetting(str string) map[string]string { + tags := strings.Split(str, ";") + setting := map[string]string{} + for _, value := range tags { + v := strings.Split(value, ":") + k := strings.TrimSpace(strings.ToUpper(v[0])) + if len(v) == 2 { + setting[k] = v[1] + } else { + setting[k] = k + } + } + return setting +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/mssql.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/mssql.go new file mode 100644 index 0000000..a9bd1e5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/mssql.go @@ -0,0 +1,80 @@ +package gorm + +import ( + "fmt" + "reflect" + "time" +) + +type mssql struct { + commonDialect +} + +func (mssql) HasTop() bool { + return true +} + +func (mssql) SqlTag(value reflect.Value, size int, autoIncrease bool) string { + switch value.Kind() { + case reflect.Bool: + return "bit" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + if autoIncrease { + return "int IDENTITY(1,1)" + } + return "int" + case reflect.Int64, reflect.Uint64: + if autoIncrease { + return "bigint IDENTITY(1,1)" + } + return "bigint" + case reflect.Float32, reflect.Float64: + return "float" + case reflect.String: + if size > 0 && size < 65532 { + return fmt.Sprintf("nvarchar(%d)", size) + } + return "text" + case reflect.Struct: + if _, ok := value.Interface().(time.Time); ok { + return "datetime2" + } + default: + if _, ok := value.Interface().([]byte); ok { + if size > 0 && size < 65532 { + return fmt.Sprintf("varchar(%d)", size) + } + return "text" + } + } + panic(fmt.Sprintf("invalid sql type %s (%s) for mssql", value.Type().Name(), value.Kind().String())) +} + +func (s mssql) HasTable(scope *Scope, tableName string) bool { + var ( + count int + databaseName = s.CurrentDatabase(scope) + ) + s.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.tables WHERE table_name = ? AND table_catalog = ?", tableName, databaseName) + return count > 0 +} + +func (s mssql) HasColumn(scope *Scope, tableName string, columnName string) bool { + var ( + count int + databaseName = s.CurrentDatabase(scope) + ) + s.RawScanInt(scope, &count, "SELECT count(*) FROM information_schema.columns WHERE table_catalog = ? AND table_name = ? AND column_name = ?", databaseName, tableName, columnName) + return count > 0 +} + +func (s mssql) HasIndex(scope *Scope, tableName string, indexName string) bool { + var count int + s.RawScanInt(scope, &count, "SELECT count(*) FROM sys.indexes WHERE name=? AND object_id=OBJECT_ID(?)", indexName, tableName) + return count > 0 +} + +func (s mssql) CurrentDatabase(scope *Scope) (name string) { + s.RawScanString(scope, &name, "SELECT DB_NAME() AS [Current Database]") + return +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/multi_primary_keys_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/multi_primary_keys_test.go new file mode 100644 index 0000000..9ca68d1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/multi_primary_keys_test.go @@ -0,0 +1,46 @@ +package gorm_test + +import ( + "fmt" + "os" + "testing" +) + +type Blog struct { + ID uint `gorm:"primary_key"` + Locale string `gorm:"primary_key"` + Subject string + Body string + Tags []Tag `gorm:"many2many:blog_tags;"` +} + +type Tag struct { + ID uint `gorm:"primary_key"` + Locale string `gorm:"primary_key"` + Value string +} + +func TestManyToManyWithMultiPrimaryKeys(t *testing.T) { + if dialect := os.Getenv("GORM_DIALECT"); dialect != "" && dialect != "sqlite" { + DB.Exec(fmt.Sprintf("drop table blog_tags;")) + DB.AutoMigrate(&Blog{}, &Tag{}) + blog := Blog{ + Locale: "ZH", + Subject: "subject", + Body: "body", + Tags: []Tag{ + {Locale: "ZH", Value: "tag1"}, + {Locale: "ZH", Value: "tag2"}, + }, + } + + DB.Save(&blog) + DB.Model(&blog).Association("Tags").Append([]Tag{{Locale: "ZH", Value: "tag3"}}) + + var tags []Tag + DB.Model(&blog).Related(&tags, "Tags") + if len(tags) != 3 { + t.Errorf("should found 3 tags with blog") + } + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/mysql.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/mysql.go new file mode 100644 index 0000000..9e1d56d --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/mysql.go @@ -0,0 +1,70 @@ +package gorm + +import ( + "fmt" + "reflect" + "time" +) + +type mysql struct { + commonDialect +} + +func (mysql) SqlTag(value reflect.Value, size int, autoIncrease bool) string { + switch value.Kind() { + case reflect.Bool: + return "boolean" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32: + if autoIncrease { + return "int AUTO_INCREMENT" + } + return "int" + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + if autoIncrease { + return "int unsigned AUTO_INCREMENT" + } + return "int unsigned" + case reflect.Int64: + if autoIncrease { + return "bigint AUTO_INCREMENT" + } + return "bigint" + case reflect.Uint64: + if autoIncrease { + return "bigint unsigned AUTO_INCREMENT" + } + return "bigint unsigned" + case reflect.Float32, reflect.Float64: + return "double" + case reflect.String: + if size > 0 && size < 65532 { + return fmt.Sprintf("varchar(%d)", size) + } + return "longtext" + case reflect.Struct: + if _, ok := value.Interface().(time.Time); ok { + return "timestamp NULL" + } + default: + if _, ok := value.Interface().([]byte); ok { + if size > 0 && size < 65532 { + return fmt.Sprintf("varbinary(%d)", size) + } + return "longblob" + } + } + panic(fmt.Sprintf("invalid sql type %s (%s) for mysql", value.Type().Name(), value.Kind().String())) +} + +func (mysql) Quote(key string) string { + return fmt.Sprintf("`%s`", key) +} + +func (mysql) SelectFromDummyTable() string { + return "FROM DUAL" +} + +func (s mysql) CurrentDatabase(scope *Scope) (name string) { + s.RawScanString(scope, &name, "SELECT DATABASE()") + return +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/pointer_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/pointer_test.go new file mode 100644 index 0000000..b47717f --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/pointer_test.go @@ -0,0 +1,84 @@ +package gorm_test + +import "testing" + +type PointerStruct struct { + ID int64 + Name *string + Num *int +} + +type NormalStruct struct { + ID int64 + Name string + Num int +} + +func TestPointerFields(t *testing.T) { + DB.DropTable(&PointerStruct{}) + DB.AutoMigrate(&PointerStruct{}) + var name = "pointer struct 1" + var num = 100 + pointerStruct := PointerStruct{Name: &name, Num: &num} + if DB.Create(&pointerStruct).Error != nil { + t.Errorf("Failed to save pointer struct") + } + + var pointerStructResult PointerStruct + if err := DB.First(&pointerStructResult, "id = ?", pointerStruct.ID).Error; err != nil || *pointerStructResult.Name != name || *pointerStructResult.Num != num { + t.Errorf("Failed to query saved pointer struct") + } + + var tableName = DB.NewScope(&PointerStruct{}).TableName() + + var normalStruct NormalStruct + DB.Table(tableName).First(&normalStruct) + if normalStruct.Name != name || normalStruct.Num != num { + t.Errorf("Failed to query saved Normal struct") + } + + var nilPointerStruct = PointerStruct{} + if err := DB.Create(&nilPointerStruct).Error; err != nil { + t.Errorf("Failed to save nil pointer struct", err) + } + + var pointerStruct2 PointerStruct + if err := DB.First(&pointerStruct2, "id = ?", nilPointerStruct.ID).Error; err != nil { + t.Errorf("Failed to query saved nil pointer struct", err) + } + + var normalStruct2 NormalStruct + if err := DB.Table(tableName).First(&normalStruct2, "id = ?", nilPointerStruct.ID).Error; err != nil { + t.Errorf("Failed to query saved nil pointer struct", err) + } + + var partialNilPointerStruct1 = PointerStruct{Num: &num} + if err := DB.Create(&partialNilPointerStruct1).Error; err != nil { + t.Errorf("Failed to save partial nil pointer struct", err) + } + + var pointerStruct3 PointerStruct + if err := DB.First(&pointerStruct3, "id = ?", partialNilPointerStruct1.ID).Error; err != nil || *pointerStruct3.Num != num { + t.Errorf("Failed to query saved partial nil pointer struct", err) + } + + var normalStruct3 NormalStruct + if err := DB.Table(tableName).First(&normalStruct3, "id = ?", partialNilPointerStruct1.ID).Error; err != nil || normalStruct3.Num != num { + t.Errorf("Failed to query saved partial pointer struct", err) + } + + var partialNilPointerStruct2 = PointerStruct{Name: &name} + if err := DB.Create(&partialNilPointerStruct2).Error; err != nil { + t.Errorf("Failed to save partial nil pointer struct", err) + } + + var pointerStruct4 PointerStruct + if err := DB.First(&pointerStruct4, "id = ?", partialNilPointerStruct2.ID).Error; err != nil || *pointerStruct4.Name != name { + t.Errorf("Failed to query saved partial nil pointer struct", err) + } + + var normalStruct4 NormalStruct + if err := DB.Table(tableName).First(&normalStruct4, "id = ?", partialNilPointerStruct2.ID).Error; err != nil || normalStruct4.Name != name { + t.Errorf("Failed to query saved partial pointer struct", err) + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/polymorphic_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/polymorphic_test.go new file mode 100644 index 0000000..78b99fe --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/polymorphic_test.go @@ -0,0 +1,56 @@ +package gorm_test + +import "testing" + +type Cat struct { + Id int + Name string + Toy Toy `gorm:"polymorphic:Owner;"` +} + +type Dog struct { + Id int + Name string + Toys []Toy `gorm:"polymorphic:Owner;"` +} + +type Toy struct { + Id int + Name string + OwnerId int + OwnerType string +} + +func TestPolymorphic(t *testing.T) { + DB.AutoMigrate(&Cat{}) + DB.AutoMigrate(&Dog{}) + DB.AutoMigrate(&Toy{}) + + cat := Cat{Name: "Mr. Bigglesworth", Toy: Toy{Name: "cat nip"}} + dog := Dog{Name: "Pluto", Toys: []Toy{Toy{Name: "orange ball"}, Toy{Name: "yellow ball"}}} + DB.Save(&cat).Save(&dog) + + var catToys []Toy + if DB.Model(&cat).Related(&catToys, "Toy").RecordNotFound() { + t.Errorf("Did not find any has one polymorphic association") + } else if len(catToys) != 1 { + t.Errorf("Should have found only one polymorphic has one association") + } else if catToys[0].Name != cat.Toy.Name { + t.Errorf("Should have found the proper has one polymorphic association") + } + + var dogToys []Toy + if DB.Model(&dog).Related(&dogToys, "Toys").RecordNotFound() { + t.Errorf("Did not find any polymorphic has many associations") + } else if len(dogToys) != len(dog.Toys) { + t.Errorf("Should have found all polymorphic has many associations") + } + + if DB.Model(&cat).Association("Toy").Count() != 1 { + t.Errorf("Should return one polymorphic has one association") + } + + if DB.Model(&dog).Association("Toys").Count() != 2 { + t.Errorf("Should return two polymorphic has many associations") + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/postgres.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/postgres.go new file mode 100644 index 0000000..357f984 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/postgres.go @@ -0,0 +1,136 @@ +package gorm + +import ( + "database/sql" + "database/sql/driver" + "fmt" + "reflect" + "time" + + "github.com/lib/pq/hstore" +) + +type postgres struct { + commonDialect +} + +func (postgres) BinVar(i int) string { + return fmt.Sprintf("$%v", i) +} + +func (postgres) SupportLastInsertId() bool { + return false +} + +func (postgres) SqlTag(value reflect.Value, size int, autoIncrease bool) string { + switch value.Kind() { + case reflect.Bool: + return "boolean" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + if autoIncrease { + return "serial" + } + return "integer" + case reflect.Int64, reflect.Uint64: + if autoIncrease { + return "bigserial" + } + return "bigint" + case reflect.Float32, reflect.Float64: + return "numeric" + case reflect.String: + if size > 0 && size < 65532 { + return fmt.Sprintf("varchar(%d)", size) + } + return "text" + case reflect.Struct: + if _, ok := value.Interface().(time.Time); ok { + return "timestamp with time zone" + } + case reflect.Map: + if value.Type() == hstoreType { + return "hstore" + } + default: + if _, ok := value.Interface().([]byte); ok { + return "bytea" + } + } + panic(fmt.Sprintf("invalid sql type %s (%s) for postgres", value.Type().Name(), value.Kind().String())) +} + +func (s postgres) ReturningStr(tableName, key string) string { + return fmt.Sprintf("RETURNING %v.%v", s.Quote(tableName), key) +} + +func (s postgres) HasTable(scope *Scope, tableName string) bool { + var count int + s.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.tables WHERE table_name = ? AND table_type = 'BASE TABLE'", tableName) + return count > 0 +} + +func (s postgres) HasColumn(scope *Scope, tableName string, columnName string) bool { + var count int + s.RawScanInt(scope, &count, "SELECT count(*) FROM INFORMATION_SCHEMA.columns WHERE table_name = ? AND column_name = ?", tableName, columnName) + return count > 0 +} + +func (postgres) RemoveIndex(scope *Scope, indexName string) { + scope.Err(scope.NewDB().Exec(fmt.Sprintf("DROP INDEX %v", indexName)).Error) +} + +func (s postgres) HasIndex(scope *Scope, tableName string, indexName string) bool { + var count int + s.RawScanInt(scope, &count, "SELECT count(*) FROM pg_indexes WHERE tablename = ? AND indexname = ?", tableName, indexName) + return count > 0 +} + +func (s postgres) CurrentDatabase(scope *Scope) (name string) { + s.RawScanString(scope, &name, "SELECT CURRENT_DATABASE()") + return +} + +var hstoreType = reflect.TypeOf(Hstore{}) + +type Hstore map[string]*string + +func (h Hstore) Value() (driver.Value, error) { + hstore := hstore.Hstore{Map: map[string]sql.NullString{}} + if len(h) == 0 { + return nil, nil + } + + for key, value := range h { + var s sql.NullString + if value != nil { + s.String = *value + s.Valid = true + } + hstore.Map[key] = s + } + return hstore.Value() +} + +func (h *Hstore) Scan(value interface{}) error { + hstore := hstore.Hstore{} + + if err := hstore.Scan(value); err != nil { + return err + } + + if len(hstore.Map) == 0 { + return nil + } + + *h = Hstore{} + for k := range hstore.Map { + if hstore.Map[k].Valid { + s := hstore.Map[k].String + (*h)[k] = &s + } else { + (*h)[k] = nil + } + } + + return nil +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/preload.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/preload.go new file mode 100644 index 0000000..0db6fbd --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/preload.go @@ -0,0 +1,246 @@ +package gorm + +import ( + "database/sql/driver" + "errors" + "fmt" + "reflect" + "strings" +) + +func getRealValue(value reflect.Value, columns []string) (results []interface{}) { + for _, column := range columns { + result := reflect.Indirect(value).FieldByName(column).Interface() + if r, ok := result.(driver.Valuer); ok { + result, _ = r.Value() + } + results = append(results, result) + } + return +} + +func equalAsString(a interface{}, b interface{}) bool { + return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b) +} + +func Preload(scope *Scope) { + if scope.Search.preload == nil { + return + } + + preloadMap := map[string]bool{} + fields := scope.Fields() + for _, preload := range scope.Search.preload { + schema, conditions := preload.schema, preload.conditions + keys := strings.Split(schema, ".") + currentScope := scope + currentFields := fields + originalConditions := conditions + conditions = []interface{}{} + for i, key := range keys { + var found bool + if preloadMap[strings.Join(keys[:i+1], ".")] { + goto nextLoop + } + + if i == len(keys)-1 { + conditions = originalConditions + } + + for _, field := range currentFields { + if field.Name != key || field.Relationship == nil { + continue + } + + found = true + switch field.Relationship.Kind { + case "has_one": + currentScope.handleHasOnePreload(field, conditions) + case "has_many": + currentScope.handleHasManyPreload(field, conditions) + case "belongs_to": + currentScope.handleBelongsToPreload(field, conditions) + case "many_to_many": + fallthrough + default: + currentScope.Err(errors.New("not supported relation")) + } + break + } + + if !found { + value := reflect.ValueOf(currentScope.Value) + if value.Kind() == reflect.Slice && value.Type().Elem().Kind() == reflect.Interface { + value = value.Index(0).Elem() + } + scope.Err(fmt.Errorf("can't find field %s in %s", key, value.Type())) + return + } + + preloadMap[strings.Join(keys[:i+1], ".")] = true + + nextLoop: + if i < len(keys)-1 { + currentScope = currentScope.getColumnsAsScope(key) + currentFields = currentScope.Fields() + } + } + } + +} + +func makeSlice(typ reflect.Type) interface{} { + if typ.Kind() == reflect.Slice { + typ = typ.Elem() + } + sliceType := reflect.SliceOf(typ) + slice := reflect.New(sliceType) + slice.Elem().Set(reflect.MakeSlice(sliceType, 0, 0)) + return slice.Interface() +} + +func (scope *Scope) handleHasOnePreload(field *Field, conditions []interface{}) { + relation := field.Relationship + + primaryKeys := scope.getColumnAsArray(relation.AssociationForeignFieldNames) + if len(primaryKeys) == 0 { + return + } + + results := makeSlice(field.Struct.Type) + scope.Err(scope.NewDB().Where(fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, relation.ForeignDBNames), toQueryMarks(primaryKeys)), toQueryValues(primaryKeys)...).Find(results, conditions...).Error) + resultValues := reflect.Indirect(reflect.ValueOf(results)) + + for i := 0; i < resultValues.Len(); i++ { + result := resultValues.Index(i) + if scope.IndirectValue().Kind() == reflect.Slice { + value := getRealValue(result, relation.ForeignFieldNames) + objects := scope.IndirectValue() + for j := 0; j < objects.Len(); j++ { + if equalAsString(getRealValue(objects.Index(j), relation.AssociationForeignFieldNames), value) { + reflect.Indirect(objects.Index(j)).FieldByName(field.Name).Set(result) + break + } + } + } else { + if err := scope.SetColumn(field, result); err != nil { + scope.Err(err) + return + } + } + } +} + +func (scope *Scope) handleHasManyPreload(field *Field, conditions []interface{}) { + relation := field.Relationship + primaryKeys := scope.getColumnAsArray(relation.AssociationForeignFieldNames) + if len(primaryKeys) == 0 { + return + } + + results := makeSlice(field.Struct.Type) + scope.Err(scope.NewDB().Where(fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, relation.ForeignDBNames), toQueryMarks(primaryKeys)), toQueryValues(primaryKeys)...).Find(results, conditions...).Error) + resultValues := reflect.Indirect(reflect.ValueOf(results)) + + if scope.IndirectValue().Kind() == reflect.Slice { + for i := 0; i < resultValues.Len(); i++ { + result := resultValues.Index(i) + value := getRealValue(result, relation.ForeignFieldNames) + objects := scope.IndirectValue() + for j := 0; j < objects.Len(); j++ { + object := reflect.Indirect(objects.Index(j)) + if equalAsString(getRealValue(object, relation.AssociationForeignFieldNames), value) { + f := object.FieldByName(field.Name) + f.Set(reflect.Append(f, result)) + break + } + } + } + } else { + scope.SetColumn(field, resultValues) + } +} + +func (scope *Scope) handleBelongsToPreload(field *Field, conditions []interface{}) { + relation := field.Relationship + primaryKeys := scope.getColumnAsArray(relation.ForeignFieldNames) + if len(primaryKeys) == 0 { + return + } + + results := makeSlice(field.Struct.Type) + scope.Err(scope.NewDB().Where(fmt.Sprintf("%v IN (%v)", toQueryCondition(scope, relation.AssociationForeignDBNames), toQueryMarks(primaryKeys)), toQueryValues(primaryKeys)...).Find(results, conditions...).Error) + resultValues := reflect.Indirect(reflect.ValueOf(results)) + + for i := 0; i < resultValues.Len(); i++ { + result := resultValues.Index(i) + if scope.IndirectValue().Kind() == reflect.Slice { + value := getRealValue(result, relation.AssociationForeignFieldNames) + objects := scope.IndirectValue() + for j := 0; j < objects.Len(); j++ { + object := reflect.Indirect(objects.Index(j)) + if equalAsString(getRealValue(object, relation.ForeignFieldNames), value) { + object.FieldByName(field.Name).Set(result) + } + } + } else { + scope.SetColumn(field, result) + } + } +} + +func (scope *Scope) getColumnAsArray(columns []string) (results [][]interface{}) { + values := scope.IndirectValue() + switch values.Kind() { + case reflect.Slice: + for i := 0; i < values.Len(); i++ { + var result []interface{} + for _, column := range columns { + result = append(result, reflect.Indirect(values.Index(i)).FieldByName(column).Interface()) + } + results = append(results, result) + } + case reflect.Struct: + var result []interface{} + for _, column := range columns { + result = append(result, values.FieldByName(column).Interface()) + } + return [][]interface{}{result} + } + return +} + +func (scope *Scope) getColumnsAsScope(column string) *Scope { + values := scope.IndirectValue() + switch values.Kind() { + case reflect.Slice: + modelType := values.Type().Elem() + if modelType.Kind() == reflect.Ptr { + modelType = modelType.Elem() + } + fieldStruct, _ := modelType.FieldByName(column) + var columns reflect.Value + if fieldStruct.Type.Kind() == reflect.Slice || fieldStruct.Type.Kind() == reflect.Ptr { + columns = reflect.New(reflect.SliceOf(reflect.PtrTo(fieldStruct.Type.Elem()))).Elem() + } else { + columns = reflect.New(reflect.SliceOf(reflect.PtrTo(fieldStruct.Type))).Elem() + } + for i := 0; i < values.Len(); i++ { + column := reflect.Indirect(values.Index(i)).FieldByName(column) + if column.Kind() == reflect.Ptr { + column = column.Elem() + } + if column.Kind() == reflect.Slice { + for i := 0; i < column.Len(); i++ { + columns = reflect.Append(columns, column.Index(i).Addr()) + } + } else { + columns = reflect.Append(columns, column.Addr()) + } + } + return scope.New(columns.Interface()) + case reflect.Struct: + return scope.New(values.FieldByName(column).Addr().Interface()) + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/preload_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/preload_test.go new file mode 100644 index 0000000..a6647bb --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/preload_test.go @@ -0,0 +1,609 @@ +package gorm_test + +import ( + "encoding/json" + "reflect" + "testing" +) + +func getPreloadUser(name string) *User { + return getPreparedUser(name, "Preload") +} + +func checkUserHasPreloadData(user User, t *testing.T) { + u := getPreloadUser(user.Name) + if user.BillingAddress.Address1 != u.BillingAddress.Address1 { + t.Error("Failed to preload user's BillingAddress") + } + + if user.ShippingAddress.Address1 != u.ShippingAddress.Address1 { + t.Error("Failed to preload user's ShippingAddress") + } + + if user.CreditCard.Number != u.CreditCard.Number { + t.Error("Failed to preload user's CreditCard") + } + + if user.Company.Name != u.Company.Name { + t.Error("Failed to preload user's Company") + } + + if len(user.Emails) != len(u.Emails) { + t.Error("Failed to preload user's Emails") + } else { + var found int + for _, e1 := range u.Emails { + for _, e2 := range user.Emails { + if e1.Email == e2.Email { + found++ + break + } + } + } + if found != len(u.Emails) { + t.Error("Failed to preload user's email details") + } + } +} + +func TestPreload(t *testing.T) { + user1 := getPreloadUser("user1") + DB.Save(user1) + + preloadDB := DB.Where("role = ?", "Preload").Preload("BillingAddress").Preload("ShippingAddress"). + Preload("CreditCard").Preload("Emails").Preload("Company") + var user User + preloadDB.Find(&user) + checkUserHasPreloadData(user, t) + + user2 := getPreloadUser("user2") + DB.Save(user2) + + user3 := getPreloadUser("user3") + DB.Save(user3) + + var users []User + preloadDB.Find(&users) + + for _, user := range users { + checkUserHasPreloadData(user, t) + } + + var users2 []*User + preloadDB.Find(&users2) + + for _, user := range users2 { + checkUserHasPreloadData(*user, t) + } + + var users3 []*User + preloadDB.Preload("Emails", "email = ?", user3.Emails[0].Email).Find(&users3) + + for _, user := range users3 { + if user.Name == user3.Name { + if len(user.Emails) != 1 { + t.Errorf("should only preload one emails for user3 when with condition") + } + } else if len(user.Emails) != 0 { + t.Errorf("should not preload any emails for other users when with condition") + } + } +} + +func TestNestedPreload1(t *testing.T) { + type ( + Level1 struct { + ID uint + Value string + Level2ID uint + } + Level2 struct { + ID uint + Level1 Level1 + Level3ID uint + } + Level3 struct { + ID uint + Name string + Level2 Level2 + } + ) + DB.DropTableIfExists(&Level3{}) + DB.DropTableIfExists(&Level2{}) + DB.DropTableIfExists(&Level1{}) + if err := DB.AutoMigrate(&Level3{}, &Level2{}, &Level1{}).Error; err != nil { + panic(err) + } + + want := Level3{Level2: Level2{Level1: Level1{Value: "value"}}} + if err := DB.Create(&want).Error; err != nil { + panic(err) + } + + var got Level3 + if err := DB.Preload("Level2").Preload("Level2.Level1").Find(&got).Error; err != nil { + panic(err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("got %s; want %s", toJSONString(got), toJSONString(want)) + } +} + +func TestNestedPreload2(t *testing.T) { + type ( + Level1 struct { + ID uint + Value string + Level2ID uint + } + Level2 struct { + ID uint + Level1s []*Level1 + Level3ID uint + } + Level3 struct { + ID uint + Name string + Level2s []Level2 + } + ) + DB.DropTableIfExists(&Level3{}) + DB.DropTableIfExists(&Level2{}) + DB.DropTableIfExists(&Level1{}) + if err := DB.AutoMigrate(&Level3{}, &Level2{}, &Level1{}).Error; err != nil { + panic(err) + } + + want := Level3{ + Level2s: []Level2{ + { + Level1s: []*Level1{ + &Level1{Value: "value1"}, + &Level1{Value: "value2"}, + }, + }, + { + Level1s: []*Level1{ + &Level1{Value: "value3"}, + }, + }, + }, + } + if err := DB.Create(&want).Error; err != nil { + panic(err) + } + + var got Level3 + if err := DB.Preload("Level2s.Level1s").Find(&got).Error; err != nil { + panic(err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("got %s; want %s", toJSONString(got), toJSONString(want)) + } +} + +func TestNestedPreload3(t *testing.T) { + type ( + Level1 struct { + ID uint + Value string + Level2ID uint + } + Level2 struct { + ID uint + Level1 Level1 + Level3ID uint + } + Level3 struct { + Name string + ID uint + Level2s []Level2 + } + ) + DB.DropTableIfExists(&Level3{}) + DB.DropTableIfExists(&Level2{}) + DB.DropTableIfExists(&Level1{}) + if err := DB.AutoMigrate(&Level3{}, &Level2{}, &Level1{}).Error; err != nil { + panic(err) + } + + want := Level3{ + Level2s: []Level2{ + {Level1: Level1{Value: "value1"}}, + {Level1: Level1{Value: "value2"}}, + }, + } + if err := DB.Create(&want).Error; err != nil { + panic(err) + } + + var got Level3 + if err := DB.Preload("Level2s.Level1").Find(&got).Error; err != nil { + panic(err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("got %s; want %s", toJSONString(got), toJSONString(want)) + } +} + +func TestNestedPreload4(t *testing.T) { + type ( + Level1 struct { + ID uint + Value string + Level2ID uint + } + Level2 struct { + ID uint + Level1s []Level1 + Level3ID uint + } + Level3 struct { + ID uint + Name string + Level2 Level2 + } + ) + DB.DropTableIfExists(&Level3{}) + DB.DropTableIfExists(&Level2{}) + DB.DropTableIfExists(&Level1{}) + if err := DB.AutoMigrate(&Level3{}, &Level2{}, &Level1{}).Error; err != nil { + panic(err) + } + + want := Level3{ + Level2: Level2{ + Level1s: []Level1{ + Level1{Value: "value1"}, + Level1{Value: "value2"}, + }, + }, + } + if err := DB.Create(&want).Error; err != nil { + panic(err) + } + + var got Level3 + if err := DB.Preload("Level2.Level1s").Find(&got).Error; err != nil { + panic(err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("got %s; want %s", toJSONString(got), toJSONString(want)) + } +} + +// Slice: []Level3 +func TestNestedPreload5(t *testing.T) { + type ( + Level1 struct { + ID uint + Value string + Level2ID uint + } + Level2 struct { + ID uint + Level1 Level1 + Level3ID uint + } + Level3 struct { + ID uint + Name string + Level2 Level2 + } + ) + DB.DropTableIfExists(&Level3{}) + DB.DropTableIfExists(&Level2{}) + DB.DropTableIfExists(&Level1{}) + if err := DB.AutoMigrate(&Level3{}, &Level2{}, &Level1{}).Error; err != nil { + panic(err) + } + + want := make([]Level3, 2) + want[0] = Level3{Level2: Level2{Level1: Level1{Value: "value"}}} + if err := DB.Create(&want[0]).Error; err != nil { + panic(err) + } + want[1] = Level3{Level2: Level2{Level1: Level1{Value: "value2"}}} + if err := DB.Create(&want[1]).Error; err != nil { + panic(err) + } + + var got []Level3 + if err := DB.Preload("Level2").Preload("Level2.Level1").Find(&got).Error; err != nil { + panic(err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("got %s; want %s", toJSONString(got), toJSONString(want)) + } +} + +func TestNestedPreload6(t *testing.T) { + type ( + Level1 struct { + ID uint + Value string + Level2ID uint + } + Level2 struct { + ID uint + Level1s []Level1 + Level3ID uint + } + Level3 struct { + ID uint + Name string + Level2s []Level2 + } + ) + DB.DropTableIfExists(&Level3{}) + DB.DropTableIfExists(&Level2{}) + DB.DropTableIfExists(&Level1{}) + if err := DB.AutoMigrate(&Level3{}, &Level2{}, &Level1{}).Error; err != nil { + panic(err) + } + + want := make([]Level3, 2) + want[0] = Level3{ + Level2s: []Level2{ + { + Level1s: []Level1{ + {Value: "value1"}, + {Value: "value2"}, + }, + }, + { + Level1s: []Level1{ + {Value: "value3"}, + }, + }, + }, + } + if err := DB.Create(&want[0]).Error; err != nil { + panic(err) + } + + want[1] = Level3{ + Level2s: []Level2{ + { + Level1s: []Level1{ + {Value: "value3"}, + {Value: "value4"}, + }, + }, + { + Level1s: []Level1{ + {Value: "value5"}, + }, + }, + }, + } + if err := DB.Create(&want[1]).Error; err != nil { + panic(err) + } + + var got []Level3 + if err := DB.Preload("Level2s.Level1s").Find(&got).Error; err != nil { + panic(err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("got %s; want %s", toJSONString(got), toJSONString(want)) + } +} + +func TestNestedPreload7(t *testing.T) { + type ( + Level1 struct { + ID uint + Value string + Level2ID uint + } + Level2 struct { + ID uint + Level1 Level1 + Level3ID uint + } + Level3 struct { + ID uint + Name string + Level2s []Level2 + } + ) + DB.DropTableIfExists(&Level3{}) + DB.DropTableIfExists(&Level2{}) + DB.DropTableIfExists(&Level1{}) + if err := DB.AutoMigrate(&Level3{}, &Level2{}, &Level1{}).Error; err != nil { + panic(err) + } + + want := make([]Level3, 2) + want[0] = Level3{ + Level2s: []Level2{ + {Level1: Level1{Value: "value1"}}, + {Level1: Level1{Value: "value2"}}, + }, + } + if err := DB.Create(&want[0]).Error; err != nil { + panic(err) + } + + want[1] = Level3{ + Level2s: []Level2{ + {Level1: Level1{Value: "value3"}}, + {Level1: Level1{Value: "value4"}}, + }, + } + if err := DB.Create(&want[1]).Error; err != nil { + panic(err) + } + + var got []Level3 + if err := DB.Preload("Level2s.Level1").Find(&got).Error; err != nil { + panic(err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("got %s; want %s", toJSONString(got), toJSONString(want)) + } +} + +func TestNestedPreload8(t *testing.T) { + type ( + Level1 struct { + ID uint + Value string + Level2ID uint + } + Level2 struct { + ID uint + Level1s []Level1 + Level3ID uint + } + Level3 struct { + ID uint + Name string + Level2 Level2 + } + ) + DB.DropTableIfExists(&Level3{}) + DB.DropTableIfExists(&Level2{}) + DB.DropTableIfExists(&Level1{}) + if err := DB.AutoMigrate(&Level3{}, &Level2{}, &Level1{}).Error; err != nil { + panic(err) + } + + want := make([]Level3, 2) + want[0] = Level3{ + Level2: Level2{ + Level1s: []Level1{ + Level1{Value: "value1"}, + Level1{Value: "value2"}, + }, + }, + } + if err := DB.Create(&want[0]).Error; err != nil { + panic(err) + } + want[1] = Level3{ + Level2: Level2{ + Level1s: []Level1{ + Level1{Value: "value3"}, + Level1{Value: "value4"}, + }, + }, + } + if err := DB.Create(&want[1]).Error; err != nil { + panic(err) + } + + var got []Level3 + if err := DB.Preload("Level2.Level1s").Find(&got).Error; err != nil { + panic(err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("got %s; want %s", toJSONString(got), toJSONString(want)) + } +} + +func TestNestedPreload9(t *testing.T) { + type ( + Level0 struct { + ID uint + Value string + Level1ID uint + } + Level1 struct { + ID uint + Value string + Level2ID uint + Level2_1ID uint + Level0s []Level0 + } + Level2 struct { + ID uint + Level1s []Level1 + Level3ID uint + } + Level2_1 struct { + ID uint + Level1s []Level1 + Level3ID uint + } + Level3 struct { + ID uint + Name string + Level2 Level2 + Level2_1 Level2_1 + } + ) + DB.DropTableIfExists(&Level3{}) + DB.DropTableIfExists(&Level2{}) + DB.DropTableIfExists(&Level2_1{}) + DB.DropTableIfExists(&Level1{}) + DB.DropTableIfExists(&Level0{}) + if err := DB.AutoMigrate(&Level3{}, &Level2{}, &Level1{}, &Level2_1{}, &Level0{}).Error; err != nil { + panic(err) + } + + want := make([]Level3, 2) + want[0] = Level3{ + Level2: Level2{ + Level1s: []Level1{ + Level1{Value: "value1"}, + Level1{Value: "value2"}, + }, + }, + Level2_1: Level2_1{ + Level1s: []Level1{ + Level1{ + Value: "value1-1", + Level0s: []Level0{{Value: "Level0-1"}}, + }, + Level1{ + Value: "value2-2", + Level0s: []Level0{{Value: "Level0-2"}}, + }, + }, + }, + } + if err := DB.Create(&want[0]).Error; err != nil { + panic(err) + } + want[1] = Level3{ + Level2: Level2{ + Level1s: []Level1{ + Level1{Value: "value3"}, + Level1{Value: "value4"}, + }, + }, + Level2_1: Level2_1{ + Level1s: []Level1{ + Level1{Value: "value3-3"}, + Level1{Value: "value4-4"}, + }, + }, + } + if err := DB.Create(&want[1]).Error; err != nil { + panic(err) + } + + var got []Level3 + if err := DB.Preload("Level2").Preload("Level2.Level1s").Preload("Level2_1").Preload("Level2_1.Level1s").Preload("Level2_1.Level1s.Level0s").Find(&got).Error; err != nil { + panic(err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("got %s; want %s", toJSONString(got), toJSONString(want)) + } +} + +func toJSONString(v interface{}) []byte { + r, _ := json.MarshalIndent(v, "", " ") + return r +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/query_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/query_test.go new file mode 100644 index 0000000..0fd5830 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/query_test.go @@ -0,0 +1,592 @@ +package gorm_test + +import ( + "fmt" + "reflect" + + "github.com/jinzhu/gorm" + "github.com/jinzhu/now" + + "testing" + "time" +) + +func TestFirstAndLast(t *testing.T) { + DB.Save(&User{Name: "user1", Emails: []Email{{Email: "user1@example.com"}}}) + DB.Save(&User{Name: "user2", Emails: []Email{{Email: "user2@example.com"}}}) + + var user1, user2, user3, user4 User + DB.First(&user1) + DB.Order("id").Limit(1).Find(&user2) + + DB.Last(&user3) + DB.Order("id desc").Limit(1).Find(&user4) + if user1.Id != user2.Id || user3.Id != user4.Id { + t.Errorf("First and Last should by order by primary key") + } + + var users []User + DB.First(&users) + if len(users) != 1 { + t.Errorf("Find first record as slice") + } + + if DB.Joins("left join emails on emails.user_id = users.id").First(&User{}).Error != nil { + t.Errorf("Should not raise any error when order with Join table") + } +} + +func TestFirstAndLastWithNoStdPrimaryKey(t *testing.T) { + DB.Save(&Animal{Name: "animal1"}) + DB.Save(&Animal{Name: "animal2"}) + + var animal1, animal2, animal3, animal4 Animal + DB.First(&animal1) + DB.Order("counter").Limit(1).Find(&animal2) + + DB.Last(&animal3) + DB.Order("counter desc").Limit(1).Find(&animal4) + if animal1.Counter != animal2.Counter || animal3.Counter != animal4.Counter { + t.Errorf("First and Last should work correctly") + } +} + +func TestUIntPrimaryKey(t *testing.T) { + var animal Animal + DB.First(&animal, uint64(1)) + if animal.Counter != 1 { + t.Errorf("Fetch a record from with a non-int primary key should work, but failed") + } + + DB.Model(Animal{}).Where(Animal{Counter: uint64(2)}).Scan(&animal) + if animal.Counter != 2 { + t.Errorf("Fetch a record from with a non-int primary key should work, but failed") + } +} + +func TestFindAsSliceOfPointers(t *testing.T) { + DB.Save(&User{Name: "user"}) + + var users []User + DB.Find(&users) + + var userPointers []*User + DB.Find(&userPointers) + + if len(users) == 0 || len(users) != len(userPointers) { + t.Errorf("Find slice of pointers") + } +} + +func TestSearchWithPlainSQL(t *testing.T) { + user1 := User{Name: "PlainSqlUser1", Age: 1, Birthday: now.MustParse("2000-1-1")} + user2 := User{Name: "PlainSqlUser2", Age: 10, Birthday: now.MustParse("2010-1-1")} + user3 := User{Name: "PlainSqlUser3", Age: 20, Birthday: now.MustParse("2020-1-1")} + DB.Save(&user1).Save(&user2).Save(&user3) + scopedb := DB.Where("name LIKE ?", "%PlainSqlUser%") + + if DB.Where("name = ?", user1.Name).First(&User{}).RecordNotFound() { + t.Errorf("Search with plain SQL") + } + + if DB.Where("name LIKE ?", "%"+user1.Name+"%").First(&User{}).RecordNotFound() { + t.Errorf("Search with plan SQL (regexp)") + } + + var users []User + DB.Find(&users, "name LIKE ? and age > ?", "%PlainSqlUser%", 1) + if len(users) != 2 { + t.Errorf("Should found 2 users that age > 1, but got %v", len(users)) + } + + DB.Where("name LIKE ?", "%PlainSqlUser%").Where("age >= ?", 1).Find(&users) + if len(users) != 3 { + t.Errorf("Should found 3 users that age >= 1, but got %v", len(users)) + } + + scopedb.Where("age <> ?", 20).Find(&users) + if len(users) != 2 { + t.Errorf("Should found 2 users age != 20, but got %v", len(users)) + } + + scopedb.Where("birthday > ?", now.MustParse("2000-1-1")).Find(&users) + if len(users) != 2 { + t.Errorf("Should found 2 users's birthday > 2000-1-1, but got %v", len(users)) + } + + scopedb.Where("birthday > ?", "2002-10-10").Find(&users) + if len(users) != 2 { + t.Errorf("Should found 2 users's birthday >= 2002-10-10, but got %v", len(users)) + } + + scopedb.Where("birthday >= ?", "2010-1-1").Where("birthday < ?", "2020-1-1").Find(&users) + if len(users) != 1 { + t.Errorf("Should found 1 users's birthday < 2020-1-1 and >= 2010-1-1, but got %v", len(users)) + } + + DB.Where("name in (?)", []string{user1.Name, user2.Name}).Find(&users) + if len(users) != 2 { + t.Errorf("Should found 2 users, but got %v", len(users)) + } + + DB.Where("id in (?)", []int64{user1.Id, user2.Id, user3.Id}).Find(&users) + if len(users) != 3 { + t.Errorf("Should found 3 users, but got %v", len(users)) + } + + DB.Where("id in (?)", user1.Id).Find(&users) + if len(users) != 1 { + t.Errorf("Should found 1 users, but got %v", len(users)) + } + + if DB.Where("name = ?", "none existing").Find(&[]User{}).RecordNotFound() { + t.Errorf("Should not get RecordNotFound error when looking for none existing records") + } +} + +func TestSearchWithStruct(t *testing.T) { + user1 := User{Name: "StructSearchUser1", Age: 1, Birthday: now.MustParse("2000-1-1")} + user2 := User{Name: "StructSearchUser2", Age: 10, Birthday: now.MustParse("2010-1-1")} + user3 := User{Name: "StructSearchUser3", Age: 20, Birthday: now.MustParse("2020-1-1")} + DB.Save(&user1).Save(&user2).Save(&user3) + + if DB.Where(user1.Id).First(&User{}).RecordNotFound() { + t.Errorf("Search with primary key") + } + + if DB.First(&User{}, user1.Id).RecordNotFound() { + t.Errorf("Search with primary key as inline condition") + } + + if DB.First(&User{}, fmt.Sprintf("%v", user1.Id)).RecordNotFound() { + t.Errorf("Search with primary key as inline condition") + } + + var users []User + DB.Where([]int64{user1.Id, user2.Id, user3.Id}).Find(&users) + if len(users) != 3 { + t.Errorf("Should found 3 users when search with primary keys, but got %v", len(users)) + } + + var user User + DB.First(&user, &User{Name: user1.Name}) + if user.Id == 0 || user.Name != user1.Name { + t.Errorf("Search first record with inline pointer of struct") + } + + DB.First(&user, User{Name: user1.Name}) + if user.Id == 0 || user.Name != user.Name { + t.Errorf("Search first record with inline struct") + } + + DB.Where(&User{Name: user1.Name}).First(&user) + if user.Id == 0 || user.Name != user1.Name { + t.Errorf("Search first record with where struct") + } + + DB.Find(&users, &User{Name: user2.Name}) + if len(users) != 1 { + t.Errorf("Search all records with inline struct") + } +} + +func TestSearchWithMap(t *testing.T) { + user1 := User{Name: "MapSearchUser1", Age: 1, Birthday: now.MustParse("2000-1-1")} + user2 := User{Name: "MapSearchUser2", Age: 10, Birthday: now.MustParse("2010-1-1")} + user3 := User{Name: "MapSearchUser3", Age: 20, Birthday: now.MustParse("2020-1-1")} + DB.Save(&user1).Save(&user2).Save(&user3) + + var user User + DB.First(&user, map[string]interface{}{"name": user1.Name}) + if user.Id == 0 || user.Name != user1.Name { + t.Errorf("Search first record with inline map") + } + + user = User{} + DB.Where(map[string]interface{}{"name": user2.Name}).First(&user) + if user.Id == 0 || user.Name != user2.Name { + t.Errorf("Search first record with where map") + } + + var users []User + DB.Where(map[string]interface{}{"name": user3.Name}).Find(&users) + if len(users) != 1 { + t.Errorf("Search all records with inline map") + } + + DB.Find(&users, map[string]interface{}{"name": user3.Name}) + if len(users) != 1 { + t.Errorf("Search all records with inline map") + } +} + +func TestSearchWithEmptyChain(t *testing.T) { + user1 := User{Name: "ChainSearchUser1", Age: 1, Birthday: now.MustParse("2000-1-1")} + user2 := User{Name: "ChainearchUser2", Age: 10, Birthday: now.MustParse("2010-1-1")} + user3 := User{Name: "ChainearchUser3", Age: 20, Birthday: now.MustParse("2020-1-1")} + DB.Save(&user1).Save(&user2).Save(&user3) + + if DB.Where("").Where("").First(&User{}).Error != nil { + t.Errorf("Should not raise any error if searching with empty strings") + } + + if DB.Where(&User{}).Where("name = ?", user1.Name).First(&User{}).Error != nil { + t.Errorf("Should not raise any error if searching with empty struct") + } + + if DB.Where(map[string]interface{}{}).Where("name = ?", user1.Name).First(&User{}).Error != nil { + t.Errorf("Should not raise any error if searching with empty map") + } +} + +func TestSelect(t *testing.T) { + user1 := User{Name: "SelectUser1"} + DB.Save(&user1) + + var user User + DB.Where("name = ?", user1.Name).Select("name").Find(&user) + if user.Id != 0 { + t.Errorf("Should not have ID because only selected name, %+v", user.Id) + } + + if user.Name != user1.Name { + t.Errorf("Should have user Name when selected it") + } +} + +func TestOrderAndPluck(t *testing.T) { + user1 := User{Name: "OrderPluckUser1", Age: 1} + user2 := User{Name: "OrderPluckUser2", Age: 10} + user3 := User{Name: "OrderPluckUser3", Age: 20} + DB.Save(&user1).Save(&user2).Save(&user3) + scopedb := DB.Model(&User{}).Where("name like ?", "%OrderPluckUser%") + + var ages []int64 + scopedb.Order("age desc").Pluck("age", &ages) + if ages[0] != 20 { + t.Errorf("The first age should be 20 when order with age desc") + } + + var ages1, ages2 []int64 + scopedb.Order("age desc").Pluck("age", &ages1).Pluck("age", &ages2) + if !reflect.DeepEqual(ages1, ages2) { + t.Errorf("The first order is the primary order") + } + + var ages3, ages4 []int64 + scopedb.Model(&User{}).Order("age desc").Pluck("age", &ages3).Order("age", true).Pluck("age", &ages4) + if reflect.DeepEqual(ages3, ages4) { + t.Errorf("Reorder should work") + } + + var names []string + var ages5 []int64 + scopedb.Model(User{}).Order("name").Order("age desc").Pluck("age", &ages5).Pluck("name", &names) + if names != nil && ages5 != nil { + if !(names[0] == user1.Name && names[1] == user2.Name && names[2] == user3.Name && ages5[2] == 20) { + t.Errorf("Order with multiple orders") + } + } else { + t.Errorf("Order with multiple orders") + } + + DB.Model(User{}).Select("name, age").Find(&[]User{}) +} + +func TestLimit(t *testing.T) { + user1 := User{Name: "LimitUser1", Age: 1} + user2 := User{Name: "LimitUser2", Age: 10} + user3 := User{Name: "LimitUser3", Age: 20} + user4 := User{Name: "LimitUser4", Age: 10} + user5 := User{Name: "LimitUser5", Age: 20} + DB.Save(&user1).Save(&user2).Save(&user3).Save(&user4).Save(&user5) + + var users1, users2, users3 []User + DB.Order("age desc").Limit(3).Find(&users1).Limit(5).Find(&users2).Limit(-1).Find(&users3) + + if len(users1) != 3 || len(users2) != 5 || len(users3) <= 5 { + t.Errorf("Limit should works") + } +} + +func TestOffset(t *testing.T) { + for i := 0; i < 20; i++ { + DB.Save(&User{Name: fmt.Sprintf("OffsetUser%v", i)}) + } + var users1, users2, users3, users4 []User + DB.Limit(100).Order("age desc").Find(&users1).Offset(3).Find(&users2).Offset(5).Find(&users3).Offset(-1).Find(&users4) + + if (len(users1) != len(users4)) || (len(users1)-len(users2) != 3) || (len(users1)-len(users3) != 5) { + t.Errorf("Offset should work") + } +} + +func TestOr(t *testing.T) { + user1 := User{Name: "OrUser1", Age: 1} + user2 := User{Name: "OrUser2", Age: 10} + user3 := User{Name: "OrUser3", Age: 20} + DB.Save(&user1).Save(&user2).Save(&user3) + + var users []User + DB.Where("name = ?", user1.Name).Or("name = ?", user2.Name).Find(&users) + if len(users) != 2 { + t.Errorf("Find users with or") + } +} + +func TestCount(t *testing.T) { + user1 := User{Name: "CountUser1", Age: 1} + user2 := User{Name: "CountUser2", Age: 10} + user3 := User{Name: "CountUser3", Age: 20} + + DB.Save(&user1).Save(&user2).Save(&user3) + var count, count1, count2 int64 + var users []User + + if err := DB.Where("name = ?", user1.Name).Or("name = ?", user3.Name).Find(&users).Count(&count).Error; err != nil { + t.Errorf(fmt.Sprintf("Count should work, but got err %v", err)) + } + + if count != int64(len(users)) { + t.Errorf("Count() method should get correct value") + } + + DB.Model(&User{}).Where("name = ?", user1.Name).Count(&count1).Or("name in (?)", []string{user2.Name, user3.Name}).Count(&count2) + if count1 != 1 || count2 != 3 { + t.Errorf("Multiple count in chain") + } +} + +func TestNot(t *testing.T) { + DB.Create(getPreparedUser("user1", "not")) + DB.Create(getPreparedUser("user2", "not")) + DB.Create(getPreparedUser("user3", "not")) + DB.Create(getPreparedUser("user4", "not")) + DB := DB.Where("role = ?", "not") + + var users1, users2, users3, users4, users5, users6, users7, users8 []User + if DB.Find(&users1).RowsAffected != 4 { + t.Errorf("should find 4 not users") + } + DB.Not(users1[0].Id).Find(&users2) + + if len(users1)-len(users2) != 1 { + t.Errorf("Should ignore the first users with Not") + } + + DB.Not([]int{}).Find(&users3) + if len(users1)-len(users3) != 0 { + t.Errorf("Should find all users with a blank condition") + } + + var name3Count int64 + DB.Table("users").Where("name = ?", "user3").Count(&name3Count) + DB.Not("name", "user3").Find(&users4) + if len(users1)-len(users4) != int(name3Count) { + t.Errorf("Should find all users's name not equal 3") + } + + DB.Not("name = ?", "user3").Find(&users4) + if len(users1)-len(users4) != int(name3Count) { + t.Errorf("Should find all users's name not equal 3") + } + + DB.Not("name <> ?", "user3").Find(&users4) + if len(users4) != int(name3Count) { + t.Errorf("Should find all users's name not equal 3") + } + + DB.Not(User{Name: "user3"}).Find(&users5) + + if len(users1)-len(users5) != int(name3Count) { + t.Errorf("Should find all users's name not equal 3") + } + + DB.Not(map[string]interface{}{"name": "user3"}).Find(&users6) + if len(users1)-len(users6) != int(name3Count) { + t.Errorf("Should find all users's name not equal 3") + } + + DB.Not("name", []string{"user3"}).Find(&users7) + if len(users1)-len(users7) != int(name3Count) { + t.Errorf("Should find all users's name not equal 3") + } + + var name2Count int64 + DB.Table("users").Where("name = ?", "user2").Count(&name2Count) + DB.Not("name", []string{"user3", "user2"}).Find(&users8) + if len(users1)-len(users8) != (int(name3Count) + int(name2Count)) { + t.Errorf("Should find all users's name not equal 3") + } +} + +func TestFillSmallerStruct(t *testing.T) { + user1 := User{Name: "SmallerUser", Age: 100} + DB.Save(&user1) + type SimpleUser struct { + Name string + Id int64 + UpdatedAt time.Time + CreatedAt time.Time + } + + var simpleUser SimpleUser + DB.Table("users").Where("name = ?", user1.Name).First(&simpleUser) + + if simpleUser.Id == 0 || simpleUser.Name == "" { + t.Errorf("Should fill data correctly into smaller struct") + } +} + +func TestFindOrInitialize(t *testing.T) { + var user1, user2, user3, user4, user5, user6 User + DB.Where(&User{Name: "find or init", Age: 33}).FirstOrInit(&user1) + if user1.Name != "find or init" || user1.Id != 0 || user1.Age != 33 { + t.Errorf("user should be initialized with search value") + } + + DB.Where(User{Name: "find or init", Age: 33}).FirstOrInit(&user2) + if user2.Name != "find or init" || user2.Id != 0 || user2.Age != 33 { + t.Errorf("user should be initialized with search value") + } + + DB.FirstOrInit(&user3, map[string]interface{}{"name": "find or init 2"}) + if user3.Name != "find or init 2" || user3.Id != 0 { + t.Errorf("user should be initialized with inline search value") + } + + DB.Where(&User{Name: "find or init"}).Attrs(User{Age: 44}).FirstOrInit(&user4) + if user4.Name != "find or init" || user4.Id != 0 || user4.Age != 44 { + t.Errorf("user should be initialized with search value and attrs") + } + + DB.Where(&User{Name: "find or init"}).Assign("age", 44).FirstOrInit(&user4) + if user4.Name != "find or init" || user4.Id != 0 || user4.Age != 44 { + t.Errorf("user should be initialized with search value and assign attrs") + } + + DB.Save(&User{Name: "find or init", Age: 33}) + DB.Where(&User{Name: "find or init"}).Attrs("age", 44).FirstOrInit(&user5) + if user5.Name != "find or init" || user5.Id == 0 || user5.Age != 33 { + t.Errorf("user should be found and not initialized by Attrs") + } + + DB.Where(&User{Name: "find or init", Age: 33}).FirstOrInit(&user6) + if user6.Name != "find or init" || user6.Id == 0 || user6.Age != 33 { + t.Errorf("user should be found with FirstOrInit") + } + + DB.Where(&User{Name: "find or init"}).Assign(User{Age: 44}).FirstOrInit(&user6) + if user6.Name != "find or init" || user6.Id == 0 || user6.Age != 44 { + t.Errorf("user should be found and updated with assigned attrs") + } +} + +func TestFindOrCreate(t *testing.T) { + var user1, user2, user3, user4, user5, user6, user7, user8 User + DB.Where(&User{Name: "find or create", Age: 33}).FirstOrCreate(&user1) + if user1.Name != "find or create" || user1.Id == 0 || user1.Age != 33 { + t.Errorf("user should be created with search value") + } + + DB.Where(&User{Name: "find or create", Age: 33}).FirstOrCreate(&user2) + if user1.Id != user2.Id || user2.Name != "find or create" || user2.Id == 0 || user2.Age != 33 { + t.Errorf("user should be created with search value") + } + + DB.FirstOrCreate(&user3, map[string]interface{}{"name": "find or create 2"}) + if user3.Name != "find or create 2" || user3.Id == 0 { + t.Errorf("user should be created with inline search value") + } + + DB.Where(&User{Name: "find or create 3"}).Attrs("age", 44).FirstOrCreate(&user4) + if user4.Name != "find or create 3" || user4.Id == 0 || user4.Age != 44 { + t.Errorf("user should be created with search value and attrs") + } + + updatedAt1 := user4.UpdatedAt + DB.Where(&User{Name: "find or create 3"}).Assign("age", 55).FirstOrCreate(&user4) + if updatedAt1.Format(time.RFC3339Nano) == user4.UpdatedAt.Format(time.RFC3339Nano) { + t.Errorf("UpdateAt should be changed when update values with assign") + } + + DB.Where(&User{Name: "find or create 4"}).Assign(User{Age: 44}).FirstOrCreate(&user4) + if user4.Name != "find or create 4" || user4.Id == 0 || user4.Age != 44 { + t.Errorf("user should be created with search value and assigned attrs") + } + + DB.Where(&User{Name: "find or create"}).Attrs("age", 44).FirstOrInit(&user5) + if user5.Name != "find or create" || user5.Id == 0 || user5.Age != 33 { + t.Errorf("user should be found and not initialized by Attrs") + } + + DB.Where(&User{Name: "find or create"}).Assign(User{Age: 44}).FirstOrCreate(&user6) + if user6.Name != "find or create" || user6.Id == 0 || user6.Age != 44 { + t.Errorf("user should be found and updated with assigned attrs") + } + + DB.Where(&User{Name: "find or create"}).Find(&user7) + if user7.Name != "find or create" || user7.Id == 0 || user7.Age != 44 { + t.Errorf("user should be found and updated with assigned attrs") + } + + DB.Where(&User{Name: "find or create embedded struct"}).Assign(User{Age: 44, CreditCard: CreditCard{Number: "1231231231"}, Emails: []Email{{Email: "jinzhu@assign_embedded_struct.com"}, {Email: "jinzhu-2@assign_embedded_struct.com"}}}).FirstOrCreate(&user8) + if DB.Where("email = ?", "jinzhu-2@assign_embedded_struct.com").First(&Email{}).RecordNotFound() { + t.Errorf("embedded struct email should be saved") + } + + if DB.Where("email = ?", "1231231231").First(&CreditCard{}).RecordNotFound() { + t.Errorf("embedded struct credit card should be saved") + } +} + +func TestSelectWithEscapedFieldName(t *testing.T) { + user1 := User{Name: "EscapedFieldNameUser", Age: 1} + user2 := User{Name: "EscapedFieldNameUser", Age: 10} + user3 := User{Name: "EscapedFieldNameUser", Age: 20} + DB.Save(&user1).Save(&user2).Save(&user3) + + var names []string + DB.Model(User{}).Where(&User{Name: "EscapedFieldNameUser"}).Pluck("\"name\"", &names) + + if len(names) != 3 { + t.Errorf("Expected 3 name, but got: %d", len(names)) + } +} + +func TestSelectWithVariables(t *testing.T) { + DB.Save(&User{Name: "jinzhu"}) + + rows, _ := DB.Table("users").Select("? as fake", gorm.Expr("name")).Rows() + + if !rows.Next() { + t.Errorf("Should have returned at least one row") + } else { + columns, _ := rows.Columns() + if !reflect.DeepEqual(columns, []string{"fake"}) { + t.Errorf("Should only contains one column") + } + } +} + +func TestSelectWithArrayInput(t *testing.T) { + DB.Save(&User{Name: "jinzhu", Age: 42}) + + var user User + DB.Select([]string{"name", "age"}).Where("age = 42 AND name = 'jinzhu'").First(&user) + + if user.Name != "jinzhu" || user.Age != 42 { + t.Errorf("Should have selected both age and name") + } +} + +func TestCurrentDatabase(t *testing.T) { + databaseName := DB.CurrentDatabase() + if err := DB.Error; err != nil { + t.Errorf("Problem getting current db name: %s", err) + } + if databaseName == "" { + t.Errorf("Current db name returned empty; this should never happen!") + } + t.Logf("Got current db name: %v", databaseName) +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/scope.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/scope.go new file mode 100644 index 0000000..7b6764a --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/scope.go @@ -0,0 +1,461 @@ +package gorm + +import ( + "errors" + "fmt" + "regexp" + "strings" + "time" + + "reflect" +) + +type Scope struct { + Search *search + Value interface{} + Sql string + SqlVars []interface{} + db *DB + indirectValue *reflect.Value + instanceId string + primaryKeyField *Field + skipLeft bool + fields map[string]*Field + selectAttrs *[]string +} + +func (scope *Scope) IndirectValue() reflect.Value { + if scope.indirectValue == nil { + value := reflect.Indirect(reflect.ValueOf(scope.Value)) + if value.Kind() == reflect.Ptr { + value = value.Elem() + } + scope.indirectValue = &value + } + return *scope.indirectValue +} + +func (scope *Scope) NeedPtr() *Scope { + reflectKind := reflect.ValueOf(scope.Value).Kind() + if !((reflectKind == reflect.Invalid) || (reflectKind == reflect.Ptr)) { + err := fmt.Errorf("%v %v\n", fileWithLineNum(), "using unaddressable value") + scope.Err(err) + fmt.Printf(err.Error()) + } + return scope +} + +// New create a new Scope without search information +func (scope *Scope) New(value interface{}) *Scope { + return &Scope{db: scope.NewDB(), Search: &search{}, Value: value} +} + +// NewDB create a new DB without search information +func (scope *Scope) NewDB() *DB { + if scope.db != nil { + db := scope.db.clone() + db.search = nil + db.Value = nil + return db + } + return nil +} + +func (scope *Scope) DB() *DB { + return scope.db +} + +// SqlDB return *sql.DB +func (scope *Scope) SqlDB() sqlCommon { + return scope.db.db +} + +// SkipLeft skip remaining callbacks +func (scope *Scope) SkipLeft() { + scope.skipLeft = true +} + +// Quote used to quote database column name according to database dialect +func (scope *Scope) Quote(str string) string { + if strings.Index(str, ".") != -1 { + newStrs := []string{} + for _, str := range strings.Split(str, ".") { + newStrs = append(newStrs, scope.Dialect().Quote(str)) + } + return strings.Join(newStrs, ".") + } else { + return scope.Dialect().Quote(str) + } +} + +func (scope *Scope) QuoteIfPossible(str string) string { + if regexp.MustCompile("^[a-zA-Z]+(.[a-zA-Z]+)*$").MatchString(str) { + return scope.Quote(str) + } + return str +} + +// Dialect get dialect +func (scope *Scope) Dialect() Dialect { + return scope.db.parent.dialect +} + +// Err write error +func (scope *Scope) Err(err error) error { + if err != nil { + scope.db.AddError(err) + } + return err +} + +// Log print log message +func (scope *Scope) Log(v ...interface{}) { + scope.db.log(v...) +} + +// HasError check if there are any error +func (scope *Scope) HasError() bool { + return scope.db.Error != nil +} + +func (scope *Scope) PrimaryFields() []*Field { + var fields = []*Field{} + for _, field := range scope.GetModelStruct().PrimaryFields { + fields = append(fields, scope.Fields()[field.DBName]) + } + return fields +} + +func (scope *Scope) PrimaryField() *Field { + if primaryFields := scope.GetModelStruct().PrimaryFields; len(primaryFields) > 0 { + if len(primaryFields) > 1 { + if field, ok := scope.Fields()["id"]; ok { + return field + } + } + return scope.Fields()[primaryFields[0].DBName] + } + return nil +} + +// PrimaryKey get the primary key's column name +func (scope *Scope) PrimaryKey() string { + if field := scope.PrimaryField(); field != nil { + return field.DBName + } + return "" +} + +// PrimaryKeyZero check the primary key is blank or not +func (scope *Scope) PrimaryKeyZero() bool { + field := scope.PrimaryField() + return field == nil || field.IsBlank +} + +// PrimaryKeyValue get the primary key's value +func (scope *Scope) PrimaryKeyValue() interface{} { + if field := scope.PrimaryField(); field != nil && field.Field.IsValid() { + return field.Field.Interface() + } + return 0 +} + +// HasColumn to check if has column +func (scope *Scope) HasColumn(column string) bool { + for _, field := range scope.GetStructFields() { + if field.IsNormal && (field.Name == column || field.DBName == column) { + return true + } + } + return false +} + +// SetColumn to set the column's value +func (scope *Scope) SetColumn(column interface{}, value interface{}) error { + if field, ok := column.(*Field); ok { + return field.Set(value) + } else if name, ok := column.(string); ok { + + if field, ok := scope.Fields()[name]; ok { + return field.Set(value) + } + + dbName := ToDBName(name) + if field, ok := scope.Fields()[dbName]; ok { + return field.Set(value) + } + + if field, ok := scope.FieldByName(name); ok { + return field.Set(value) + } + } + return errors.New("could not convert column to field") +} + +func (scope *Scope) CallMethod(name string, checkError bool) { + if scope.Value == nil || (checkError && scope.HasError()) { + return + } + + call := func(value interface{}) { + if fm := reflect.ValueOf(value).MethodByName(name); fm.IsValid() { + switch f := fm.Interface().(type) { + case func(): + f() + case func(s *Scope): + f(scope) + case func(s *DB): + newDB := scope.NewDB() + f(newDB) + scope.Err(newDB.Error) + case func() error: + scope.Err(f()) + case func(s *Scope) error: + scope.Err(f(scope)) + case func(s *DB) error: + scope.Err(f(scope.NewDB())) + default: + scope.Err(fmt.Errorf("unsupported function %v", name)) + } + } + } + + if values := scope.IndirectValue(); values.Kind() == reflect.Slice { + for i := 0; i < values.Len(); i++ { + value := values.Index(i).Addr().Interface() + if values.Index(i).Kind() == reflect.Ptr { + value = values.Index(i).Interface() + } + call(value) + } + } else { + if scope.IndirectValue().CanAddr() { + call(scope.IndirectValue().Addr().Interface()) + } else { + call(scope.IndirectValue().Interface()) + } + } +} + +func (scope *Scope) CallMethodWithErrorCheck(name string) { + scope.CallMethod(name, true) +} + +// AddToVars add value as sql's vars, gorm will escape them +func (scope *Scope) AddToVars(value interface{}) string { + if expr, ok := value.(*expr); ok { + exp := expr.expr + for _, arg := range expr.args { + exp = strings.Replace(exp, "?", scope.AddToVars(arg), 1) + } + return exp + } else { + scope.SqlVars = append(scope.SqlVars, value) + return scope.Dialect().BinVar(len(scope.SqlVars)) + } +} + +type tabler interface { + TableName() string +} + +type dbTabler interface { + TableName(*DB) string +} + +// TableName get table name +func (scope *Scope) TableName() string { + if scope.Search != nil && len(scope.Search.tableName) > 0 { + return scope.Search.tableName + } + + if tabler, ok := scope.Value.(tabler); ok { + return tabler.TableName() + } + + if tabler, ok := scope.Value.(dbTabler); ok { + return tabler.TableName(scope.db) + } + + return scope.GetModelStruct().TableName(scope.db.Model(scope.Value)) +} + +func (scope *Scope) QuotedTableName() (name string) { + if scope.Search != nil && len(scope.Search.tableName) > 0 { + if strings.Index(scope.Search.tableName, " ") != -1 { + return scope.Search.tableName + } + return scope.Quote(scope.Search.tableName) + } else { + return scope.Quote(scope.TableName()) + } +} + +// CombinedConditionSql get combined condition sql +func (scope *Scope) CombinedConditionSql() string { + return scope.joinsSql() + scope.whereSql() + scope.groupSql() + + scope.havingSql() + scope.orderSql() + scope.limitSql() + scope.offsetSql() +} + +func (scope *Scope) FieldByName(name string) (field *Field, ok bool) { + for _, field := range scope.Fields() { + if field.Name == name || field.DBName == name { + return field, true + } + } + return nil, false +} + +// Raw set sql +func (scope *Scope) Raw(sql string) *Scope { + scope.Sql = strings.Replace(sql, "$$", "?", -1) + return scope +} + +// Exec invoke sql +func (scope *Scope) Exec() *Scope { + defer scope.Trace(NowFunc()) + + if !scope.HasError() { + if result, err := scope.SqlDB().Exec(scope.Sql, scope.SqlVars...); scope.Err(err) == nil { + if count, err := result.RowsAffected(); scope.Err(err) == nil { + scope.db.RowsAffected = count + } + } + } + return scope +} + +// Set set value by name +func (scope *Scope) Set(name string, value interface{}) *Scope { + scope.db.InstantSet(name, value) + return scope +} + +// Get get value by name +func (scope *Scope) Get(name string) (interface{}, bool) { + return scope.db.Get(name) +} + +// InstanceId get InstanceId for scope +func (scope *Scope) InstanceId() string { + if scope.instanceId == "" { + scope.instanceId = fmt.Sprintf("%v%v", &scope, &scope.db) + } + return scope.instanceId +} + +func (scope *Scope) InstanceSet(name string, value interface{}) *Scope { + return scope.Set(name+scope.InstanceId(), value) +} + +func (scope *Scope) InstanceGet(name string) (interface{}, bool) { + return scope.Get(name + scope.InstanceId()) +} + +// Trace print sql log +func (scope *Scope) Trace(t time.Time) { + if len(scope.Sql) > 0 { + scope.db.slog(scope.Sql, t, scope.SqlVars...) + } +} + +// Begin start a transaction +func (scope *Scope) Begin() *Scope { + if db, ok := scope.SqlDB().(sqlDb); ok { + if tx, err := db.Begin(); err == nil { + scope.db.db = interface{}(tx).(sqlCommon) + scope.InstanceSet("gorm:started_transaction", true) + } + } + return scope +} + +// CommitOrRollback commit current transaction if there is no error, otherwise rollback it +func (scope *Scope) CommitOrRollback() *Scope { + if _, ok := scope.InstanceGet("gorm:started_transaction"); ok { + if db, ok := scope.db.db.(sqlTx); ok { + if scope.HasError() { + db.Rollback() + } else { + db.Commit() + } + scope.db.db = scope.db.parent.db + } + } + return scope +} + +func (scope *Scope) SelectAttrs() []string { + if scope.selectAttrs == nil { + attrs := []string{} + for _, value := range scope.Search.selects { + if str, ok := value.(string); ok { + attrs = append(attrs, str) + } else if strs, ok := value.([]string); ok { + attrs = append(attrs, strs...) + } else if strs, ok := value.([]interface{}); ok { + for _, str := range strs { + attrs = append(attrs, fmt.Sprintf("%v", str)) + } + } + } + scope.selectAttrs = &attrs + } + return *scope.selectAttrs +} + +func (scope *Scope) OmitAttrs() []string { + return scope.Search.omits +} + +func (scope *Scope) changeableDBColumn(column string) bool { + selectAttrs := scope.SelectAttrs() + omitAttrs := scope.OmitAttrs() + + if len(selectAttrs) > 0 { + for _, attr := range selectAttrs { + if column == ToDBName(attr) { + return true + } + } + return false + } + + for _, attr := range omitAttrs { + if column == ToDBName(attr) { + return false + } + } + return true +} + +func (scope *Scope) changeableField(field *Field) bool { + selectAttrs := scope.SelectAttrs() + omitAttrs := scope.OmitAttrs() + + if len(selectAttrs) > 0 { + for _, attr := range selectAttrs { + if field.Name == attr || field.DBName == attr { + return true + } + } + return false + } + + for _, attr := range omitAttrs { + if field.Name == attr || field.DBName == attr { + return false + } + } + + return !field.IsIgnored +} + +func (scope *Scope) shouldSaveAssociations() bool { + saveAssociations, ok := scope.Get("gorm:save_associations") + if ok && !saveAssociations.(bool) { + return false + } + return true +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/scope_private.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/scope_private.go new file mode 100644 index 0000000..f6f815d --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/scope_private.go @@ -0,0 +1,637 @@ +package gorm + +import ( + "database/sql" + "database/sql/driver" + "fmt" + "reflect" + "regexp" + "strconv" + "strings" +) + +func (scope *Scope) primaryCondition(value interface{}) string { + return fmt.Sprintf("(%v = %v)", scope.Quote(scope.PrimaryKey()), value) +} + +func (scope *Scope) buildWhereCondition(clause map[string]interface{}) (str string) { + switch value := clause["query"].(type) { + case string: + // if string is number + if regexp.MustCompile("^\\s*\\d+\\s*$").MatchString(value) { + id, _ := strconv.Atoi(value) + return scope.primaryCondition(scope.AddToVars(id)) + } else if value != "" { + str = fmt.Sprintf("(%v)", value) + } + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, sql.NullInt64: + return scope.primaryCondition(scope.AddToVars(value)) + case []int, []int8, []int16, []int32, []int64, []uint, []uint8, []uint16, []uint32, []uint64, []string, []interface{}: + str = fmt.Sprintf("(%v in (?))", scope.Quote(scope.PrimaryKey())) + clause["args"] = []interface{}{value} + case map[string]interface{}: + var sqls []string + for key, value := range value { + sqls = append(sqls, fmt.Sprintf("(%v = %v)", scope.Quote(key), scope.AddToVars(value))) + } + return strings.Join(sqls, " AND ") + case interface{}: + var sqls []string + for _, field := range scope.New(value).Fields() { + if !field.IsIgnored && !field.IsBlank { + sqls = append(sqls, fmt.Sprintf("(%v = %v)", scope.Quote(field.DBName), scope.AddToVars(field.Field.Interface()))) + } + } + return strings.Join(sqls, " AND ") + } + + args := clause["args"].([]interface{}) + for _, arg := range args { + switch reflect.ValueOf(arg).Kind() { + case reflect.Slice: // For where("id in (?)", []int64{1,2}) + values := reflect.ValueOf(arg) + var tempMarks []string + for i := 0; i < values.Len(); i++ { + tempMarks = append(tempMarks, scope.AddToVars(values.Index(i).Interface())) + } + str = strings.Replace(str, "?", strings.Join(tempMarks, ","), 1) + default: + if valuer, ok := interface{}(arg).(driver.Valuer); ok { + arg, _ = valuer.Value() + } + + str = strings.Replace(str, "?", scope.AddToVars(arg), 1) + } + } + return +} + +func (scope *Scope) buildNotCondition(clause map[string]interface{}) (str string) { + var notEqualSql string + var primaryKey = scope.PrimaryKey() + + switch value := clause["query"].(type) { + case string: + // is number + if regexp.MustCompile("^\\s*\\d+\\s*$").MatchString(value) { + id, _ := strconv.Atoi(value) + return fmt.Sprintf("(%v <> %v)", scope.Quote(primaryKey), id) + } else if regexp.MustCompile("(?i) (=|<>|>|<|LIKE|IS) ").MatchString(value) { + str = fmt.Sprintf(" NOT (%v) ", value) + notEqualSql = fmt.Sprintf("NOT (%v)", value) + } else { + str = fmt.Sprintf("(%v NOT IN (?))", scope.Quote(value)) + notEqualSql = fmt.Sprintf("(%v <> ?)", scope.Quote(value)) + } + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, sql.NullInt64: + return fmt.Sprintf("(%v <> %v)", scope.Quote(primaryKey), value) + case []int, []int8, []int16, []int32, []int64, []uint, []uint8, []uint16, []uint32, []uint64, []string: + if reflect.ValueOf(value).Len() > 0 { + str = fmt.Sprintf("(%v NOT IN (?))", scope.Quote(primaryKey)) + clause["args"] = []interface{}{value} + } + return "" + case map[string]interface{}: + var sqls []string + for key, value := range value { + sqls = append(sqls, fmt.Sprintf("(%v <> %v)", scope.Quote(key), scope.AddToVars(value))) + } + return strings.Join(sqls, " AND ") + case interface{}: + var sqls []string + for _, field := range scope.New(value).Fields() { + if !field.IsBlank { + sqls = append(sqls, fmt.Sprintf("(%v <> %v)", scope.Quote(field.DBName), scope.AddToVars(field.Field.Interface()))) + } + } + return strings.Join(sqls, " AND ") + } + + args := clause["args"].([]interface{}) + for _, arg := range args { + switch reflect.ValueOf(arg).Kind() { + case reflect.Slice: // For where("id in (?)", []int64{1,2}) + values := reflect.ValueOf(arg) + var tempMarks []string + for i := 0; i < values.Len(); i++ { + tempMarks = append(tempMarks, scope.AddToVars(values.Index(i).Interface())) + } + str = strings.Replace(str, "?", strings.Join(tempMarks, ","), 1) + default: + if scanner, ok := interface{}(arg).(driver.Valuer); ok { + arg, _ = scanner.Value() + } + str = strings.Replace(notEqualSql, "?", scope.AddToVars(arg), 1) + } + } + return +} + +func (scope *Scope) buildSelectQuery(clause map[string]interface{}) (str string) { + switch value := clause["query"].(type) { + case string: + str = value + case []string: + str = strings.Join(value, ", ") + } + + args := clause["args"].([]interface{}) + for _, arg := range args { + switch reflect.ValueOf(arg).Kind() { + case reflect.Slice: + values := reflect.ValueOf(arg) + var tempMarks []string + for i := 0; i < values.Len(); i++ { + tempMarks = append(tempMarks, scope.AddToVars(values.Index(i).Interface())) + } + str = strings.Replace(str, "?", strings.Join(tempMarks, ","), 1) + default: + if valuer, ok := interface{}(arg).(driver.Valuer); ok { + arg, _ = valuer.Value() + } + str = strings.Replace(str, "?", scope.AddToVars(arg), 1) + } + } + return +} + +func (scope *Scope) whereSql() (sql string) { + var primaryConditions, andConditions, orConditions []string + + if !scope.Search.Unscoped && scope.Fields()["deleted_at"] != nil { + sql := fmt.Sprintf("(%v.deleted_at IS NULL OR %v.deleted_at <= '0001-01-02')", scope.QuotedTableName(), scope.QuotedTableName()) + primaryConditions = append(primaryConditions, sql) + } + + if !scope.PrimaryKeyZero() { + primaryConditions = append(primaryConditions, scope.primaryCondition(scope.AddToVars(scope.PrimaryKeyValue()))) + } + + for _, clause := range scope.Search.whereConditions { + if sql := scope.buildWhereCondition(clause); sql != "" { + andConditions = append(andConditions, sql) + } + } + + for _, clause := range scope.Search.orConditions { + if sql := scope.buildWhereCondition(clause); sql != "" { + orConditions = append(orConditions, sql) + } + } + + for _, clause := range scope.Search.notConditions { + if sql := scope.buildNotCondition(clause); sql != "" { + andConditions = append(andConditions, sql) + } + } + + orSql := strings.Join(orConditions, " OR ") + combinedSql := strings.Join(andConditions, " AND ") + if len(combinedSql) > 0 { + if len(orSql) > 0 { + combinedSql = combinedSql + " OR " + orSql + } + } else { + combinedSql = orSql + } + + if len(primaryConditions) > 0 { + sql = "WHERE " + strings.Join(primaryConditions, " AND ") + if len(combinedSql) > 0 { + sql = sql + " AND (" + combinedSql + ")" + } + } else if len(combinedSql) > 0 { + sql = "WHERE " + combinedSql + } + return +} + +var hasCountRegexp = regexp.MustCompile(`(?i)count(.+)`) + +func (scope *Scope) selectSql() string { + if len(scope.Search.selects) == 0 { + return "*" + } + sql := scope.buildSelectQuery(scope.Search.selects) + scope.Search.countingQuery = hasCountRegexp.MatchString(sql) + return scope.buildSelectQuery(scope.Search.selects) +} + +func (scope *Scope) orderSql() string { + if len(scope.Search.orders) == 0 || scope.Search.countingQuery { + return "" + } + return " ORDER BY " + strings.Join(scope.Search.orders, ",") +} + +func (scope *Scope) limitSql() string { + if !scope.Dialect().HasTop() { + if len(scope.Search.limit) == 0 { + return "" + } + return " LIMIT " + scope.Search.limit + } + + return "" +} + +func (scope *Scope) topSql() string { + if scope.Dialect().HasTop() && len(scope.Search.offset) == 0 { + if len(scope.Search.limit) == 0 { + return "" + } + return " TOP(" + scope.Search.limit + ")" + } + + return "" +} + +func (scope *Scope) offsetSql() string { + if len(scope.Search.offset) == 0 { + return "" + } + + if scope.Dialect().HasTop() { + sql := " OFFSET " + scope.Search.offset + " ROW " + if len(scope.Search.limit) > 0 { + sql += "FETCH NEXT " + scope.Search.limit + " ROWS ONLY" + } + return sql + } + return " OFFSET " + scope.Search.offset +} + +func (scope *Scope) groupSql() string { + if len(scope.Search.group) == 0 { + return "" + } + return " GROUP BY " + scope.Search.group +} + +func (scope *Scope) havingSql() string { + if scope.Search.havingConditions == nil { + return "" + } + + var andConditions []string + + for _, clause := range scope.Search.havingConditions { + if sql := scope.buildWhereCondition(clause); sql != "" { + andConditions = append(andConditions, sql) + } + } + + combinedSql := strings.Join(andConditions, " AND ") + if len(combinedSql) == 0 { + return "" + } + + return " HAVING " + combinedSql +} + +func (scope *Scope) joinsSql() string { + return scope.Search.joins + " " +} + +func (scope *Scope) prepareQuerySql() { + if scope.Search.raw { + scope.Raw(strings.TrimSuffix(strings.TrimPrefix(scope.CombinedConditionSql(), " WHERE ("), ")")) + } else { + scope.Raw(fmt.Sprintf("SELECT %v %v FROM %v %v", scope.topSql(), scope.selectSql(), scope.QuotedTableName(), scope.CombinedConditionSql())) + } + return +} + +func (scope *Scope) inlineCondition(values ...interface{}) *Scope { + if len(values) > 0 { + scope.Search.Where(values[0], values[1:]...) + } + return scope +} + +func (scope *Scope) callCallbacks(funcs []*func(s *Scope)) *Scope { + for _, f := range funcs { + (*f)(scope) + if scope.skipLeft { + break + } + } + return scope +} + +func (scope *Scope) updatedAttrsWithValues(values map[string]interface{}, ignoreProtectedAttrs bool) (results map[string]interface{}, hasUpdate bool) { + if !scope.IndirectValue().CanAddr() { + return values, true + } + + var hasExpr bool + fields := scope.Fields() + for key, value := range values { + if field, ok := fields[ToDBName(key)]; ok && field.Field.IsValid() { + if !reflect.DeepEqual(field.Field, reflect.ValueOf(value)) { + if _, ok := value.(*expr); ok { + hasExpr = true + } else if !equalAsString(field.Field.Interface(), value) { + hasUpdate = true + field.Set(value) + } + } + } + } + if hasExpr { + var updateMap = map[string]interface{}{} + for key, value := range fields { + if v, ok := values[key]; ok { + updateMap[key] = v + } else { + updateMap[key] = value.Field.Interface() + } + } + return updateMap, true + } + return +} + +func (scope *Scope) row() *sql.Row { + defer scope.Trace(NowFunc()) + scope.callCallbacks(scope.db.parent.callback.rowQueries) + scope.prepareQuerySql() + return scope.SqlDB().QueryRow(scope.Sql, scope.SqlVars...) +} + +func (scope *Scope) rows() (*sql.Rows, error) { + defer scope.Trace(NowFunc()) + scope.callCallbacks(scope.db.parent.callback.rowQueries) + scope.prepareQuerySql() + return scope.SqlDB().Query(scope.Sql, scope.SqlVars...) +} + +func (scope *Scope) initialize() *Scope { + for _, clause := range scope.Search.whereConditions { + scope.updatedAttrsWithValues(convertInterfaceToMap(clause["query"]), false) + } + scope.updatedAttrsWithValues(convertInterfaceToMap(scope.Search.initAttrs), false) + scope.updatedAttrsWithValues(convertInterfaceToMap(scope.Search.assignAttrs), false) + return scope +} + +func (scope *Scope) pluck(column string, value interface{}) *Scope { + dest := reflect.Indirect(reflect.ValueOf(value)) + scope.Search.Select(column) + if dest.Kind() != reflect.Slice { + scope.Err(fmt.Errorf("results should be a slice, not %s", dest.Kind())) + return scope + } + + rows, err := scope.rows() + if scope.Err(err) == nil { + defer rows.Close() + for rows.Next() { + elem := reflect.New(dest.Type().Elem()).Interface() + scope.Err(rows.Scan(elem)) + dest.Set(reflect.Append(dest, reflect.ValueOf(elem).Elem())) + } + } + return scope +} + +func (scope *Scope) count(value interface{}) *Scope { + scope.Search.Select("count(*)") + scope.Err(scope.row().Scan(value)) + return scope +} + +func (scope *Scope) typeName() string { + value := scope.IndirectValue() + if value.Kind() == reflect.Slice { + return value.Type().Elem().Name() + } + + return value.Type().Name() +} + +func (scope *Scope) related(value interface{}, foreignKeys ...string) *Scope { + toScope := scope.db.NewScope(value) + fromFields := scope.Fields() + toFields := toScope.Fields() + for _, foreignKey := range append(foreignKeys, toScope.typeName()+"Id", scope.typeName()+"Id") { + var fromField, toField *Field + if field, ok := scope.FieldByName(foreignKey); ok { + fromField = field + } else { + fromField = fromFields[ToDBName(foreignKey)] + } + if field, ok := toScope.FieldByName(foreignKey); ok { + toField = field + } else { + toField = toFields[ToDBName(foreignKey)] + } + + if fromField != nil { + if relationship := fromField.Relationship; relationship != nil { + if relationship.Kind == "many_to_many" { + joinTableHandler := relationship.JoinTableHandler + scope.Err(joinTableHandler.JoinWith(joinTableHandler, toScope.db, scope.Value).Find(value).Error) + } else if relationship.Kind == "belongs_to" { + query := toScope.db + for idx, foreignKey := range relationship.ForeignDBNames { + if field, ok := scope.FieldByName(foreignKey); ok { + query = query.Where(fmt.Sprintf("%v = ?", scope.Quote(relationship.AssociationForeignDBNames[idx])), field.Field.Interface()) + } + } + scope.Err(query.Find(value).Error) + } else if relationship.Kind == "has_many" || relationship.Kind == "has_one" { + query := toScope.db + for idx, foreignKey := range relationship.ForeignDBNames { + if field, ok := scope.FieldByName(relationship.AssociationForeignDBNames[idx]); ok { + query = query.Where(fmt.Sprintf("%v = ?", scope.Quote(foreignKey)), field.Field.Interface()) + } + } + + if relationship.PolymorphicType != "" { + query = query.Where(fmt.Sprintf("%v = ?", scope.Quote(relationship.PolymorphicDBName)), scope.TableName()) + } + scope.Err(query.Find(value).Error) + } + } else { + sql := fmt.Sprintf("%v = ?", scope.Quote(toScope.PrimaryKey())) + scope.Err(toScope.db.Where(sql, fromField.Field.Interface()).Find(value).Error) + } + return scope + } else if toField != nil { + sql := fmt.Sprintf("%v = ?", scope.Quote(toField.DBName)) + scope.Err(toScope.db.Where(sql, scope.PrimaryKeyValue()).Find(value).Error) + return scope + } + } + + scope.Err(fmt.Errorf("invalid association %v", foreignKeys)) + return scope +} + +/** + Return the table options string or an empty string if the table options does not exist +*/ +func (scope *Scope) getTableOptions() string { + tableOptions, ok := scope.Get("gorm:table_options") + if !ok { + return "" + } + return tableOptions.(string) +} + +func (scope *Scope) createJoinTable(field *StructField) { + if relationship := field.Relationship; relationship != nil && relationship.JoinTableHandler != nil { + joinTableHandler := relationship.JoinTableHandler + joinTable := joinTableHandler.Table(scope.db) + if !scope.Dialect().HasTable(scope, joinTable) { + toScope := &Scope{Value: reflect.New(field.Struct.Type).Interface()} + + var sqlTypes []string + for idx, fieldName := range relationship.ForeignFieldNames { + if field, ok := scope.Fields()[fieldName]; ok { + value := reflect.Indirect(reflect.New(field.Struct.Type)) + primaryKeySqlType := scope.Dialect().SqlTag(value, 255, false) + sqlTypes = append(sqlTypes, scope.Quote(relationship.ForeignDBNames[idx])+" "+primaryKeySqlType) + } + } + + for idx, fieldName := range relationship.AssociationForeignFieldNames { + if field, ok := toScope.Fields()[fieldName]; ok { + value := reflect.Indirect(reflect.New(field.Struct.Type)) + primaryKeySqlType := scope.Dialect().SqlTag(value, 255, false) + sqlTypes = append(sqlTypes, scope.Quote(relationship.AssociationForeignDBNames[idx])+" "+primaryKeySqlType) + } + } + scope.Err(scope.NewDB().Exec(fmt.Sprintf("CREATE TABLE %v (%v) %s", scope.Quote(joinTable), strings.Join(sqlTypes, ","), scope.getTableOptions())).Error) + } + scope.NewDB().Table(joinTable).AutoMigrate(joinTableHandler) + } +} + +func (scope *Scope) createTable() *Scope { + var tags []string + var primaryKeys []string + for _, field := range scope.GetStructFields() { + if field.IsNormal { + sqlTag := scope.generateSqlTag(field) + tags = append(tags, scope.Quote(field.DBName)+" "+sqlTag) + } + + if field.IsPrimaryKey { + primaryKeys = append(primaryKeys, scope.Quote(field.DBName)) + } + scope.createJoinTable(field) + } + + var primaryKeyStr string + if len(primaryKeys) > 0 { + primaryKeyStr = fmt.Sprintf(", PRIMARY KEY (%v)", strings.Join(primaryKeys, ",")) + } + scope.Raw(fmt.Sprintf("CREATE TABLE %v (%v %v) %s", scope.QuotedTableName(), strings.Join(tags, ","), primaryKeyStr, scope.getTableOptions())).Exec() + return scope +} + +func (scope *Scope) dropTable() *Scope { + scope.Raw(fmt.Sprintf("DROP TABLE %v", scope.QuotedTableName())).Exec() + return scope +} + +func (scope *Scope) dropTableIfExists() *Scope { + if scope.Dialect().HasTable(scope, scope.TableName()) { + scope.dropTable() + } + return scope +} + +func (scope *Scope) modifyColumn(column string, typ string) { + scope.Raw(fmt.Sprintf("ALTER TABLE %v MODIFY %v %v", scope.QuotedTableName(), scope.Quote(column), typ)).Exec() +} + +func (scope *Scope) dropColumn(column string) { + scope.Raw(fmt.Sprintf("ALTER TABLE %v DROP COLUMN %v", scope.QuotedTableName(), scope.Quote(column))).Exec() +} + +func (scope *Scope) addIndex(unique bool, indexName string, column ...string) { + if scope.Dialect().HasIndex(scope, scope.TableName(), indexName) { + return + } + + var columns []string + for _, name := range column { + columns = append(columns, scope.QuoteIfPossible(name)) + } + + sqlCreate := "CREATE INDEX" + if unique { + sqlCreate = "CREATE UNIQUE INDEX" + } + + scope.Raw(fmt.Sprintf("%s %v ON %v(%v);", sqlCreate, indexName, scope.QuotedTableName(), strings.Join(columns, ", "))).Exec() +} + +func (scope *Scope) addForeignKey(field string, dest string, onDelete string, onUpdate string) { + var table = scope.TableName() + var keyName = fmt.Sprintf("%s_%s_%s_foreign", table, field, regexp.MustCompile("[^a-zA-Z]").ReplaceAllString(dest, "_")) + keyName = regexp.MustCompile("_+").ReplaceAllString(keyName, "_") + var query = `ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s ON DELETE %s ON UPDATE %s;` + scope.Raw(fmt.Sprintf(query, scope.QuotedTableName(), scope.QuoteIfPossible(keyName), scope.QuoteIfPossible(field), dest, onDelete, onUpdate)).Exec() +} + +func (scope *Scope) removeIndex(indexName string) { + scope.Dialect().RemoveIndex(scope, indexName) +} + +func (scope *Scope) autoMigrate() *Scope { + tableName := scope.TableName() + quotedTableName := scope.QuotedTableName() + + if !scope.Dialect().HasTable(scope, tableName) { + scope.createTable() + } else { + for _, field := range scope.GetStructFields() { + if !scope.Dialect().HasColumn(scope, tableName, field.DBName) { + if field.IsNormal { + sqlTag := scope.generateSqlTag(field) + scope.Raw(fmt.Sprintf("ALTER TABLE %v ADD %v %v;", quotedTableName, scope.Quote(field.DBName), sqlTag)).Exec() + } + } + scope.createJoinTable(field) + } + } + + scope.autoIndex() + return scope +} + +func (scope *Scope) autoIndex() *Scope { + var indexes = map[string][]string{} + var uniqueIndexes = map[string][]string{} + + for _, field := range scope.GetStructFields() { + sqlSettings := parseTagSetting(field.Tag.Get("sql")) + if name, ok := sqlSettings["INDEX"]; ok { + if name == "INDEX" { + name = fmt.Sprintf("idx_%v_%v", scope.TableName(), field.DBName) + } + indexes[name] = append(indexes[name], field.DBName) + } + + if name, ok := sqlSettings["UNIQUE_INDEX"]; ok { + if name == "UNIQUE_INDEX" { + name = fmt.Sprintf("uix_%v_%v", scope.TableName(), field.DBName) + } + uniqueIndexes[name] = append(uniqueIndexes[name], field.DBName) + } + } + + for name, columns := range indexes { + scope.addIndex(false, name, columns...) + } + + for name, columns := range uniqueIndexes { + scope.addIndex(true, name, columns...) + } + + return scope +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/scope_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/scope_test.go new file mode 100644 index 0000000..4245899 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/scope_test.go @@ -0,0 +1,43 @@ +package gorm_test + +import ( + "github.com/jinzhu/gorm" + "testing" +) + +func NameIn1And2(d *gorm.DB) *gorm.DB { + return d.Where("name in (?)", []string{"ScopeUser1", "ScopeUser2"}) +} + +func NameIn2And3(d *gorm.DB) *gorm.DB { + return d.Where("name in (?)", []string{"ScopeUser2", "ScopeUser3"}) +} + +func NameIn(names []string) func(d *gorm.DB) *gorm.DB { + return func(d *gorm.DB) *gorm.DB { + return d.Where("name in (?)", names) + } +} + +func TestScopes(t *testing.T) { + user1 := User{Name: "ScopeUser1", Age: 1} + user2 := User{Name: "ScopeUser2", Age: 1} + user3 := User{Name: "ScopeUser3", Age: 2} + DB.Save(&user1).Save(&user2).Save(&user3) + + var users1, users2, users3 []User + DB.Scopes(NameIn1And2).Find(&users1) + if len(users1) != 2 { + t.Errorf("Should found two users's name in 1, 2") + } + + DB.Scopes(NameIn1And2, NameIn2And3).Find(&users2) + if len(users2) != 1 { + t.Errorf("Should found one user's name is 2") + } + + DB.Scopes(NameIn([]string{user1.Name, user3.Name})).Find(&users3) + if len(users3) != 2 { + t.Errorf("Should found two users's name in 1, 3") + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/search.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/search.go new file mode 100644 index 0000000..166b9a8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/search.go @@ -0,0 +1,149 @@ +package gorm + +import "fmt" + +type search struct { + db *DB + whereConditions []map[string]interface{} + orConditions []map[string]interface{} + notConditions []map[string]interface{} + havingConditions []map[string]interface{} + initAttrs []interface{} + assignAttrs []interface{} + selects map[string]interface{} + omits []string + orders []string + joins string + preload []searchPreload + offset string + limit string + group string + tableName string + raw bool + Unscoped bool + countingQuery bool +} + +type searchPreload struct { + schema string + conditions []interface{} +} + +func (s *search) clone() *search { + clone := *s + return &clone +} + +func (s *search) Where(query interface{}, values ...interface{}) *search { + s.whereConditions = append(s.whereConditions, map[string]interface{}{"query": query, "args": values}) + return s +} + +func (s *search) Not(query interface{}, values ...interface{}) *search { + s.notConditions = append(s.notConditions, map[string]interface{}{"query": query, "args": values}) + return s +} + +func (s *search) Or(query interface{}, values ...interface{}) *search { + s.orConditions = append(s.orConditions, map[string]interface{}{"query": query, "args": values}) + return s +} + +func (s *search) Attrs(attrs ...interface{}) *search { + s.initAttrs = append(s.initAttrs, toSearchableMap(attrs...)) + return s +} + +func (s *search) Assign(attrs ...interface{}) *search { + s.assignAttrs = append(s.assignAttrs, toSearchableMap(attrs...)) + return s +} + +func (s *search) Order(value string, reorder ...bool) *search { + if len(reorder) > 0 && reorder[0] { + if value != "" { + s.orders = []string{value} + } else { + s.orders = []string{} + } + } else if value != "" { + s.orders = append(s.orders, value) + } + return s +} + +func (s *search) Select(query interface{}, args ...interface{}) *search { + s.selects = map[string]interface{}{"query": query, "args": args} + return s +} + +func (s *search) Omit(columns ...string) *search { + s.omits = columns + return s +} + +func (s *search) Limit(value interface{}) *search { + s.limit = s.getInterfaceAsSql(value) + return s +} + +func (s *search) Offset(value interface{}) *search { + s.offset = s.getInterfaceAsSql(value) + return s +} + +func (s *search) Group(query string) *search { + s.group = s.getInterfaceAsSql(query) + return s +} + +func (s *search) Having(query string, values ...interface{}) *search { + s.havingConditions = append(s.havingConditions, map[string]interface{}{"query": query, "args": values}) + return s +} + +func (s *search) Joins(query string) *search { + s.joins = query + return s +} + +func (s *search) Preload(schema string, values ...interface{}) *search { + var preloads []searchPreload + for _, preload := range s.preload { + if preload.schema != schema { + preloads = append(preloads, preload) + } + } + preloads = append(preloads, searchPreload{schema, values}) + s.preload = preloads + return s +} + +func (s *search) Raw(b bool) *search { + s.raw = b + return s +} + +func (s *search) unscoped() *search { + s.Unscoped = true + return s +} + +func (s *search) Table(name string) *search { + s.tableName = name + return s +} + +func (s *search) getInterfaceAsSql(value interface{}) (str string) { + switch value.(type) { + case string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + str = fmt.Sprintf("%v", value) + default: + s.db.AddError(InvalidSql) + } + + if str == "-1" { + return "" + } + return +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/search_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/search_test.go new file mode 100644 index 0000000..4db7ab6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/search_test.go @@ -0,0 +1,30 @@ +package gorm + +import ( + "reflect" + "testing" +) + +func TestCloneSearch(t *testing.T) { + s := new(search) + s.Where("name = ?", "jinzhu").Order("name").Attrs("name", "jinzhu").Select("name, age") + + s1 := s.clone() + s1.Where("age = ?", 20).Order("age").Attrs("email", "a@e.org").Select("email") + + if reflect.DeepEqual(s.whereConditions, s1.whereConditions) { + t.Errorf("Where should be copied") + } + + if reflect.DeepEqual(s.orders, s1.orders) { + t.Errorf("Order should be copied") + } + + if reflect.DeepEqual(s.initAttrs, s1.initAttrs) { + t.Errorf("InitAttrs should be copied") + } + + if reflect.DeepEqual(s.Select, s1.Select) { + t.Errorf("selectStr should be copied") + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/slice_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/slice_test.go new file mode 100644 index 0000000..2141054 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/slice_test.go @@ -0,0 +1,70 @@ +package gorm_test + +import ( + "database/sql/driver" + "encoding/json" + "testing" +) + +func TestScannableSlices(t *testing.T) { + if err := DB.AutoMigrate(&RecordWithSlice{}).Error; err != nil { + t.Errorf("Should create table with slice values correctly: %s", err) + } + + r1 := RecordWithSlice{ + Strings: ExampleStringSlice{"a", "b", "c"}, + Structs: ExampleStructSlice{ + {"name1", "value1"}, + {"name2", "value2"}, + }, + } + + if err := DB.Save(&r1).Error; err != nil { + t.Errorf("Should save record with slice values") + } + + var r2 RecordWithSlice + + if err := DB.Find(&r2).Error; err != nil { + t.Errorf("Should fetch record with slice values") + } + + if len(r2.Strings) != 3 || r2.Strings[0] != "a" || r2.Strings[1] != "b" || r2.Strings[2] != "c" { + t.Errorf("Should have serialised and deserialised a string array") + } + + if len(r2.Structs) != 2 || r2.Structs[0].Name != "name1" || r2.Structs[0].Value != "value1" || r2.Structs[1].Name != "name2" || r2.Structs[1].Value != "value2" { + t.Errorf("Should have serialised and deserialised a struct array") + } +} + +type RecordWithSlice struct { + ID uint64 + Strings ExampleStringSlice `sql:"type:text"` + Structs ExampleStructSlice `sql:"type:text"` +} + +type ExampleStringSlice []string + +func (l ExampleStringSlice) Value() (driver.Value, error) { + return json.Marshal(l) +} + +func (l *ExampleStringSlice) Scan(input interface{}) error { + return json.Unmarshal(input.([]byte), l) +} + +type ExampleStruct struct { + Name string + Value string +} + +type ExampleStructSlice []ExampleStruct + +func (l ExampleStructSlice) Value() (driver.Value, error) { + return json.Marshal(l) +} + +func (l *ExampleStructSlice) Scan(input interface{}) error { + return json.Unmarshal(input.([]byte), l) +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/sqlite3.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/sqlite3.go new file mode 100644 index 0000000..4584642 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/sqlite3.go @@ -0,0 +1,81 @@ +package gorm + +import ( + "fmt" + "reflect" + "time" +) + +type sqlite3 struct { + commonDialect +} + +func (sqlite3) SqlTag(value reflect.Value, size int, autoIncrease bool) string { + switch value.Kind() { + case reflect.Bool: + return "bool" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + return "integer" + case reflect.Int64, reflect.Uint64: + if autoIncrease { + return "integer" + } + return "bigint" + case reflect.Float32, reflect.Float64: + return "real" + case reflect.String: + if size > 0 && size < 65532 { + return fmt.Sprintf("varchar(%d)", size) + } + return "text" + case reflect.Struct: + if _, ok := value.Interface().(time.Time); ok { + return "datetime" + } + default: + if _, ok := value.Interface().([]byte); ok { + return "blob" + } + } + panic(fmt.Sprintf("invalid sql type %s (%s) for sqlite3", value.Type().Name(), value.Kind().String())) +} + +func (s sqlite3) HasTable(scope *Scope, tableName string) bool { + var count int + s.RawScanInt(scope, &count, "SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?", tableName) + return count > 0 +} + +func (s sqlite3) HasColumn(scope *Scope, tableName string, columnName string) bool { + var count int + s.RawScanInt(scope, &count, fmt.Sprintf("SELECT count(*) FROM sqlite_master WHERE tbl_name = ? AND (sql LIKE '%%(\"%v\" %%' OR sql LIKE '%%,\"%v\" %%' OR sql LIKE '%%( %v %%' OR sql LIKE '%%, %v %%');\n", columnName, columnName, columnName, columnName), tableName) + return count > 0 +} + +func (s sqlite3) HasIndex(scope *Scope, tableName string, indexName string) bool { + var count int + s.RawScanInt(scope, &count, fmt.Sprintf("SELECT count(*) FROM sqlite_master WHERE tbl_name = ? AND sql LIKE '%%INDEX %v ON%%'", indexName), tableName) + return count > 0 +} + +func (sqlite3) RemoveIndex(scope *Scope, indexName string) { + scope.Err(scope.NewDB().Exec(fmt.Sprintf("DROP INDEX %v", indexName)).Error) +} + +func (sqlite3) CurrentDatabase(scope *Scope) (name string) { + var ( + ifaces = make([]interface{}, 3) + pointers = make([]*string, 3) + i int + ) + for i = 0; i < 3; i++ { + ifaces[i] = &pointers[i] + } + if err := scope.NewDB().Raw("PRAGMA database_list").Row().Scan(ifaces...); scope.Err(err) != nil { + return + } + if pointers[1] != nil { + name = *pointers[1] + } + return +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/structs_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/structs_test.go new file mode 100644 index 0000000..9a9b23d --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/structs_test.go @@ -0,0 +1,219 @@ +package gorm_test + +import ( + "database/sql" + "database/sql/driver" + "errors" + "fmt" + + "reflect" + "time" +) + +type User struct { + Id int64 + Age int64 + UserNum Num + Name string `sql:"size:255"` + Birthday time.Time // Time + CreatedAt time.Time // CreatedAt: Time of record is created, will be insert automatically + UpdatedAt time.Time // UpdatedAt: Time of record is updated, will be updated automatically + Emails []Email // Embedded structs + BillingAddress Address // Embedded struct + BillingAddressID sql.NullInt64 // Embedded struct's foreign key + ShippingAddress Address // Embedded struct + ShippingAddressId int64 // Embedded struct's foreign key + CreditCard CreditCard + Latitude float64 + Languages []Language `gorm:"many2many:user_languages;"` + CompanyID int64 + Company Company + Role + PasswordHash []byte + IgnoreMe int64 `sql:"-"` + IgnoreStringSlice []string `sql:"-"` + Ignored struct{ Name string } `sql:"-"` + IgnoredPointer *User `sql:"-"` +} + +type CreditCard struct { + ID int8 + Number string + UserId sql.NullInt64 + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt time.Time +} + +type Email struct { + Id int16 + UserId int + Email string `sql:"type:varchar(100);"` + CreatedAt time.Time + UpdatedAt time.Time +} + +type Address struct { + ID int + Address1 string + Address2 string + Post string + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt time.Time +} + +type Language struct { + Id int + Name string + Users []User `gorm:"many2many:user_languages;"` +} + +type Product struct { + Id int64 + Code string + Price int64 + CreatedAt time.Time + UpdatedAt time.Time + AfterFindCallTimes int64 + BeforeCreateCallTimes int64 + AfterCreateCallTimes int64 + BeforeUpdateCallTimes int64 + AfterUpdateCallTimes int64 + BeforeSaveCallTimes int64 + AfterSaveCallTimes int64 + BeforeDeleteCallTimes int64 + AfterDeleteCallTimes int64 +} + +type Company struct { + Id int64 + Name string + Owner *User `sql:"-"` +} + +type Role struct { + Name string +} + +func (role *Role) Scan(value interface{}) error { + if b, ok := value.([]uint8); ok { + role.Name = string(b) + } else { + role.Name = value.(string) + } + return nil +} + +func (role Role) Value() (driver.Value, error) { + return role.Name, nil +} + +func (role Role) IsAdmin() bool { + return role.Name == "admin" +} + +type Num int64 + +func (i *Num) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + case int64: + *i = Num(s) + default: + return errors.New("Cannot scan NamedInt from " + reflect.ValueOf(src).String()) + } + return nil +} + +type Animal struct { + Counter uint64 `gorm:"primary_key:yes"` + Name string `sql:"DEFAULT:'galeone'"` + From string //test reserved sql keyword as field name + Age time.Time `sql:"DEFAULT:current_timestamp"` + unexported string // unexported value + CreatedAt time.Time + UpdatedAt time.Time +} + +type JoinTable struct { + From uint64 + To uint64 + Time time.Time `sql:"default: null"` +} + +type Post struct { + Id int64 + CategoryId sql.NullInt64 + MainCategoryId int64 + Title string + Body string + Comments []*Comment + Category Category + MainCategory Category +} + +type Category struct { + Id int64 + Name string +} + +type Comment struct { + Id int64 + PostId int64 + Content string + Post Post +} + +// Scanner +type NullValue struct { + Id int64 + Name sql.NullString `sql:"not null"` + Age sql.NullInt64 + Male sql.NullBool + Height sql.NullFloat64 + AddedAt NullTime +} + +type NullTime struct { + Time time.Time + Valid bool +} + +func (nt *NullTime) Scan(value interface{}) error { + if value == nil { + nt.Valid = false + return nil + } + nt.Time, nt.Valid = value.(time.Time), true + return nil +} + +func (nt NullTime) Value() (driver.Value, error) { + if !nt.Valid { + return nil, nil + } + return nt.Time, nil +} + +func getPreparedUser(name string, role string) *User { + var company Company + DB.Where(Company{Name: role}).FirstOrCreate(&company) + + return &User{ + Name: name, + Age: 20, + Role: Role{role}, + BillingAddress: Address{Address1: fmt.Sprintf("Billing Address %v", name)}, + ShippingAddress: Address{Address1: fmt.Sprintf("Shipping Address %v", name)}, + CreditCard: CreditCard{Number: fmt.Sprintf("123456%v", name)}, + Emails: []Email{ + {Email: fmt.Sprintf("user_%v@example1.com", name)}, {Email: fmt.Sprintf("user_%v@example2.com", name)}, + }, + Company: company, + Languages: []Language{ + {Name: fmt.Sprintf("lang_1_%v", name)}, + {Name: fmt.Sprintf("lang_2_%v", name)}, + }, + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/test_all.sh b/Godeps/_workspace/src/github.com/jinzhu/gorm/test_all.sh new file mode 100644 index 0000000..6c5593b --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/test_all.sh @@ -0,0 +1,5 @@ +dialects=("postgres" "mysql" "sqlite") + +for dialect in "${dialects[@]}" ; do + GORM_DIALECT=${dialect} go test +done diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/update_test.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/update_test.go new file mode 100644 index 0000000..9a0af80 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/update_test.go @@ -0,0 +1,413 @@ +package gorm_test + +import ( + "testing" + "time" + + "github.com/jinzhu/gorm" +) + +func TestUpdate(t *testing.T) { + product1 := Product{Code: "product1code"} + product2 := Product{Code: "product2code"} + + DB.Save(&product1).Save(&product2).Update("code", "product2newcode") + + if product2.Code != "product2newcode" { + t.Errorf("Record should be updated") + } + + DB.First(&product1, product1.Id) + DB.First(&product2, product2.Id) + updatedAt1 := product1.UpdatedAt + updatedAt2 := product2.UpdatedAt + + var product3 Product + DB.First(&product3, product2.Id).Update("code", "product2newcode") + if updatedAt2.Format(time.RFC3339Nano) != product3.UpdatedAt.Format(time.RFC3339Nano) { + t.Errorf("updatedAt should not be updated if nothing changed") + } + + if DB.First(&Product{}, "code = ?", product1.Code).RecordNotFound() { + t.Errorf("Product1 should not be updated") + } + + if !DB.First(&Product{}, "code = ?", "product2code").RecordNotFound() { + t.Errorf("Product2's code should be updated") + } + + if DB.First(&Product{}, "code = ?", "product2newcode").RecordNotFound() { + t.Errorf("Product2's code should be updated") + } + + DB.Table("products").Where("code in (?)", []string{"product1code"}).Update("code", "product1newcode") + + var product4 Product + DB.First(&product4, product1.Id) + if updatedAt1.Format(time.RFC3339Nano) != product4.UpdatedAt.Format(time.RFC3339Nano) { + t.Errorf("updatedAt should be updated if something changed") + } + + if !DB.First(&Product{}, "code = 'product1code'").RecordNotFound() { + t.Errorf("Product1's code should be updated") + } + + if DB.First(&Product{}, "code = 'product1newcode'").RecordNotFound() { + t.Errorf("Product should not be changed to 789") + } + + if DB.Model(product2).Update("CreatedAt", time.Now().Add(time.Hour)).Error != nil { + t.Error("No error should raise when update with CamelCase") + } + + if DB.Model(&product2).UpdateColumn("CreatedAt", time.Now().Add(time.Hour)).Error != nil { + t.Error("No error should raise when update_column with CamelCase") + } + + var products []Product + DB.Find(&products) + if count := DB.Model(Product{}).Update("CreatedAt", time.Now().Add(2*time.Hour)).RowsAffected; count != int64(len(products)) { + t.Error("RowsAffected should be correct when do batch update") + } + + DB.First(&product4, product4.Id) + DB.Model(&product4).Update("price", gorm.Expr("price + ? - ?", 100, 50)) + var product5 Product + DB.First(&product5, product4.Id) + if product5.Price != product4.Price+100-50 { + t.Errorf("Update with expression") + } + if product5.UpdatedAt.Format(time.RFC3339Nano) == product4.UpdatedAt.Format(time.RFC3339Nano) { + t.Errorf("Update with expression should update UpdatedAt") + } +} + +func TestUpdateWithNoStdPrimaryKeyAndDefaultValues(t *testing.T) { + animal := Animal{Name: "Ferdinand"} + DB.Save(&animal) + updatedAt1 := animal.UpdatedAt + + DB.Save(&animal).Update("name", "Francis") + + if updatedAt1.Format(time.RFC3339Nano) == animal.UpdatedAt.Format(time.RFC3339Nano) { + t.Errorf("updatedAt should not be updated if nothing changed") + } + + var animals []Animal + DB.Find(&animals) + if count := DB.Model(Animal{}).Update("CreatedAt", time.Now().Add(2*time.Hour)).RowsAffected; count != int64(len(animals)) { + t.Error("RowsAffected should be correct when do batch update") + } + + animal = Animal{From: "somewhere"} // No name fields, should be filled with the default value (galeone) + DB.Save(&animal).Update("From", "a nice place") // The name field shoul be untouched + DB.First(&animal, animal.Counter) + if animal.Name != "galeone" { + t.Errorf("Name fiels shouldn't be changed if untouched, but got %v", animal.Name) + } + + // When changing a field with a default value, the change must occur + animal.Name = "amazing horse" + DB.Save(&animal) + DB.First(&animal, animal.Counter) + if animal.Name != "amazing horse" { + t.Errorf("Update a filed with a default value should occur. But got %v\n", animal.Name) + } +} + +func TestUpdates(t *testing.T) { + product1 := Product{Code: "product1code", Price: 10} + product2 := Product{Code: "product2code", Price: 10} + DB.Save(&product1).Save(&product2) + DB.Model(&product1).Updates(map[string]interface{}{"code": "product1newcode", "price": 100}) + if product1.Code != "product1newcode" || product1.Price != 100 { + t.Errorf("Record should be updated also with map") + } + + DB.First(&product1, product1.Id) + DB.First(&product2, product2.Id) + updatedAt1 := product1.UpdatedAt + updatedAt2 := product2.UpdatedAt + + var product3 Product + DB.First(&product3, product1.Id).Updates(Product{Code: "product1newcode", Price: 100}) + if product3.Code != "product1newcode" || product3.Price != 100 { + t.Errorf("Record should be updated with struct") + } + + if updatedAt1.Format(time.RFC3339Nano) != product3.UpdatedAt.Format(time.RFC3339Nano) { + t.Errorf("updatedAt should not be updated if nothing changed") + } + + if DB.First(&Product{}, "code = ? and price = ?", product2.Code, product2.Price).RecordNotFound() { + t.Errorf("Product2 should not be updated") + } + + if DB.First(&Product{}, "code = ?", "product1newcode").RecordNotFound() { + t.Errorf("Product1 should be updated") + } + + DB.Table("products").Where("code in (?)", []string{"product2code"}).Updates(Product{Code: "product2newcode"}) + if !DB.First(&Product{}, "code = 'product2code'").RecordNotFound() { + t.Errorf("Product2's code should be updated") + } + + var product4 Product + DB.First(&product4, product2.Id) + if updatedAt2.Format(time.RFC3339Nano) != product4.UpdatedAt.Format(time.RFC3339Nano) { + t.Errorf("updatedAt should be updated if something changed") + } + + if DB.First(&Product{}, "code = ?", "product2newcode").RecordNotFound() { + t.Errorf("product2's code should be updated") + } + + DB.Model(&product4).Updates(map[string]interface{}{"price": gorm.Expr("price + ?", 100)}) + var product5 Product + DB.First(&product5, product4.Id) + if product5.Price != product4.Price+100 { + t.Errorf("Updates with expression") + } + if product5.UpdatedAt.Format(time.RFC3339Nano) == product4.UpdatedAt.Format(time.RFC3339Nano) { + t.Errorf("Updates with expression should update UpdatedAt") + } +} + +func TestUpdateColumn(t *testing.T) { + product1 := Product{Code: "product1code", Price: 10} + product2 := Product{Code: "product2code", Price: 20} + DB.Save(&product1).Save(&product2).UpdateColumn(map[string]interface{}{"code": "product2newcode", "price": 100}) + if product2.Code != "product2newcode" || product2.Price != 100 { + t.Errorf("product 2 should be updated with update column") + } + + var product3 Product + DB.First(&product3, product1.Id) + if product3.Code != "product1code" || product3.Price != 10 { + t.Errorf("product 1 should not be updated") + } + + DB.First(&product2, product2.Id) + updatedAt2 := product2.UpdatedAt + DB.Model(product2).UpdateColumn("code", "update_column_new") + var product4 Product + DB.First(&product4, product2.Id) + if updatedAt2.Format(time.RFC3339Nano) != product4.UpdatedAt.Format(time.RFC3339Nano) { + t.Errorf("updatedAt should not be updated with update column") + } + + DB.Model(&product4).UpdateColumn("price", gorm.Expr("price + 100 - 50")) + var product5 Product + DB.First(&product5, product4.Id) + if product5.Price != product4.Price+100-50 { + t.Errorf("UpdateColumn with expression") + } + if product5.UpdatedAt.Format(time.RFC3339Nano) != product4.UpdatedAt.Format(time.RFC3339Nano) { + t.Errorf("UpdateColumn with expression should not update UpdatedAt") + } +} + +func TestSelectWithUpdate(t *testing.T) { + user := getPreparedUser("select_user", "select_with_update") + DB.Create(user) + + var reloadUser User + DB.First(&reloadUser, user.Id) + reloadUser.Name = "new_name" + reloadUser.Age = 50 + reloadUser.BillingAddress = Address{Address1: "New Billing Address"} + reloadUser.ShippingAddress = Address{Address1: "New ShippingAddress Address"} + reloadUser.CreditCard = CreditCard{Number: "987654321"} + reloadUser.Emails = []Email{ + {Email: "new_user_1@example1.com"}, {Email: "new_user_2@example2.com"}, {Email: "new_user_3@example2.com"}, + } + reloadUser.Company = Company{Name: "new company"} + + DB.Select("Name", "BillingAddress", "CreditCard", "Company", "Emails").Save(&reloadUser) + + var queryUser User + DB.Preload("BillingAddress").Preload("ShippingAddress"). + Preload("CreditCard").Preload("Emails").Preload("Company").First(&queryUser, user.Id) + + if queryUser.Name == user.Name || queryUser.Age != user.Age { + t.Errorf("Should only update users with name column") + } + + if queryUser.BillingAddressID.Int64 == user.BillingAddressID.Int64 || + queryUser.ShippingAddressId != user.ShippingAddressId || + queryUser.CreditCard.ID == user.CreditCard.ID || + len(queryUser.Emails) == len(user.Emails) || queryUser.Company.Id == user.Company.Id { + t.Errorf("Should only update selected relationships") + } +} + +func TestSelectWithUpdateWithMap(t *testing.T) { + user := getPreparedUser("select_user", "select_with_update_map") + DB.Create(user) + + updateValues := map[string]interface{}{ + "Name": "new_name", + "Age": 50, + "BillingAddress": Address{Address1: "New Billing Address"}, + "ShippingAddress": Address{Address1: "New ShippingAddress Address"}, + "CreditCard": CreditCard{Number: "987654321"}, + "Emails": []Email{ + {Email: "new_user_1@example1.com"}, {Email: "new_user_2@example2.com"}, {Email: "new_user_3@example2.com"}, + }, + "Company": Company{Name: "new company"}, + } + + var reloadUser User + DB.First(&reloadUser, user.Id) + DB.Model(&reloadUser).Select("Name", "BillingAddress", "CreditCard", "Company", "Emails").Update(updateValues) + + var queryUser User + DB.Preload("BillingAddress").Preload("ShippingAddress"). + Preload("CreditCard").Preload("Emails").Preload("Company").First(&queryUser, user.Id) + + if queryUser.Name == user.Name || queryUser.Age != user.Age { + t.Errorf("Should only update users with name column") + } + + if queryUser.BillingAddressID.Int64 == user.BillingAddressID.Int64 || + queryUser.ShippingAddressId != user.ShippingAddressId || + queryUser.CreditCard.ID == user.CreditCard.ID || + len(queryUser.Emails) == len(user.Emails) || queryUser.Company.Id == user.Company.Id { + t.Errorf("Should only update selected relationships") + } +} + +func TestOmitWithUpdate(t *testing.T) { + user := getPreparedUser("omit_user", "omit_with_update") + DB.Create(user) + + var reloadUser User + DB.First(&reloadUser, user.Id) + reloadUser.Name = "new_name" + reloadUser.Age = 50 + reloadUser.BillingAddress = Address{Address1: "New Billing Address"} + reloadUser.ShippingAddress = Address{Address1: "New ShippingAddress Address"} + reloadUser.CreditCard = CreditCard{Number: "987654321"} + reloadUser.Emails = []Email{ + {Email: "new_user_1@example1.com"}, {Email: "new_user_2@example2.com"}, {Email: "new_user_3@example2.com"}, + } + reloadUser.Company = Company{Name: "new company"} + + DB.Omit("Name", "BillingAddress", "CreditCard", "Company", "Emails").Save(&reloadUser) + + var queryUser User + DB.Preload("BillingAddress").Preload("ShippingAddress"). + Preload("CreditCard").Preload("Emails").Preload("Company").First(&queryUser, user.Id) + + if queryUser.Name != user.Name || queryUser.Age == user.Age { + t.Errorf("Should only update users with name column") + } + + if queryUser.BillingAddressID.Int64 != user.BillingAddressID.Int64 || + queryUser.ShippingAddressId == user.ShippingAddressId || + queryUser.CreditCard.ID != user.CreditCard.ID || + len(queryUser.Emails) != len(user.Emails) || queryUser.Company.Id != user.Company.Id { + t.Errorf("Should only update relationships that not omited") + } +} + +func TestOmitWithUpdateWithMap(t *testing.T) { + user := getPreparedUser("select_user", "select_with_update_map") + DB.Create(user) + + updateValues := map[string]interface{}{ + "Name": "new_name", + "Age": 50, + "BillingAddress": Address{Address1: "New Billing Address"}, + "ShippingAddress": Address{Address1: "New ShippingAddress Address"}, + "CreditCard": CreditCard{Number: "987654321"}, + "Emails": []Email{ + {Email: "new_user_1@example1.com"}, {Email: "new_user_2@example2.com"}, {Email: "new_user_3@example2.com"}, + }, + "Company": Company{Name: "new company"}, + } + + var reloadUser User + DB.First(&reloadUser, user.Id) + DB.Model(&reloadUser).Omit("Name", "BillingAddress", "CreditCard", "Company", "Emails").Update(updateValues) + + var queryUser User + DB.Preload("BillingAddress").Preload("ShippingAddress"). + Preload("CreditCard").Preload("Emails").Preload("Company").First(&queryUser, user.Id) + + if queryUser.Name != user.Name || queryUser.Age == user.Age { + t.Errorf("Should only update users with name column") + } + + if queryUser.BillingAddressID.Int64 != user.BillingAddressID.Int64 || + queryUser.ShippingAddressId == user.ShippingAddressId || + queryUser.CreditCard.ID != user.CreditCard.ID || + len(queryUser.Emails) != len(user.Emails) || queryUser.Company.Id != user.Company.Id { + t.Errorf("Should only update relationships not omited") + } +} + +func TestSelectWithUpdateColumn(t *testing.T) { + user := getPreparedUser("select_user", "select_with_update_map") + DB.Create(user) + + updateValues := map[string]interface{}{"Name": "new_name", "Age": 50} + + var reloadUser User + DB.First(&reloadUser, user.Id) + DB.Model(&reloadUser).Select("Name").UpdateColumn(updateValues) + + var queryUser User + DB.First(&queryUser, user.Id) + + if queryUser.Name == user.Name || queryUser.Age != user.Age { + t.Errorf("Should only update users with name column") + } +} + +func TestOmitWithUpdateColumn(t *testing.T) { + user := getPreparedUser("select_user", "select_with_update_map") + DB.Create(user) + + updateValues := map[string]interface{}{"Name": "new_name", "Age": 50} + + var reloadUser User + DB.First(&reloadUser, user.Id) + DB.Model(&reloadUser).Omit("Name").UpdateColumn(updateValues) + + var queryUser User + DB.First(&queryUser, user.Id) + + if queryUser.Name != user.Name || queryUser.Age == user.Age { + t.Errorf("Should omit name column when update user") + } +} + +func TestUpdateColumnsSkipsAssociations(t *testing.T) { + user := getPreparedUser("update_columns_user", "special_role") + user.Age = 99 + address1 := "first street" + user.BillingAddress = Address{Address1: address1} + DB.Save(user) + + // Update a single field of the user and verify that the changed address is not stored. + newAge := int64(100) + user.BillingAddress.Address1 = "second street" + db := DB.Model(user).UpdateColumns(User{Age: newAge}) + if db.RowsAffected != 1 { + t.Errorf("Expected RowsAffected=1 but instead RowsAffected=%v", DB.RowsAffected) + } + + // Verify that Age now=`newAge`. + freshUser := &User{Id: user.Id} + DB.First(freshUser) + if freshUser.Age != newAge { + t.Errorf("Expected freshly queried user to have Age=%v but instead found Age=%v", newAge, freshUser.Age) + } + + // Verify that user's BillingAddress.Address1 is not changed and is still "first street". + DB.First(&freshUser.BillingAddress, freshUser.BillingAddressID) + if freshUser.BillingAddress.Address1 != address1 { + t.Errorf("Expected user's BillingAddress.Address1=%s to remain unchanged after UpdateColumns invocation, but BillingAddress.Address1=%s", address1, freshUser.BillingAddress.Address1) + } +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/utils.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/utils.go new file mode 100644 index 0000000..ca7e04e --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/utils.go @@ -0,0 +1,48 @@ +package gorm + +import ( + "bytes" + "strings" +) + +// Copied from golint +var commonInitialisms = []string{"API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "LHS", "QPS", "RAM", "RHS", "RPC", "SLA", "SMTP", "SSH", "TLS", "TTL", "UI", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XSRF", "XSS"} +var commonInitialismsReplacer *strings.Replacer + +func init() { + var commonInitialismsForReplacer []string + for _, initialism := range commonInitialisms { + commonInitialismsForReplacer = append(commonInitialismsForReplacer, initialism, strings.Title(strings.ToLower(initialism))) + } + commonInitialismsReplacer = strings.NewReplacer(commonInitialismsForReplacer...) +} + +var smap = map[string]string{} + +func ToDBName(name string) string { + if v, ok := smap[name]; ok { + return v + } + + value := commonInitialismsReplacer.Replace(name) + buf := bytes.NewBufferString("") + for i, v := range value { + if i > 0 && v >= 'A' && v <= 'Z' { + buf.WriteRune('_') + } + buf.WriteRune(v) + } + + s := strings.ToLower(buf.String()) + smap[name] = s + return s +} + +type expr struct { + expr string + args []interface{} +} + +func Expr(expression string, args ...interface{}) *expr { + return &expr{expr: expression, args: args} +} diff --git a/Godeps/_workspace/src/github.com/jinzhu/gorm/utils_private.go b/Godeps/_workspace/src/github.com/jinzhu/gorm/utils_private.go new file mode 100644 index 0000000..6f609ae --- /dev/null +++ b/Godeps/_workspace/src/github.com/jinzhu/gorm/utils_private.go @@ -0,0 +1,73 @@ +package gorm + +import ( + "fmt" + "reflect" + "regexp" + "runtime" +) + +func fileWithLineNum() string { + for i := 2; i < 15; i++ { + _, file, line, ok := runtime.Caller(i) + if ok && (!regexp.MustCompile(`jinzhu/gorm/.*.go`).MatchString(file) || regexp.MustCompile(`jinzhu/gorm/.*test.go`).MatchString(file)) { + return fmt.Sprintf("%v:%v", file, line) + } + } + return "" +} + +func isBlank(value reflect.Value) bool { + return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface()) +} + +func toSearchableMap(attrs ...interface{}) (result interface{}) { + if len(attrs) > 1 { + if str, ok := attrs[0].(string); ok { + result = map[string]interface{}{str: attrs[1]} + } + } else if len(attrs) == 1 { + if attr, ok := attrs[0].(map[string]interface{}); ok { + result = attr + } + + if attr, ok := attrs[0].(interface{}); ok { + result = attr + } + } + return +} + +func convertInterfaceToMap(values interface{}) map[string]interface{} { + attrs := map[string]interface{}{} + + switch value := values.(type) { + case map[string]interface{}: + for k, v := range value { + attrs[ToDBName(k)] = v + } + case []interface{}: + for _, v := range value { + for key, value := range convertInterfaceToMap(v) { + attrs[key] = value + } + } + case interface{}: + reflectValue := reflect.ValueOf(values) + + switch reflectValue.Kind() { + case reflect.Map: + for _, key := range reflectValue.MapKeys() { + attrs[ToDBName(key.Interface().(string))] = reflectValue.MapIndex(key).Interface() + } + default: + scope := Scope{Value: values} + for _, field := range scope.Fields() { + if !field.IsBlank && !field.IsIgnored { + attrs[field.DBName] = field.Field.Interface() + } + } + } + } + return attrs +} diff --git a/Godeps/_workspace/src/github.com/lib/pq/.gitignore b/Godeps/_workspace/src/github.com/lib/pq/.gitignore new file mode 100644 index 0000000..0f1d00e --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/.gitignore @@ -0,0 +1,4 @@ +.db +*.test +*~ +*.swp diff --git a/Godeps/_workspace/src/github.com/lib/pq/.travis.yml b/Godeps/_workspace/src/github.com/lib/pq/.travis.yml new file mode 100644 index 0000000..9bf6837 --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/.travis.yml @@ -0,0 +1,68 @@ +language: go + +go: + - 1.1 + - 1.2 + - 1.3 + - 1.4 + - tip + +before_install: + - psql --version + - sudo /etc/init.d/postgresql stop + - sudo apt-get -y --purge remove postgresql libpq-dev libpq5 postgresql-client-common postgresql-common + - sudo rm -rf /var/lib/postgresql + - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - + - sudo sh -c "echo deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main $PGVERSION >> /etc/apt/sources.list.d/postgresql.list" + - sudo apt-get update -qq + - sudo apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::="--force-confnew" install postgresql-$PGVERSION postgresql-server-dev-$PGVERSION postgresql-contrib-$PGVERSION + - sudo chmod 777 /etc/postgresql/$PGVERSION/main/pg_hba.conf + - echo "local all postgres trust" > /etc/postgresql/$PGVERSION/main/pg_hba.conf + - echo "local all all trust" >> /etc/postgresql/$PGVERSION/main/pg_hba.conf + - echo "hostnossl all pqgossltest 127.0.0.1/32 reject" >> /etc/postgresql/$PGVERSION/main/pg_hba.conf + - echo "hostnossl all pqgosslcert 127.0.0.1/32 reject" >> /etc/postgresql/$PGVERSION/main/pg_hba.conf + - echo "hostssl all pqgossltest 127.0.0.1/32 trust" >> /etc/postgresql/$PGVERSION/main/pg_hba.conf + - echo "hostssl all pqgosslcert 127.0.0.1/32 cert" >> /etc/postgresql/$PGVERSION/main/pg_hba.conf + - echo "host all all 127.0.0.1/32 trust" >> /etc/postgresql/$PGVERSION/main/pg_hba.conf + - echo "hostnossl all pqgossltest ::1/128 reject" >> /etc/postgresql/$PGVERSION/main/pg_hba.conf + - echo "hostnossl all pqgosslcert ::1/128 reject" >> /etc/postgresql/$PGVERSION/main/pg_hba.conf + - echo "hostssl all pqgossltest ::1/128 trust" >> /etc/postgresql/$PGVERSION/main/pg_hba.conf + - echo "hostssl all pqgosslcert ::1/128 cert" >> /etc/postgresql/$PGVERSION/main/pg_hba.conf + - echo "host all all ::1/128 trust" >> /etc/postgresql/$PGVERSION/main/pg_hba.conf + - sudo install -o postgres -g postgres -m 600 -t /var/lib/postgresql/$PGVERSION/main/ certs/server.key certs/server.crt certs/root.crt + - sudo bash -c "[[ '${PGVERSION}' < '9.2' ]] || (echo \"ssl_cert_file = 'server.crt'\" >> /etc/postgresql/$PGVERSION/main/postgresql.conf)" + - sudo bash -c "[[ '${PGVERSION}' < '9.2' ]] || (echo \"ssl_key_file = 'server.key'\" >> /etc/postgresql/$PGVERSION/main/postgresql.conf)" + - sudo bash -c "[[ '${PGVERSION}' < '9.2' ]] || (echo \"ssl_ca_file = 'root.crt'\" >> /etc/postgresql/$PGVERSION/main/postgresql.conf)" + - sudo sh -c "echo 127.0.0.1 postgres >> /etc/hosts" + - sudo ls -l /var/lib/postgresql/$PGVERSION/main/ + - sudo cat /etc/postgresql/$PGVERSION/main/postgresql.conf + - sudo chmod 600 $PQSSLCERTTEST_PATH/postgresql.key + - sudo /etc/init.d/postgresql restart + +env: + global: + - PGUSER=postgres + - PQGOSSLTESTS=1 + - PQSSLCERTTEST_PATH=$PWD/certs + - PGHOST=127.0.0.1 + matrix: + - PGVERSION=9.4 PQTEST_BINARY_PARAMETERS=yes + - PGVERSION=9.3 PQTEST_BINARY_PARAMETERS=yes + - PGVERSION=9.2 PQTEST_BINARY_PARAMETERS=yes + - PGVERSION=9.1 PQTEST_BINARY_PARAMETERS=yes + - PGVERSION=9.0 PQTEST_BINARY_PARAMETERS=yes + - PGVERSION=8.4 PQTEST_BINARY_PARAMETERS=yes + - PGVERSION=9.4 PQTEST_BINARY_PARAMETERS=no + - PGVERSION=9.3 PQTEST_BINARY_PARAMETERS=no + - PGVERSION=9.2 PQTEST_BINARY_PARAMETERS=no + - PGVERSION=9.1 PQTEST_BINARY_PARAMETERS=no + - PGVERSION=9.0 PQTEST_BINARY_PARAMETERS=no + - PGVERSION=8.4 PQTEST_BINARY_PARAMETERS=no + +script: + - go test -v ./... + +before_script: + - psql -c 'create database pqgotest' -U postgres + - psql -c 'create user pqgossltest' -U postgres + - psql -c 'create user pqgosslcert' -U postgres diff --git a/Godeps/_workspace/src/github.com/lib/pq/CONTRIBUTING.md b/Godeps/_workspace/src/github.com/lib/pq/CONTRIBUTING.md new file mode 100644 index 0000000..84c937f --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/CONTRIBUTING.md @@ -0,0 +1,29 @@ +## Contributing to pq + +`pq` has a backlog of pull requests, but contributions are still very +much welcome. You can help with patch review, submitting bug reports, +or adding new functionality. There is no formal style guide, but +please conform to the style of existing code and general Go formatting +conventions when submitting patches. + +### Patch review + +Help review existing open pull requests by commenting on the code or +proposed functionality. + +### Bug reports + +We appreciate any bug reports, but especially ones with self-contained +(doesn't depend on code outside of pq), minimal (can't be simplified +further) test cases. It's especially helpful if you can submit a pull +request with just the failing test case (you'll probably want to +pattern it after the tests in +[conn_test.go](https://github.com/lib/pq/blob/master/conn_test.go). + +### New functionality + +There are a number of pending patches for new functionality, so +additional feature patches will take a while to merge. Still, patches +are generally reviewed based on usefulness and complexity in addition +to time-in-queue, so if you have a knockout idea, take a shot. Feel +free to open an issue discussion your proposed patch beforehand. diff --git a/Godeps/_workspace/src/github.com/lib/pq/LICENSE.md b/Godeps/_workspace/src/github.com/lib/pq/LICENSE.md new file mode 100644 index 0000000..5773904 --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/LICENSE.md @@ -0,0 +1,8 @@ +Copyright (c) 2011-2013, 'pq' Contributors +Portions Copyright (C) 2011 Blake Mizerany + +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/Godeps/_workspace/src/github.com/lib/pq/README.md b/Godeps/_workspace/src/github.com/lib/pq/README.md new file mode 100644 index 0000000..358d644 --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/README.md @@ -0,0 +1,103 @@ +# pq - A pure Go postgres driver for Go's database/sql package + +[![Build Status](https://travis-ci.org/lib/pq.png?branch=master)](https://travis-ci.org/lib/pq) + +## Install + + go get github.com/lib/pq + +## Docs + +For detailed documentation and basic usage examples, please see the package +documentation at . + +## Tests + +`go test` is used for testing. A running PostgreSQL server is +required, with the ability to log in. The default database to connect +to test with is "pqgotest," but it can be overridden using environment +variables. + +Example: + + PGHOST=/var/run/postgresql go test github.com/lib/pq + +Optionally, a benchmark suite can be run as part of the tests: + + PGHOST=/var/run/postgresql go test -bench . + +## Features + +* SSL +* Handles bad connections for `database/sql` +* Scan `time.Time` correctly (i.e. `timestamp[tz]`, `time[tz]`, `date`) +* Scan binary blobs correctly (i.e. `bytea`) +* Package for `hstore` support +* COPY FROM support +* pq.ParseURL for converting urls to connection strings for sql.Open. +* Many libpq compatible environment variables +* Unix socket support +* Notifications: `LISTEN`/`NOTIFY` + +## Future / Things you can help with + +* Better COPY FROM / COPY TO (see discussion in #181) + +## Thank you (alphabetical) + +Some of these contributors are from the original library `bmizerany/pq.go` whose +code still exists in here. + +* Andy Balholm (andybalholm) +* Ben Berkert (benburkert) +* Benjamin Heatwole (bheatwole) +* Bill Mill (llimllib) +* Bjørn Madsen (aeons) +* Blake Gentry (bgentry) +* Brad Fitzpatrick (bradfitz) +* Charlie Melbye (cmelbye) +* Chris Bandy (cbandy) +* Chris Gilling (cgilling) +* Chris Walsh (cwds) +* Dan Sosedoff (sosedoff) +* Daniel Farina (fdr) +* Eric Chlebek (echlebek) +* Eric Garrido (minusnine) +* Eric Urban (hydrogen18) +* Everyone at The Go Team +* Evan Shaw (edsrzf) +* Ewan Chou (coocood) +* Federico Romero (federomero) +* Fumin (fumin) +* Gary Burd (garyburd) +* Heroku (heroku) +* James Pozdena (jpoz) +* Jason McVetta (jmcvetta) +* Jeremy Jay (pbnjay) +* Joakim Sernbrant (serbaut) +* John Gallagher (jgallagher) +* Jonathan Rudenberg (titanous) +* Joël Stemmer (jstemmer) +* Kamil Kisiel (kisielk) +* Kelly Dunn (kellydunn) +* Keith Rarick (kr) +* Kir Shatrov (kirs) +* Lann Martin (lann) +* Maciek Sakrejda (deafbybeheading) +* Marc Brinkmann (mbr) +* Marko Tiikkaja (johto) +* Matt Newberry (MattNewberry) +* Matt Robenolt (mattrobenolt) +* Martin Olsen (martinolsen) +* Mike Lewis (mikelikespie) +* Nicolas Patry (Narsil) +* Oliver Tonnhofer (olt) +* Patrick Hayes (phayes) +* Paul Hammond (paulhammond) +* Ryan Smith (ryandotsmith) +* Samuel Stauffer (samuel) +* Timothée Peignier (cyberdelia) +* Travis Cline (tmc) +* TruongSinh Tran-Nguyen (truongsinh) +* Yaismel Miranda (ympons) +* notedit (notedit) diff --git a/Godeps/_workspace/src/github.com/lib/pq/bench_test.go b/Godeps/_workspace/src/github.com/lib/pq/bench_test.go new file mode 100644 index 0000000..e71f41d --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/bench_test.go @@ -0,0 +1,435 @@ +// +build go1.1 + +package pq + +import ( + "bufio" + "bytes" + "database/sql" + "database/sql/driver" + "io" + "math/rand" + "net" + "runtime" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/lib/pq/oid" +) + +var ( + selectStringQuery = "SELECT '" + strings.Repeat("0123456789", 10) + "'" + selectSeriesQuery = "SELECT generate_series(1, 100)" +) + +func BenchmarkSelectString(b *testing.B) { + var result string + benchQuery(b, selectStringQuery, &result) +} + +func BenchmarkSelectSeries(b *testing.B) { + var result int + benchQuery(b, selectSeriesQuery, &result) +} + +func benchQuery(b *testing.B, query string, result interface{}) { + b.StopTimer() + db := openTestConn(b) + defer db.Close() + b.StartTimer() + + for i := 0; i < b.N; i++ { + benchQueryLoop(b, db, query, result) + } +} + +func benchQueryLoop(b *testing.B, db *sql.DB, query string, result interface{}) { + rows, err := db.Query(query) + if err != nil { + b.Fatal(err) + } + defer rows.Close() + for rows.Next() { + err = rows.Scan(result) + if err != nil { + b.Fatal("failed to scan", err) + } + } +} + +// reading from circularConn yields content[:prefixLen] once, followed by +// content[prefixLen:] over and over again. It never returns EOF. +type circularConn struct { + content string + prefixLen int + pos int + net.Conn // for all other net.Conn methods that will never be called +} + +func (r *circularConn) Read(b []byte) (n int, err error) { + n = copy(b, r.content[r.pos:]) + r.pos += n + if r.pos >= len(r.content) { + r.pos = r.prefixLen + } + return +} + +func (r *circularConn) Write(b []byte) (n int, err error) { return len(b), nil } + +func (r *circularConn) Close() error { return nil } + +func fakeConn(content string, prefixLen int) *conn { + c := &circularConn{content: content, prefixLen: prefixLen} + return &conn{buf: bufio.NewReader(c), c: c} +} + +// This benchmark is meant to be the same as BenchmarkSelectString, but takes +// out some of the factors this package can't control. The numbers are less noisy, +// but also the costs of network communication aren't accurately represented. +func BenchmarkMockSelectString(b *testing.B) { + b.StopTimer() + // taken from a recorded run of BenchmarkSelectString + // See: http://www.postgresql.org/docs/current/static/protocol-message-formats.html + const response = "1\x00\x00\x00\x04" + + "t\x00\x00\x00\x06\x00\x00" + + "T\x00\x00\x00!\x00\x01?column?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xc1\xff\xfe\xff\xff\xff\xff\x00\x00" + + "Z\x00\x00\x00\x05I" + + "2\x00\x00\x00\x04" + + "D\x00\x00\x00n\x00\x01\x00\x00\x00d0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" + + "C\x00\x00\x00\rSELECT 1\x00" + + "Z\x00\x00\x00\x05I" + + "3\x00\x00\x00\x04" + + "Z\x00\x00\x00\x05I" + c := fakeConn(response, 0) + b.StartTimer() + + for i := 0; i < b.N; i++ { + benchMockQuery(b, c, selectStringQuery) + } +} + +var seriesRowData = func() string { + var buf bytes.Buffer + for i := 1; i <= 100; i++ { + digits := byte(2) + if i >= 100 { + digits = 3 + } else if i < 10 { + digits = 1 + } + buf.WriteString("D\x00\x00\x00") + buf.WriteByte(10 + digits) + buf.WriteString("\x00\x01\x00\x00\x00") + buf.WriteByte(digits) + buf.WriteString(strconv.Itoa(i)) + } + return buf.String() +}() + +func BenchmarkMockSelectSeries(b *testing.B) { + b.StopTimer() + var response = "1\x00\x00\x00\x04" + + "t\x00\x00\x00\x06\x00\x00" + + "T\x00\x00\x00!\x00\x01?column?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xc1\xff\xfe\xff\xff\xff\xff\x00\x00" + + "Z\x00\x00\x00\x05I" + + "2\x00\x00\x00\x04" + + seriesRowData + + "C\x00\x00\x00\x0fSELECT 100\x00" + + "Z\x00\x00\x00\x05I" + + "3\x00\x00\x00\x04" + + "Z\x00\x00\x00\x05I" + c := fakeConn(response, 0) + b.StartTimer() + + for i := 0; i < b.N; i++ { + benchMockQuery(b, c, selectSeriesQuery) + } +} + +func benchMockQuery(b *testing.B, c *conn, query string) { + stmt, err := c.Prepare(query) + if err != nil { + b.Fatal(err) + } + defer stmt.Close() + rows, err := stmt.Query(nil) + if err != nil { + b.Fatal(err) + } + defer rows.Close() + var dest [1]driver.Value + for { + if err := rows.Next(dest[:]); err != nil { + if err == io.EOF { + break + } + b.Fatal(err) + } + } +} + +func BenchmarkPreparedSelectString(b *testing.B) { + var result string + benchPreparedQuery(b, selectStringQuery, &result) +} + +func BenchmarkPreparedSelectSeries(b *testing.B) { + var result int + benchPreparedQuery(b, selectSeriesQuery, &result) +} + +func benchPreparedQuery(b *testing.B, query string, result interface{}) { + b.StopTimer() + db := openTestConn(b) + defer db.Close() + stmt, err := db.Prepare(query) + if err != nil { + b.Fatal(err) + } + defer stmt.Close() + b.StartTimer() + + for i := 0; i < b.N; i++ { + benchPreparedQueryLoop(b, db, stmt, result) + } +} + +func benchPreparedQueryLoop(b *testing.B, db *sql.DB, stmt *sql.Stmt, result interface{}) { + rows, err := stmt.Query() + if err != nil { + b.Fatal(err) + } + if !rows.Next() { + rows.Close() + b.Fatal("no rows") + } + defer rows.Close() + for rows.Next() { + err = rows.Scan(&result) + if err != nil { + b.Fatal("failed to scan") + } + } +} + +// See the comment for BenchmarkMockSelectString. +func BenchmarkMockPreparedSelectString(b *testing.B) { + b.StopTimer() + const parseResponse = "1\x00\x00\x00\x04" + + "t\x00\x00\x00\x06\x00\x00" + + "T\x00\x00\x00!\x00\x01?column?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xc1\xff\xfe\xff\xff\xff\xff\x00\x00" + + "Z\x00\x00\x00\x05I" + const responses = parseResponse + + "2\x00\x00\x00\x04" + + "D\x00\x00\x00n\x00\x01\x00\x00\x00d0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" + + "C\x00\x00\x00\rSELECT 1\x00" + + "Z\x00\x00\x00\x05I" + c := fakeConn(responses, len(parseResponse)) + + stmt, err := c.Prepare(selectStringQuery) + if err != nil { + b.Fatal(err) + } + b.StartTimer() + + for i := 0; i < b.N; i++ { + benchPreparedMockQuery(b, c, stmt) + } +} + +func BenchmarkMockPreparedSelectSeries(b *testing.B) { + b.StopTimer() + const parseResponse = "1\x00\x00\x00\x04" + + "t\x00\x00\x00\x06\x00\x00" + + "T\x00\x00\x00!\x00\x01?column?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xc1\xff\xfe\xff\xff\xff\xff\x00\x00" + + "Z\x00\x00\x00\x05I" + var responses = parseResponse + + "2\x00\x00\x00\x04" + + seriesRowData + + "C\x00\x00\x00\x0fSELECT 100\x00" + + "Z\x00\x00\x00\x05I" + c := fakeConn(responses, len(parseResponse)) + + stmt, err := c.Prepare(selectSeriesQuery) + if err != nil { + b.Fatal(err) + } + b.StartTimer() + + for i := 0; i < b.N; i++ { + benchPreparedMockQuery(b, c, stmt) + } +} + +func benchPreparedMockQuery(b *testing.B, c *conn, stmt driver.Stmt) { + rows, err := stmt.Query(nil) + if err != nil { + b.Fatal(err) + } + defer rows.Close() + var dest [1]driver.Value + for { + if err := rows.Next(dest[:]); err != nil { + if err == io.EOF { + break + } + b.Fatal(err) + } + } +} + +func BenchmarkEncodeInt64(b *testing.B) { + for i := 0; i < b.N; i++ { + encode(¶meterStatus{}, int64(1234), oid.T_int8) + } +} + +func BenchmarkEncodeFloat64(b *testing.B) { + for i := 0; i < b.N; i++ { + encode(¶meterStatus{}, 3.14159, oid.T_float8) + } +} + +var testByteString = []byte("abcdefghijklmnopqrstuvwxyz") + +func BenchmarkEncodeByteaHex(b *testing.B) { + for i := 0; i < b.N; i++ { + encode(¶meterStatus{serverVersion: 90000}, testByteString, oid.T_bytea) + } +} +func BenchmarkEncodeByteaEscape(b *testing.B) { + for i := 0; i < b.N; i++ { + encode(¶meterStatus{serverVersion: 84000}, testByteString, oid.T_bytea) + } +} + +func BenchmarkEncodeBool(b *testing.B) { + for i := 0; i < b.N; i++ { + encode(¶meterStatus{}, true, oid.T_bool) + } +} + +var testTimestamptz = time.Date(2001, time.January, 1, 0, 0, 0, 0, time.Local) + +func BenchmarkEncodeTimestamptz(b *testing.B) { + for i := 0; i < b.N; i++ { + encode(¶meterStatus{}, testTimestamptz, oid.T_timestamptz) + } +} + +var testIntBytes = []byte("1234") + +func BenchmarkDecodeInt64(b *testing.B) { + for i := 0; i < b.N; i++ { + decode(¶meterStatus{}, testIntBytes, oid.T_int8, formatText) + } +} + +var testFloatBytes = []byte("3.14159") + +func BenchmarkDecodeFloat64(b *testing.B) { + for i := 0; i < b.N; i++ { + decode(¶meterStatus{}, testFloatBytes, oid.T_float8, formatText) + } +} + +var testBoolBytes = []byte{'t'} + +func BenchmarkDecodeBool(b *testing.B) { + for i := 0; i < b.N; i++ { + decode(¶meterStatus{}, testBoolBytes, oid.T_bool, formatText) + } +} + +func TestDecodeBool(t *testing.T) { + db := openTestConn(t) + rows, err := db.Query("select true") + if err != nil { + t.Fatal(err) + } + rows.Close() +} + +var testTimestamptzBytes = []byte("2013-09-17 22:15:32.360754-07") + +func BenchmarkDecodeTimestamptz(b *testing.B) { + for i := 0; i < b.N; i++ { + decode(¶meterStatus{}, testTimestamptzBytes, oid.T_timestamptz, formatText) + } +} + +func BenchmarkDecodeTimestamptzMultiThread(b *testing.B) { + oldProcs := runtime.GOMAXPROCS(0) + defer runtime.GOMAXPROCS(oldProcs) + runtime.GOMAXPROCS(runtime.NumCPU()) + globalLocationCache = newLocationCache() + + f := func(wg *sync.WaitGroup, loops int) { + defer wg.Done() + for i := 0; i < loops; i++ { + decode(¶meterStatus{}, testTimestamptzBytes, oid.T_timestamptz, formatText) + } + } + + wg := &sync.WaitGroup{} + b.ResetTimer() + for j := 0; j < 10; j++ { + wg.Add(1) + go f(wg, b.N/10) + } + wg.Wait() +} + +func BenchmarkLocationCache(b *testing.B) { + globalLocationCache = newLocationCache() + for i := 0; i < b.N; i++ { + globalLocationCache.getLocation(rand.Intn(10000)) + } +} + +func BenchmarkLocationCacheMultiThread(b *testing.B) { + oldProcs := runtime.GOMAXPROCS(0) + defer runtime.GOMAXPROCS(oldProcs) + runtime.GOMAXPROCS(runtime.NumCPU()) + globalLocationCache = newLocationCache() + + f := func(wg *sync.WaitGroup, loops int) { + defer wg.Done() + for i := 0; i < loops; i++ { + globalLocationCache.getLocation(rand.Intn(10000)) + } + } + + wg := &sync.WaitGroup{} + b.ResetTimer() + for j := 0; j < 10; j++ { + wg.Add(1) + go f(wg, b.N/10) + } + wg.Wait() +} + +// Stress test the performance of parsing results from the wire. +func BenchmarkResultParsing(b *testing.B) { + b.StopTimer() + + db := openTestConn(b) + defer db.Close() + _, err := db.Exec("BEGIN") + if err != nil { + b.Fatal(err) + } + + b.StartTimer() + for i := 0; i < b.N; i++ { + res, err := db.Query("SELECT generate_series(1, 50000)") + if err != nil { + b.Fatal(err) + } + res.Close() + } +} diff --git a/Godeps/_workspace/src/github.com/lib/pq/buf.go b/Godeps/_workspace/src/github.com/lib/pq/buf.go new file mode 100644 index 0000000..666b001 --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/buf.go @@ -0,0 +1,91 @@ +package pq + +import ( + "bytes" + "encoding/binary" + + "github.com/lib/pq/oid" +) + +type readBuf []byte + +func (b *readBuf) int32() (n int) { + n = int(int32(binary.BigEndian.Uint32(*b))) + *b = (*b)[4:] + return +} + +func (b *readBuf) oid() (n oid.Oid) { + n = oid.Oid(binary.BigEndian.Uint32(*b)) + *b = (*b)[4:] + return +} + +// N.B: this is actually an unsigned 16-bit integer, unlike int32 +func (b *readBuf) int16() (n int) { + n = int(binary.BigEndian.Uint16(*b)) + *b = (*b)[2:] + return +} + +func (b *readBuf) string() string { + i := bytes.IndexByte(*b, 0) + if i < 0 { + errorf("invalid message format; expected string terminator") + } + s := (*b)[:i] + *b = (*b)[i+1:] + return string(s) +} + +func (b *readBuf) next(n int) (v []byte) { + v = (*b)[:n] + *b = (*b)[n:] + return +} + +func (b *readBuf) byte() byte { + return b.next(1)[0] +} + +type writeBuf struct { + buf []byte + pos int +} + +func (b *writeBuf) int32(n int) { + x := make([]byte, 4) + binary.BigEndian.PutUint32(x, uint32(n)) + b.buf = append(b.buf, x...) +} + +func (b *writeBuf) int16(n int) { + x := make([]byte, 2) + binary.BigEndian.PutUint16(x, uint16(n)) + b.buf = append(b.buf, x...) +} + +func (b *writeBuf) string(s string) { + b.buf = append(b.buf, (s + "\000")...) +} + +func (b *writeBuf) byte(c byte) { + b.buf = append(b.buf, c) +} + +func (b *writeBuf) bytes(v []byte) { + b.buf = append(b.buf, v...) +} + +func (b *writeBuf) wrap() []byte { + p := b.buf[b.pos:] + binary.BigEndian.PutUint32(p, uint32(len(p))) + return b.buf +} + +func (b *writeBuf) next(c byte) { + p := b.buf[b.pos:] + binary.BigEndian.PutUint32(p, uint32(len(p))) + b.pos = len(b.buf) + 1 + b.buf = append(b.buf, c, 0, 0, 0, 0) +} diff --git a/Godeps/_workspace/src/github.com/lib/pq/certs/README b/Godeps/_workspace/src/github.com/lib/pq/certs/README new file mode 100644 index 0000000..24ab7b2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/certs/README @@ -0,0 +1,3 @@ +This directory contains certificates and private keys for testing some +SSL-related functionality in Travis. Do NOT use these certificates for +anything other than testing. diff --git a/Godeps/_workspace/src/github.com/lib/pq/certs/postgresql.crt b/Godeps/_workspace/src/github.com/lib/pq/certs/postgresql.crt new file mode 100644 index 0000000..6e6b428 --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/certs/postgresql.crt @@ -0,0 +1,69 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 2 (0x2) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=Nevada, L=Las Vegas, O=github.com/lib/pq, CN=pq CA + Validity + Not Before: Oct 11 15:10:11 2014 GMT + Not After : Oct 8 15:10:11 2024 GMT + Subject: C=US, ST=Nevada, L=Las Vegas, O=github.com/lib/pq, CN=pqgosslcert + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:e3:8c:06:9a:70:54:51:d1:34:34:83:39:cd:a2: + 59:0f:05:ed:8d:d8:0e:34:d0:92:f4:09:4d:ee:8c: + 78:55:49:24:f8:3c:e0:34:58:02:b2:e7:94:58:c1: + e8:e5:bb:d1:af:f6:54:c1:40:b1:90:70:79:0d:35: + 54:9c:8f:16:e9:c2:f0:92:e6:64:49:38:c1:76:f8: + 47:66:c4:5b:4a:b6:a9:43:ce:c8:be:6c:4d:2b:94: + 97:3c:55:bc:d1:d0:6e:b7:53:ae:89:5c:4b:6b:86: + 40:be:c1:ae:1e:64:ce:9c:ae:87:0a:69:e5:c8:21: + 12:be:ae:1d:f6:45:df:16:a7 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + 9B:25:31:63:A2:D8:06:FF:CB:E3:E9:96:FF:0D:BA:DC:12:7D:04:CF + X509v3 Authority Key Identifier: + keyid:52:93:ED:1E:76:0A:9F:65:4F:DE:19:66:C1:D5:22:40:35:CB:A0:72 + + X509v3 Basic Constraints: + CA:FALSE + X509v3 Key Usage: + Digital Signature, Non Repudiation, Key Encipherment + Signature Algorithm: sha256WithRSAEncryption + 3e:f5:f8:0b:4e:11:bd:00:86:1f:ce:dc:97:02:98:91:11:f5: + 65:f6:f2:8a:b2:3e:47:92:05:69:28:c9:e9:b4:f7:cf:93:d1: + 2d:81:5d:00:3c:23:be:da:70:ea:59:e1:2c:d3:25:49:ae:a6: + 95:54:c1:10:df:23:e3:fe:d6:e4:76:c7:6b:73:ad:1b:34:7c: + e2:56:cc:c0:37:ae:c5:7a:11:20:6c:3d:05:0e:99:cd:22:6c: + cf:59:a1:da:28:d4:65:ba:7d:2f:2b:3d:69:6d:a6:c1:ae:57: + bf:56:64:13:79:f8:48:46:65:eb:81:67:28:0b:7b:de:47:10: + b3:80:3c:31:d1:58:94:01:51:4a:c7:c8:1a:01:a8:af:c4:cd: + bb:84:a5:d9:8b:b4:b9:a1:64:3e:95:d9:90:1d:d5:3f:67:cc: + 3b:ba:f5:b4:d1:33:77:ee:c2:d2:3e:7e:c5:66:6e:b7:35:4c: + 60:57:b0:b8:be:36:c8:f3:d3:95:8c:28:4a:c9:f7:27:a4:0d: + e5:96:99:eb:f5:c8:bd:f3:84:6d:ef:02:f9:8a:36:7d:6b:5f: + 36:68:37:41:d9:74:ae:c6:78:2e:44:86:a1:ad:43:ca:fb:b5: + 3e:ba:10:23:09:02:ac:62:d1:d0:83:c8:95:b9:e3:5e:30:ff: + 5b:2b:38:fa +-----BEGIN CERTIFICATE----- +MIIDEzCCAfugAwIBAgIBAjANBgkqhkiG9w0BAQsFADBeMQswCQYDVQQGEwJVUzEP +MA0GA1UECBMGTmV2YWRhMRIwEAYDVQQHEwlMYXMgVmVnYXMxGjAYBgNVBAoTEWdp +dGh1Yi5jb20vbGliL3BxMQ4wDAYDVQQDEwVwcSBDQTAeFw0xNDEwMTExNTEwMTFa +Fw0yNDEwMDgxNTEwMTFaMGQxCzAJBgNVBAYTAlVTMQ8wDQYDVQQIEwZOZXZhZGEx +EjAQBgNVBAcTCUxhcyBWZWdhczEaMBgGA1UEChMRZ2l0aHViLmNvbS9saWIvcHEx +FDASBgNVBAMTC3BxZ29zc2xjZXJ0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB +gQDjjAaacFRR0TQ0gznNolkPBe2N2A400JL0CU3ujHhVSST4POA0WAKy55RYwejl +u9Gv9lTBQLGQcHkNNVScjxbpwvCS5mRJOMF2+EdmxFtKtqlDzsi+bE0rlJc8VbzR +0G63U66JXEtrhkC+wa4eZM6crocKaeXIIRK+rh32Rd8WpwIDAQABo1owWDAdBgNV +HQ4EFgQUmyUxY6LYBv/L4+mW/w263BJ9BM8wHwYDVR0jBBgwFoAUUpPtHnYKn2VP +3hlmwdUiQDXLoHIwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwDQYJKoZIhvcNAQEL +BQADggEBAD71+AtOEb0Ahh/O3JcCmJER9WX28oqyPkeSBWkoyem098+T0S2BXQA8 +I77acOpZ4SzTJUmuppVUwRDfI+P+1uR2x2tzrRs0fOJWzMA3rsV6ESBsPQUOmc0i +bM9Zodoo1GW6fS8rPWltpsGuV79WZBN5+EhGZeuBZygLe95HELOAPDHRWJQBUUrH +yBoBqK/EzbuEpdmLtLmhZD6V2ZAd1T9nzDu69bTRM3fuwtI+fsVmbrc1TGBXsLi+ +Nsjz05WMKErJ9yekDeWWmev1yL3zhG3vAvmKNn1rXzZoN0HZdK7GeC5EhqGtQ8r7 +tT66ECMJAqxi0dCDyJW5414w/1srOPo= +-----END CERTIFICATE----- diff --git a/Godeps/_workspace/src/github.com/lib/pq/certs/postgresql.key b/Godeps/_workspace/src/github.com/lib/pq/certs/postgresql.key new file mode 100644 index 0000000..eb8b20b --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/certs/postgresql.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQDjjAaacFRR0TQ0gznNolkPBe2N2A400JL0CU3ujHhVSST4POA0 +WAKy55RYwejlu9Gv9lTBQLGQcHkNNVScjxbpwvCS5mRJOMF2+EdmxFtKtqlDzsi+ +bE0rlJc8VbzR0G63U66JXEtrhkC+wa4eZM6crocKaeXIIRK+rh32Rd8WpwIDAQAB +AoGAM5dM6/kp9P700i8qjOgRPym96Zoh5nGfz/rIE5z/r36NBkdvIg8OVZfR96nH +b0b9TOMR5lsPp0sI9yivTWvX6qyvLJRWy2vvx17hXK9NxXUNTAm0PYZUTvCtcPeX +RnJpzQKNZQPkFzF0uXBc4CtPK2Vz0+FGvAelrhYAxnw1dIkCQQD+9qaW5QhXjsjb +Nl85CmXgxPmGROcgLQCO+omfrjf9UXrituU9Dz6auym5lDGEdMFnkzfr+wpasEy9 +mf5ZZOhDAkEA5HjXfVGaCtpydOt6hDon/uZsyssCK2lQ7NSuE3vP+sUsYMzIpEoy +t3VWXqKbo+g9KNDTP4WEliqp1aiSIylzzQJANPeqzihQnlgEdD4MdD4rwhFJwVIp +Le8Lcais1KaN7StzOwxB/XhgSibd2TbnPpw+3bSg5n5lvUdo+e62/31OHwJAU1jS +I+F09KikQIr28u3UUWT2IzTT4cpVv1AHAQyV3sG3YsjSGT0IK20eyP9BEBZU2WL0 +7aNjrvR5aHxKc5FXsQJABsFtyGpgI5X4xufkJZVZ+Mklz2n7iXa+XPatMAHFxAtb +EEMt60rngwMjXAzBSC6OYuYogRRAY3UCacNC5VhLYQ== +-----END RSA PRIVATE KEY----- diff --git a/Godeps/_workspace/src/github.com/lib/pq/certs/root.crt b/Godeps/_workspace/src/github.com/lib/pq/certs/root.crt new file mode 100644 index 0000000..aecf8f6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/certs/root.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEAzCCAuugAwIBAgIJANmheROCdW1NMA0GCSqGSIb3DQEBBQUAMF4xCzAJBgNV +BAYTAlVTMQ8wDQYDVQQIEwZOZXZhZGExEjAQBgNVBAcTCUxhcyBWZWdhczEaMBgG +A1UEChMRZ2l0aHViLmNvbS9saWIvcHExDjAMBgNVBAMTBXBxIENBMB4XDTE0MTAx +MTE1MDQyOVoXDTI0MTAwODE1MDQyOVowXjELMAkGA1UEBhMCVVMxDzANBgNVBAgT +Bk5ldmFkYTESMBAGA1UEBxMJTGFzIFZlZ2FzMRowGAYDVQQKExFnaXRodWIuY29t +L2xpYi9wcTEOMAwGA1UEAxMFcHEgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCV4PxP7ShzWBzUCThcKk3qZtOLtHmszQVtbqhvgTpm1kTRtKBdVMu0 +pLAHQ3JgJCnAYgH0iZxVGoMP16T3irdgsdC48+nNTFM2T0cCdkfDURGIhSFN47cb +Pgy306BcDUD2q7ucW33+dlFSRuGVewocoh4BWM/vMtMvvWzdi4Ag/L/jhb+5wZxZ +sWymsadOVSDePEMKOvlCa3EdVwVFV40TVyDb+iWBUivDAYsS2a3KajuJrO6MbZiE +Sp2RCIkZS2zFmzWxVRi9ZhzIZhh7EVF9JAaNC3T52jhGUdlRq3YpBTMnd89iOh74 +6jWXG7wSuPj3haFzyNhmJ0ZUh+2Ynoh1AgMBAAGjgcMwgcAwHQYDVR0OBBYEFFKT +7R52Cp9lT94ZZsHVIkA1y6ByMIGQBgNVHSMEgYgwgYWAFFKT7R52Cp9lT94ZZsHV +IkA1y6ByoWKkYDBeMQswCQYDVQQGEwJVUzEPMA0GA1UECBMGTmV2YWRhMRIwEAYD +VQQHEwlMYXMgVmVnYXMxGjAYBgNVBAoTEWdpdGh1Yi5jb20vbGliL3BxMQ4wDAYD +VQQDEwVwcSBDQYIJANmheROCdW1NMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF +BQADggEBAAEhCLWkqJNMI8b4gkbmj5fqQ/4+oO83bZ3w2Oqf6eZ8I8BC4f2NOyE6 +tRUlq5+aU7eqC1cOAvGjO+YHN/bF/DFpwLlzvUSXt+JP/pYcUjL7v+pIvwqec9hD +ndvM4iIbkD/H/OYQ3L+N3W+G1x7AcFIX+bGCb3PzYVQAjxreV6//wgKBosMGFbZo +HPxT9RPMun61SViF04H5TNs0derVn1+5eiiYENeAhJzQNyZoOOUuX1X/Inx9bEPh +C5vFBtSMgIytPgieRJVWAiMLYsfpIAStrHztRAbBs2DU01LmMgRvHdxgFEKinC/d +UHZZQDP+6pT+zADrGhQGXe4eThaO6f0= +-----END CERTIFICATE----- diff --git a/Godeps/_workspace/src/github.com/lib/pq/certs/server.crt b/Godeps/_workspace/src/github.com/lib/pq/certs/server.crt new file mode 100644 index 0000000..ddc995a --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/certs/server.crt @@ -0,0 +1,81 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=Nevada, L=Las Vegas, O=github.com/lib/pq, CN=pq CA + Validity + Not Before: Oct 11 15:05:15 2014 GMT + Not After : Oct 8 15:05:15 2024 GMT + Subject: C=US, ST=Nevada, L=Las Vegas, O=github.com/lib/pq, CN=postgres + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (2048 bit) + Modulus (2048 bit): + 00:d7:8a:4c:85:fb:17:a5:3c:8f:e0:72:11:29:ce: + 3f:b0:1f:3f:7d:c6:ee:7f:a7:fc:02:2b:35:47:08: + a6:3d:90:df:5c:56:14:94:00:c7:6d:d1:d2:e2:61: + 95:77:b8:e3:a6:66:31:f9:1f:21:7d:62:e1:27:da: + 94:37:61:4a:ea:63:53:a0:61:b8:9c:bb:a5:e2:e7: + b7:a6:d8:0f:05:04:c7:29:e2:ea:49:2b:7f:de:15: + 00:a6:18:70:50:c7:0c:de:9a:f9:5a:96:b0:e1:94: + 06:c6:6d:4a:21:3b:b4:0f:a5:6d:92:86:34:b2:4e: + d7:0e:a7:19:c0:77:0b:7b:87:c8:92:de:42:ff:86: + d2:b7:9a:a4:d4:15:23:ca:ad:a5:69:21:b8:ce:7e: + 66:cb:85:5d:b9:ed:8b:2d:09:8d:94:e4:04:1e:72: + ec:ef:d0:76:90:15:5a:a4:f7:91:4b:e9:ce:4e:9d: + 5d:9a:70:17:9c:d8:e9:73:83:ea:3d:61:99:a6:cd: + ac:91:40:5a:88:77:e5:4e:2a:8e:3d:13:f3:f9:38: + 6f:81:6b:8a:95:ca:0e:07:ab:6f:da:b4:8c:d9:ff: + aa:78:03:aa:c7:c2:cf:6f:64:92:d3:d8:83:d5:af: + f1:23:18:a7:2e:7b:17:0b:e7:7d:f1:fa:a8:41:a3: + 04:57 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + EE:F0:B3:46:DC:C7:09:EB:0E:B6:2F:E5:FE:62:60:45:44:9F:59:CC + X509v3 Authority Key Identifier: + keyid:52:93:ED:1E:76:0A:9F:65:4F:DE:19:66:C1:D5:22:40:35:CB:A0:72 + + X509v3 Basic Constraints: + CA:FALSE + X509v3 Key Usage: + Digital Signature, Non Repudiation, Key Encipherment + Signature Algorithm: sha256WithRSAEncryption + 7e:5a:6e:be:bf:d2:6c:c1:d6:fa:b6:fb:3f:06:53:36:08:87: + 9d:95:b1:39:af:9e:f6:47:38:17:39:da:25:7c:f2:ad:0c:e3: + ab:74:19:ca:fb:8c:a0:50:c0:1d:19:8a:9c:21:ed:0f:3a:d1: + 96:54:2e:10:09:4f:b8:70:f7:2b:99:43:d2:c6:15:bc:3f:24: + 7d:28:39:32:3f:8d:a4:4f:40:75:7f:3e:0d:1c:d1:69:f2:4e: + 98:83:47:97:d2:25:ac:c9:36:86:2f:04:a6:c4:86:c7:c4:00: + 5f:7f:b9:ad:fc:bf:e9:f5:78:d7:82:1a:51:0d:fc:ab:9e:92: + 1d:5f:0c:18:d1:82:e0:14:c9:ce:91:89:71:ff:49:49:ff:35: + bf:7b:44:78:42:c1:d0:66:65:bb:28:2e:60:ca:9b:20:12:a9: + 90:61:b1:96:ec:15:46:c9:37:f7:07:90:8a:89:45:2a:3f:37: + ec:dc:e3:e5:8f:c3:3a:57:80:a5:54:60:0c:e1:b2:26:99:2b: + 40:7e:36:d1:9a:70:02:ec:63:f4:3b:72:ae:81:fb:30:20:6d: + cb:48:46:c6:b5:8f:39:b1:84:05:25:55:8d:f5:62:f6:1b:46: + 2e:da:a3:4c:26:12:44:d7:56:b6:b8:a9:ca:d3:ab:71:45:7c: + 9f:48:6d:1e +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIBATANBgkqhkiG9w0BAQsFADBeMQswCQYDVQQGEwJVUzEP +MA0GA1UECBMGTmV2YWRhMRIwEAYDVQQHEwlMYXMgVmVnYXMxGjAYBgNVBAoTEWdp +dGh1Yi5jb20vbGliL3BxMQ4wDAYDVQQDEwVwcSBDQTAeFw0xNDEwMTExNTA1MTVa +Fw0yNDEwMDgxNTA1MTVaMGExCzAJBgNVBAYTAlVTMQ8wDQYDVQQIEwZOZXZhZGEx +EjAQBgNVBAcTCUxhcyBWZWdhczEaMBgGA1UEChMRZ2l0aHViLmNvbS9saWIvcHEx +ETAPBgNVBAMTCHBvc3RncmVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA14pMhfsXpTyP4HIRKc4/sB8/fcbuf6f8Ais1RwimPZDfXFYUlADHbdHS4mGV +d7jjpmYx+R8hfWLhJ9qUN2FK6mNToGG4nLul4ue3ptgPBQTHKeLqSSt/3hUAphhw +UMcM3pr5Wpaw4ZQGxm1KITu0D6VtkoY0sk7XDqcZwHcLe4fIkt5C/4bSt5qk1BUj +yq2laSG4zn5my4Vdue2LLQmNlOQEHnLs79B2kBVapPeRS+nOTp1dmnAXnNjpc4Pq +PWGZps2skUBaiHflTiqOPRPz+ThvgWuKlcoOB6tv2rSM2f+qeAOqx8LPb2SS09iD +1a/xIxinLnsXC+d98fqoQaMEVwIDAQABo1owWDAdBgNVHQ4EFgQU7vCzRtzHCesO +ti/l/mJgRUSfWcwwHwYDVR0jBBgwFoAUUpPtHnYKn2VP3hlmwdUiQDXLoHIwCQYD +VR0TBAIwADALBgNVHQ8EBAMCBeAwDQYJKoZIhvcNAQELBQADggEBAH5abr6/0mzB +1vq2+z8GUzYIh52VsTmvnvZHOBc52iV88q0M46t0Gcr7jKBQwB0Zipwh7Q860ZZU +LhAJT7hw9yuZQ9LGFbw/JH0oOTI/jaRPQHV/Pg0c0WnyTpiDR5fSJazJNoYvBKbE +hsfEAF9/ua38v+n1eNeCGlEN/Kuekh1fDBjRguAUyc6RiXH/SUn/Nb97RHhCwdBm +ZbsoLmDKmyASqZBhsZbsFUbJN/cHkIqJRSo/N+zc4+WPwzpXgKVUYAzhsiaZK0B+ +NtGacALsY/Q7cq6B+zAgbctIRsa1jzmxhAUlVY31YvYbRi7ao0wmEkTXVra4qcrT +q3FFfJ9IbR4= +-----END CERTIFICATE----- diff --git a/Godeps/_workspace/src/github.com/lib/pq/certs/server.key b/Godeps/_workspace/src/github.com/lib/pq/certs/server.key new file mode 100644 index 0000000..bd7b019 --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/certs/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEA14pMhfsXpTyP4HIRKc4/sB8/fcbuf6f8Ais1RwimPZDfXFYU +lADHbdHS4mGVd7jjpmYx+R8hfWLhJ9qUN2FK6mNToGG4nLul4ue3ptgPBQTHKeLq +SSt/3hUAphhwUMcM3pr5Wpaw4ZQGxm1KITu0D6VtkoY0sk7XDqcZwHcLe4fIkt5C +/4bSt5qk1BUjyq2laSG4zn5my4Vdue2LLQmNlOQEHnLs79B2kBVapPeRS+nOTp1d +mnAXnNjpc4PqPWGZps2skUBaiHflTiqOPRPz+ThvgWuKlcoOB6tv2rSM2f+qeAOq +x8LPb2SS09iD1a/xIxinLnsXC+d98fqoQaMEVwIDAQABAoIBAF3ZoihUhJ82F4+r +Gz4QyDpv4L1reT2sb1aiabhcU8ZK5nbWJG+tRyjSS/i2dNaEcttpdCj9HR/zhgZM +bm0OuAgG58rVwgS80CZUruq++Qs+YVojq8/gWPTiQD4SNhV2Fmx3HkwLgUk3oxuT +SsvdqzGE3okGVrutCIcgy126eA147VPMoej1Bb3fO6npqK0pFPhZfAc0YoqJuM+k +obRm5pAnGUipyLCFXjA9HYPKwYZw2RtfdA3CiImHeanSdqS+ctrC9y8BV40Th7gZ +haXdKUNdjmIxV695QQ1mkGqpKLZFqhzKioGQ2/Ly2d1iaKN9fZltTusu8unepWJ2 +tlT9qMECgYEA9uHaF1t2CqE+AJvWTihHhPIIuLxoOQXYea1qvxfcH/UMtaLKzCNm +lQ5pqCGsPvp+10f36yttO1ZehIvlVNXuJsjt0zJmPtIolNuJY76yeussfQ9jHheB +5uPEzCFlHzxYbBUyqgWaF6W74okRGzEGJXjYSP0yHPPdU4ep2q3bGiUCgYEA34Af +wBSuQSK7uLxArWHvQhyuvi43ZGXls6oRGl+Ysj54s8BP6XGkq9hEJ6G4yxgyV+BR +DUOs5X8/TLT8POuIMYvKTQthQyCk0eLv2FLdESDuuKx0kBVY3s8lK3/z5HhrdOiN +VMNZU+xDKgKc3hN9ypkk8vcZe6EtH7Y14e0rVcsCgYBTgxi8F/M5K0wG9rAqphNz +VFBA9XKn/2M33cKjO5X5tXIEKzpAjaUQvNxexG04rJGljzG8+mar0M6ONahw5yD1 +O7i/XWgazgpuOEkkVYiYbd8RutfDgR4vFVMn3hAP3eDnRtBplRWH9Ec3HTiNIys6 +F8PKBOQjyRZQQC7jyzW3hQKBgACe5HeuFwXLSOYsb6mLmhR+6+VPT4wR1F95W27N +USk9jyxAnngxfpmTkiziABdgS9N+pfr5cyN4BP77ia/Jn6kzkC5Cl9SN5KdIkA3z +vPVtN/x/ThuQU5zaymmig1ThGLtMYggYOslG4LDfLPxY5YKIhle+Y+259twdr2yf +Mf2dAoGAaGv3tWMgnIdGRk6EQL/yb9PKHo7ShN+tKNlGaK7WwzBdKs+Fe8jkgcr7 +pz4Ne887CmxejdISzOCcdT+Zm9Bx6I/uZwWOtDvWpIgIxVX9a9URj/+D1MxTE/y4 +d6H+c89yDY62I2+drMpdjCd3EtCaTlxpTbRS+s1eAHMH7aEkcCE= +-----END RSA PRIVATE KEY----- diff --git a/Godeps/_workspace/src/github.com/lib/pq/conn.go b/Godeps/_workspace/src/github.com/lib/pq/conn.go new file mode 100644 index 0000000..ce661d6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/conn.go @@ -0,0 +1,1775 @@ +package pq + +import ( + "bufio" + "crypto/md5" + "crypto/tls" + "crypto/x509" + "database/sql" + "database/sql/driver" + "encoding/binary" + "errors" + "fmt" + "io" + "io/ioutil" + "net" + "os" + "os/user" + "path" + "path/filepath" + "strconv" + "strings" + "time" + "unicode" + + "github.com/lib/pq/oid" +) + +// Common error types +var ( + ErrNotSupported = errors.New("pq: Unsupported command") + ErrInFailedTransaction = errors.New("pq: Could not complete operation in a failed transaction") + ErrSSLNotSupported = errors.New("pq: SSL is not enabled on the server") + ErrSSLKeyHasWorldPermissions = errors.New("pq: Private key file has group or world access. Permissions should be u=rw (0600) or less.") + ErrCouldNotDetectUsername = errors.New("pq: Could not detect default username. Please provide one explicitly.") +) + +type drv struct{} + +func (d *drv) Open(name string) (driver.Conn, error) { + return Open(name) +} + +func init() { + sql.Register("postgres", &drv{}) +} + +type parameterStatus struct { + // server version in the same format as server_version_num, or 0 if + // unavailable + serverVersion int + + // the current location based on the TimeZone value of the session, if + // available + currentLocation *time.Location +} + +type transactionStatus byte + +const ( + txnStatusIdle transactionStatus = 'I' + txnStatusIdleInTransaction transactionStatus = 'T' + txnStatusInFailedTransaction transactionStatus = 'E' +) + +func (s transactionStatus) String() string { + switch s { + case txnStatusIdle: + return "idle" + case txnStatusIdleInTransaction: + return "idle in transaction" + case txnStatusInFailedTransaction: + return "in a failed transaction" + default: + errorf("unknown transactionStatus %d", s) + } + + panic("not reached") +} + +type Dialer interface { + Dial(network, address string) (net.Conn, error) + DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) +} + +type defaultDialer struct{} + +func (d defaultDialer) Dial(ntw, addr string) (net.Conn, error) { + return net.Dial(ntw, addr) +} +func (d defaultDialer) DialTimeout(ntw, addr string, timeout time.Duration) (net.Conn, error) { + return net.DialTimeout(ntw, addr, timeout) +} + +type conn struct { + c net.Conn + buf *bufio.Reader + namei int + scratch [512]byte + txnStatus transactionStatus + + parameterStatus parameterStatus + + saveMessageType byte + saveMessageBuffer []byte + + // If true, this connection is bad and all public-facing functions should + // return ErrBadConn. + bad bool + + // If set, this connection should never use the binary format when + // receiving query results from prepared statements. Only provided for + // debugging. + disablePreparedBinaryResult bool + + // Whether to always send []byte parameters over as binary. Enables single + // round-trip mode for non-prepared Query calls. + binaryParameters bool +} + +// Handle driver-side settings in parsed connection string. +func (c *conn) handleDriverSettings(o values) (err error) { + boolSetting := func(key string, val *bool) error { + if value := o.Get(key); value != "" { + if value == "yes" { + *val = true + } else if value == "no" { + *val = false + } else { + return fmt.Errorf("unrecognized value %q for %s", value, key) + } + } + return nil + } + + err = boolSetting("disable_prepared_binary_result", &c.disablePreparedBinaryResult) + if err != nil { + return err + } + err = boolSetting("binary_parameters", &c.binaryParameters) + if err != nil { + return err + } + return nil +} + +func (c *conn) writeBuf(b byte) *writeBuf { + c.scratch[0] = b + return &writeBuf{ + buf: c.scratch[:5], + pos: 1, + } +} + +func Open(name string) (_ driver.Conn, err error) { + return DialOpen(defaultDialer{}, name) +} + +func DialOpen(d Dialer, name string) (_ driver.Conn, err error) { + // Handle any panics during connection initialization. Note that we + // specifically do *not* want to use errRecover(), as that would turn any + // connection errors into ErrBadConns, hiding the real error message from + // the user. + defer errRecoverNoErrBadConn(&err) + + o := make(values) + + // A number of defaults are applied here, in this order: + // + // * Very low precedence defaults applied in every situation + // * Environment variables + // * Explicitly passed connection information + o.Set("host", "localhost") + o.Set("port", "5432") + // N.B.: Extra float digits should be set to 3, but that breaks + // Postgres 8.4 and older, where the max is 2. + o.Set("extra_float_digits", "2") + for k, v := range parseEnviron(os.Environ()) { + o.Set(k, v) + } + + if strings.HasPrefix(name, "postgres://") || strings.HasPrefix(name, "postgresql://") { + name, err = ParseURL(name) + if err != nil { + return nil, err + } + } + + if err := parseOpts(name, o); err != nil { + return nil, err + } + + // Use the "fallback" application name if necessary + if fallback := o.Get("fallback_application_name"); fallback != "" { + if !o.Isset("application_name") { + o.Set("application_name", fallback) + } + } + + // We can't work with any client_encoding other than UTF-8 currently. + // However, we have historically allowed the user to set it to UTF-8 + // explicitly, and there's no reason to break such programs, so allow that. + // Note that the "options" setting could also set client_encoding, but + // parsing its value is not worth it. Instead, we always explicitly send + // client_encoding as a separate run-time parameter, which should override + // anything set in options. + if enc := o.Get("client_encoding"); enc != "" && !isUTF8(enc) { + return nil, errors.New("client_encoding must be absent or 'UTF8'") + } + o.Set("client_encoding", "UTF8") + // DateStyle needs a similar treatment. + if datestyle := o.Get("datestyle"); datestyle != "" { + if datestyle != "ISO, MDY" { + panic(fmt.Sprintf("setting datestyle must be absent or %v; got %v", + "ISO, MDY", datestyle)) + } + } else { + o.Set("datestyle", "ISO, MDY") + } + + // If a user is not provided by any other means, the last + // resort is to use the current operating system provided user + // name. + if o.Get("user") == "" { + u, err := userCurrent() + if err != nil { + return nil, err + } else { + o.Set("user", u) + } + } + + cn := &conn{} + err = cn.handleDriverSettings(o) + if err != nil { + return nil, err + } + + cn.c, err = dial(d, o) + if err != nil { + return nil, err + } + cn.ssl(o) + cn.buf = bufio.NewReader(cn.c) + cn.startup(o) + + // reset the deadline, in case one was set (see dial) + if timeout := o.Get("connect_timeout"); timeout != "" && timeout != "0" { + err = cn.c.SetDeadline(time.Time{}) + } + return cn, err +} + +func dial(d Dialer, o values) (net.Conn, error) { + ntw, addr := network(o) + // SSL is not necessary or supported over UNIX domain sockets + if ntw == "unix" { + o["sslmode"] = "disable" + } + + // Zero or not specified means wait indefinitely. + if timeout := o.Get("connect_timeout"); timeout != "" && timeout != "0" { + seconds, err := strconv.ParseInt(timeout, 10, 0) + if err != nil { + return nil, fmt.Errorf("invalid value for parameter connect_timeout: %s", err) + } + duration := time.Duration(seconds) * time.Second + // connect_timeout should apply to the entire connection establishment + // procedure, so we both use a timeout for the TCP connection + // establishment and set a deadline for doing the initial handshake. + // The deadline is then reset after startup() is done. + deadline := time.Now().Add(duration) + conn, err := d.DialTimeout(ntw, addr, duration) + if err != nil { + return nil, err + } + err = conn.SetDeadline(deadline) + return conn, err + } + return d.Dial(ntw, addr) +} + +func network(o values) (string, string) { + host := o.Get("host") + + if strings.HasPrefix(host, "/") { + sockPath := path.Join(host, ".s.PGSQL."+o.Get("port")) + return "unix", sockPath + } + + return "tcp", host + ":" + o.Get("port") +} + +type values map[string]string + +func (vs values) Set(k, v string) { + vs[k] = v +} + +func (vs values) Get(k string) (v string) { + return vs[k] +} + +func (vs values) Isset(k string) bool { + _, ok := vs[k] + return ok +} + +// scanner implements a tokenizer for libpq-style option strings. +type scanner struct { + s []rune + i int +} + +// newScanner returns a new scanner initialized with the option string s. +func newScanner(s string) *scanner { + return &scanner{[]rune(s), 0} +} + +// Next returns the next rune. +// It returns 0, false if the end of the text has been reached. +func (s *scanner) Next() (rune, bool) { + if s.i >= len(s.s) { + return 0, false + } + r := s.s[s.i] + s.i++ + return r, true +} + +// SkipSpaces returns the next non-whitespace rune. +// It returns 0, false if the end of the text has been reached. +func (s *scanner) SkipSpaces() (rune, bool) { + r, ok := s.Next() + for unicode.IsSpace(r) && ok { + r, ok = s.Next() + } + return r, ok +} + +// parseOpts parses the options from name and adds them to the values. +// +// The parsing code is based on conninfo_parse from libpq's fe-connect.c +func parseOpts(name string, o values) error { + s := newScanner(name) + + for { + var ( + keyRunes, valRunes []rune + r rune + ok bool + ) + + if r, ok = s.SkipSpaces(); !ok { + break + } + + // Scan the key + for !unicode.IsSpace(r) && r != '=' { + keyRunes = append(keyRunes, r) + if r, ok = s.Next(); !ok { + break + } + } + + // Skip any whitespace if we're not at the = yet + if r != '=' { + r, ok = s.SkipSpaces() + } + + // The current character should be = + if r != '=' || !ok { + return fmt.Errorf(`missing "=" after %q in connection info string"`, string(keyRunes)) + } + + // Skip any whitespace after the = + if r, ok = s.SkipSpaces(); !ok { + // If we reach the end here, the last value is just an empty string as per libpq. + o.Set(string(keyRunes), "") + break + } + + if r != '\'' { + for !unicode.IsSpace(r) { + if r == '\\' { + if r, ok = s.Next(); !ok { + return fmt.Errorf(`missing character after backslash`) + } + } + valRunes = append(valRunes, r) + + if r, ok = s.Next(); !ok { + break + } + } + } else { + quote: + for { + if r, ok = s.Next(); !ok { + return fmt.Errorf(`unterminated quoted string literal in connection string`) + } + switch r { + case '\'': + break quote + case '\\': + r, _ = s.Next() + fallthrough + default: + valRunes = append(valRunes, r) + } + } + } + + o.Set(string(keyRunes), string(valRunes)) + } + + return nil +} + +func (cn *conn) isInTransaction() bool { + return cn.txnStatus == txnStatusIdleInTransaction || + cn.txnStatus == txnStatusInFailedTransaction +} + +func (cn *conn) checkIsInTransaction(intxn bool) { + if cn.isInTransaction() != intxn { + cn.bad = true + errorf("unexpected transaction status %v", cn.txnStatus) + } +} + +func (cn *conn) Begin() (_ driver.Tx, err error) { + if cn.bad { + return nil, driver.ErrBadConn + } + defer cn.errRecover(&err) + + cn.checkIsInTransaction(false) + _, commandTag, err := cn.simpleExec("BEGIN") + if err != nil { + return nil, err + } + if commandTag != "BEGIN" { + cn.bad = true + return nil, fmt.Errorf("unexpected command tag %s", commandTag) + } + if cn.txnStatus != txnStatusIdleInTransaction { + cn.bad = true + return nil, fmt.Errorf("unexpected transaction status %v", cn.txnStatus) + } + return cn, nil +} + +func (cn *conn) Commit() (err error) { + if cn.bad { + return driver.ErrBadConn + } + defer cn.errRecover(&err) + + cn.checkIsInTransaction(true) + // We don't want the client to think that everything is okay if it tries + // to commit a failed transaction. However, no matter what we return, + // database/sql will release this connection back into the free connection + // pool so we have to abort the current transaction here. Note that you + // would get the same behaviour if you issued a COMMIT in a failed + // transaction, so it's also the least surprising thing to do here. + if cn.txnStatus == txnStatusInFailedTransaction { + if err := cn.Rollback(); err != nil { + return err + } + return ErrInFailedTransaction + } + + _, commandTag, err := cn.simpleExec("COMMIT") + if err != nil { + if cn.isInTransaction() { + cn.bad = true + } + return err + } + if commandTag != "COMMIT" { + cn.bad = true + return fmt.Errorf("unexpected command tag %s", commandTag) + } + cn.checkIsInTransaction(false) + return nil +} + +func (cn *conn) Rollback() (err error) { + if cn.bad { + return driver.ErrBadConn + } + defer cn.errRecover(&err) + + cn.checkIsInTransaction(true) + _, commandTag, err := cn.simpleExec("ROLLBACK") + if err != nil { + if cn.isInTransaction() { + cn.bad = true + } + return err + } + if commandTag != "ROLLBACK" { + return fmt.Errorf("unexpected command tag %s", commandTag) + } + cn.checkIsInTransaction(false) + return nil +} + +func (cn *conn) gname() string { + cn.namei++ + return strconv.FormatInt(int64(cn.namei), 10) +} + +func (cn *conn) simpleExec(q string) (res driver.Result, commandTag string, err error) { + b := cn.writeBuf('Q') + b.string(q) + cn.send(b) + + for { + t, r := cn.recv1() + switch t { + case 'C': + res, commandTag = cn.parseComplete(r.string()) + case 'Z': + cn.processReadyForQuery(r) + // done + return + case 'E': + err = parseError(r) + case 'T', 'D', 'I': + // ignore any results + default: + cn.bad = true + errorf("unknown response for simple query: %q", t) + } + } +} + +func (cn *conn) simpleQuery(q string) (res *rows, err error) { + defer cn.errRecover(&err) + + st := &stmt{cn: cn, name: ""} + + b := cn.writeBuf('Q') + b.string(q) + cn.send(b) + + for { + t, r := cn.recv1() + switch t { + case 'C', 'I': + // We allow queries which don't return any results through Query as + // well as Exec. We still have to give database/sql a rows object + // the user can close, though, to avoid connections from being + // leaked. A "rows" with done=true works fine for that purpose. + if err != nil { + cn.bad = true + errorf("unexpected message %q in simple query execution", t) + } + res = &rows{ + cn: cn, + colNames: st.colNames, + colTyps: st.colTyps, + colFmts: st.colFmts, + done: true, + } + case 'Z': + cn.processReadyForQuery(r) + // done + return + case 'E': + res = nil + err = parseError(r) + case 'D': + if res == nil { + cn.bad = true + errorf("unexpected DataRow in simple query execution") + } + // the query didn't fail; kick off to Next + cn.saveMessage(t, r) + return + case 'T': + // res might be non-nil here if we received a previous + // CommandComplete, but that's fine; just overwrite it + res = &rows{cn: cn} + res.colNames, res.colFmts, res.colTyps = parsePortalRowDescribe(r) + + // To work around a bug in QueryRow in Go 1.2 and earlier, wait + // until the first DataRow has been received. + default: + cn.bad = true + errorf("unknown response for simple query: %q", t) + } + } +} + +// Decides which column formats to use for a prepared statement. The input is +// an array of type oids, one element per result column. +func decideColumnFormats(colTyps []oid.Oid, forceText bool) (colFmts []format, colFmtData []byte) { + if len(colTyps) == 0 { + return nil, colFmtDataAllText + } + + colFmts = make([]format, len(colTyps)) + if forceText { + return colFmts, colFmtDataAllText + } + + allBinary := true + allText := true + for i, o := range colTyps { + switch o { + // This is the list of types to use binary mode for when receiving them + // through a prepared statement. If a type appears in this list, it + // must also be implemented in binaryDecode in encode.go. + case oid.T_bytea: + fallthrough + case oid.T_int8: + fallthrough + case oid.T_int4: + fallthrough + case oid.T_int2: + colFmts[i] = formatBinary + allText = false + + default: + allBinary = false + } + } + + if allBinary { + return colFmts, colFmtDataAllBinary + } else if allText { + return colFmts, colFmtDataAllText + } else { + colFmtData = make([]byte, 2+len(colFmts)*2) + binary.BigEndian.PutUint16(colFmtData, uint16(len(colFmts))) + for i, v := range colFmts { + binary.BigEndian.PutUint16(colFmtData[2+i*2:], uint16(v)) + } + return colFmts, colFmtData + } +} + +func (cn *conn) prepareTo(q, stmtName string) *stmt { + st := &stmt{cn: cn, name: stmtName} + + b := cn.writeBuf('P') + b.string(st.name) + b.string(q) + b.int16(0) + + b.next('D') + b.byte('S') + b.string(st.name) + + b.next('S') + cn.send(b) + + cn.readParseResponse() + st.paramTyps, st.colNames, st.colTyps = cn.readStatementDescribeResponse() + st.colFmts, st.colFmtData = decideColumnFormats(st.colTyps, cn.disablePreparedBinaryResult) + cn.readReadyForQuery() + return st +} + +func (cn *conn) Prepare(q string) (_ driver.Stmt, err error) { + if cn.bad { + return nil, driver.ErrBadConn + } + defer cn.errRecover(&err) + + if len(q) >= 4 && strings.EqualFold(q[:4], "COPY") { + return cn.prepareCopyIn(q) + } + return cn.prepareTo(q, cn.gname()), nil +} + +func (cn *conn) Close() (err error) { + if cn.bad { + return driver.ErrBadConn + } + defer cn.errRecover(&err) + + // Don't go through send(); ListenerConn relies on us not scribbling on the + // scratch buffer of this connection. + err = cn.sendSimpleMessage('X') + if err != nil { + return err + } + + return cn.c.Close() +} + +// Implement the "Queryer" interface +func (cn *conn) Query(query string, args []driver.Value) (_ driver.Rows, err error) { + if cn.bad { + return nil, driver.ErrBadConn + } + defer cn.errRecover(&err) + + // Check to see if we can use the "simpleQuery" interface, which is + // *much* faster than going through prepare/exec + if len(args) == 0 { + return cn.simpleQuery(query) + } + + if cn.binaryParameters { + cn.sendBinaryModeQuery(query, args) + + cn.readParseResponse() + cn.readBindResponse() + rows := &rows{cn: cn} + rows.colNames, rows.colFmts, rows.colTyps = cn.readPortalDescribeResponse() + cn.postExecuteWorkaround() + return rows, nil + } else { + st := cn.prepareTo(query, "") + st.exec(args) + return &rows{ + cn: cn, + colNames: st.colNames, + colTyps: st.colTyps, + colFmts: st.colFmts, + }, nil + } +} + +// Implement the optional "Execer" interface for one-shot queries +func (cn *conn) Exec(query string, args []driver.Value) (res driver.Result, err error) { + if cn.bad { + return nil, driver.ErrBadConn + } + defer cn.errRecover(&err) + + // Check to see if we can use the "simpleExec" interface, which is + // *much* faster than going through prepare/exec + if len(args) == 0 { + // ignore commandTag, our caller doesn't care + r, _, err := cn.simpleExec(query) + return r, err + } + + if cn.binaryParameters { + cn.sendBinaryModeQuery(query, args) + + cn.readParseResponse() + cn.readBindResponse() + cn.readPortalDescribeResponse() + cn.postExecuteWorkaround() + res, _, err = cn.readExecuteResponse("Execute") + return res, err + } else { + // Use the unnamed statement to defer planning until bind + // time, or else value-based selectivity estimates cannot be + // used. + st := cn.prepareTo(query, "") + r, err := st.Exec(args) + if err != nil { + panic(err) + } + return r, err + } +} + +func (cn *conn) send(m *writeBuf) { + _, err := cn.c.Write(m.wrap()) + if err != nil { + panic(err) + } +} + +func (cn *conn) sendStartupPacket(m *writeBuf) { + // sanity check + if m.buf[0] != 0 { + panic("oops") + } + + _, err := cn.c.Write((m.wrap())[1:]) + if err != nil { + panic(err) + } +} + +// Send a message of type typ to the server on the other end of cn. The +// message should have no payload. This method does not use the scratch +// buffer. +func (cn *conn) sendSimpleMessage(typ byte) (err error) { + _, err = cn.c.Write([]byte{typ, '\x00', '\x00', '\x00', '\x04'}) + return err +} + +// saveMessage memorizes a message and its buffer in the conn struct. +// recvMessage will then return these values on the next call to it. This +// method is useful in cases where you have to see what the next message is +// going to be (e.g. to see whether it's an error or not) but you can't handle +// the message yourself. +func (cn *conn) saveMessage(typ byte, buf *readBuf) { + if cn.saveMessageType != 0 { + cn.bad = true + errorf("unexpected saveMessageType %d", cn.saveMessageType) + } + cn.saveMessageType = typ + cn.saveMessageBuffer = *buf +} + +// recvMessage receives any message from the backend, or returns an error if +// a problem occurred while reading the message. +func (cn *conn) recvMessage(r *readBuf) (byte, error) { + // workaround for a QueryRow bug, see exec + if cn.saveMessageType != 0 { + t := cn.saveMessageType + *r = cn.saveMessageBuffer + cn.saveMessageType = 0 + cn.saveMessageBuffer = nil + return t, nil + } + + x := cn.scratch[:5] + _, err := io.ReadFull(cn.buf, x) + if err != nil { + return 0, err + } + + // read the type and length of the message that follows + t := x[0] + n := int(binary.BigEndian.Uint32(x[1:])) - 4 + var y []byte + if n <= len(cn.scratch) { + y = cn.scratch[:n] + } else { + y = make([]byte, n) + } + _, err = io.ReadFull(cn.buf, y) + if err != nil { + return 0, err + } + *r = y + return t, nil +} + +// recv receives a message from the backend, but if an error happened while +// reading the message or the received message was an ErrorResponse, it panics. +// NoticeResponses are ignored. This function should generally be used only +// during the startup sequence. +func (cn *conn) recv() (t byte, r *readBuf) { + for { + var err error + r = &readBuf{} + t, err = cn.recvMessage(r) + if err != nil { + panic(err) + } + + switch t { + case 'E': + panic(parseError(r)) + case 'N': + // ignore + default: + return + } + } +} + +// recv1Buf is exactly equivalent to recv1, except it uses a buffer supplied by +// the caller to avoid an allocation. +func (cn *conn) recv1Buf(r *readBuf) byte { + for { + t, err := cn.recvMessage(r) + if err != nil { + panic(err) + } + + switch t { + case 'A', 'N': + // ignore + case 'S': + cn.processParameterStatus(r) + default: + return t + } + } +} + +// recv1 receives a message from the backend, panicking if an error occurs +// while attempting to read it. All asynchronous messages are ignored, with +// the exception of ErrorResponse. +func (cn *conn) recv1() (t byte, r *readBuf) { + r = &readBuf{} + t = cn.recv1Buf(r) + return t, r +} + +func (cn *conn) ssl(o values) { + verifyCaOnly := false + tlsConf := tls.Config{} + switch mode := o.Get("sslmode"); mode { + case "require", "": + tlsConf.InsecureSkipVerify = true + case "verify-ca": + // We must skip TLS's own verification since it requires full + // verification since Go 1.3. + tlsConf.InsecureSkipVerify = true + verifyCaOnly = true + case "verify-full": + tlsConf.ServerName = o.Get("host") + case "disable": + return + default: + errorf(`unsupported sslmode %q; only "require" (default), "verify-full", and "disable" supported`, mode) + } + + cn.setupSSLClientCertificates(&tlsConf, o) + cn.setupSSLCA(&tlsConf, o) + + w := cn.writeBuf(0) + w.int32(80877103) + cn.sendStartupPacket(w) + + b := cn.scratch[:1] + _, err := io.ReadFull(cn.c, b) + if err != nil { + panic(err) + } + + if b[0] != 'S' { + panic(ErrSSLNotSupported) + } + + client := tls.Client(cn.c, &tlsConf) + if verifyCaOnly { + cn.verifyCA(client, &tlsConf) + } + cn.c = client +} + +// verifyCA carries out a TLS handshake to the server and verifies the +// presented certificate against the effective CA, i.e. the one specified in +// sslrootcert or the system CA if sslrootcert was not specified. +func (cn *conn) verifyCA(client *tls.Conn, tlsConf *tls.Config) { + err := client.Handshake() + if err != nil { + panic(err) + } + certs := client.ConnectionState().PeerCertificates + opts := x509.VerifyOptions{ + DNSName: client.ConnectionState().ServerName, + Intermediates: x509.NewCertPool(), + Roots: tlsConf.RootCAs, + } + for i, cert := range certs { + if i == 0 { + continue + } + opts.Intermediates.AddCert(cert) + } + _, err = certs[0].Verify(opts) + if err != nil { + panic(err) + } +} + +// This function sets up SSL client certificates based on either the "sslkey" +// and "sslcert" settings (possibly set via the environment variables PGSSLKEY +// and PGSSLCERT, respectively), or if they aren't set, from the .postgresql +// directory in the user's home directory. If the file paths are set +// explicitly, the files must exist. The key file must also not be +// world-readable, or this function will panic with +// ErrSSLKeyHasWorldPermissions. +func (cn *conn) setupSSLClientCertificates(tlsConf *tls.Config, o values) { + var missingOk bool + + sslkey := o.Get("sslkey") + sslcert := o.Get("sslcert") + if sslkey != "" && sslcert != "" { + // If the user has set an sslkey and sslcert, they *must* exist. + missingOk = false + } else { + // Automatically load certificates from ~/.postgresql. + user, err := user.Current() + if err != nil { + // user.Current() might fail when cross-compiling. We have to + // ignore the error and continue without client certificates, since + // we wouldn't know where to load them from. + return + } + + sslkey = filepath.Join(user.HomeDir, ".postgresql", "postgresql.key") + sslcert = filepath.Join(user.HomeDir, ".postgresql", "postgresql.crt") + missingOk = true + } + + // Check that both files exist, and report the error or stop, depending on + // which behaviour we want. Note that we don't do any more extensive + // checks than this (such as checking that the paths aren't directories); + // LoadX509KeyPair() will take care of the rest. + keyfinfo, err := os.Stat(sslkey) + if err != nil && missingOk { + return + } else if err != nil { + panic(err) + } + _, err = os.Stat(sslcert) + if err != nil && missingOk { + return + } else if err != nil { + panic(err) + } + + // If we got this far, the key file must also have the correct permissions + kmode := keyfinfo.Mode() + if kmode != kmode&0600 { + panic(ErrSSLKeyHasWorldPermissions) + } + + cert, err := tls.LoadX509KeyPair(sslcert, sslkey) + if err != nil { + panic(err) + } + tlsConf.Certificates = []tls.Certificate{cert} +} + +// Sets up RootCAs in the TLS configuration if sslrootcert is set. +func (cn *conn) setupSSLCA(tlsConf *tls.Config, o values) { + if sslrootcert := o.Get("sslrootcert"); sslrootcert != "" { + tlsConf.RootCAs = x509.NewCertPool() + + cert, err := ioutil.ReadFile(sslrootcert) + if err != nil { + panic(err) + } + + ok := tlsConf.RootCAs.AppendCertsFromPEM(cert) + if !ok { + errorf("couldn't parse pem in sslrootcert") + } + } +} + +// isDriverSetting returns true iff a setting is purely for configuring the +// driver's options and should not be sent to the server in the connection +// startup packet. +func isDriverSetting(key string) bool { + switch key { + case "host", "port": + return true + case "password": + return true + case "sslmode", "sslcert", "sslkey", "sslrootcert": + return true + case "fallback_application_name": + return true + case "connect_timeout": + return true + case "disable_prepared_binary_result": + return true + case "binary_parameters": + return true + + default: + return false + } +} + +func (cn *conn) startup(o values) { + w := cn.writeBuf(0) + w.int32(196608) + // Send the backend the name of the database we want to connect to, and the + // user we want to connect as. Additionally, we send over any run-time + // parameters potentially included in the connection string. If the server + // doesn't recognize any of them, it will reply with an error. + for k, v := range o { + if isDriverSetting(k) { + // skip options which can't be run-time parameters + continue + } + // The protocol requires us to supply the database name as "database" + // instead of "dbname". + if k == "dbname" { + k = "database" + } + w.string(k) + w.string(v) + } + w.string("") + cn.sendStartupPacket(w) + + for { + t, r := cn.recv() + switch t { + case 'K': + case 'S': + cn.processParameterStatus(r) + case 'R': + cn.auth(r, o) + case 'Z': + cn.processReadyForQuery(r) + return + default: + errorf("unknown response for startup: %q", t) + } + } +} + +func (cn *conn) auth(r *readBuf, o values) { + switch code := r.int32(); code { + case 0: + // OK + case 3: + w := cn.writeBuf('p') + w.string(o.Get("password")) + cn.send(w) + + t, r := cn.recv() + if t != 'R' { + errorf("unexpected password response: %q", t) + } + + if r.int32() != 0 { + errorf("unexpected authentication response: %q", t) + } + case 5: + s := string(r.next(4)) + w := cn.writeBuf('p') + w.string("md5" + md5s(md5s(o.Get("password")+o.Get("user"))+s)) + cn.send(w) + + t, r := cn.recv() + if t != 'R' { + errorf("unexpected password response: %q", t) + } + + if r.int32() != 0 { + errorf("unexpected authentication response: %q", t) + } + default: + errorf("unknown authentication response: %d", code) + } +} + +type format int + +const formatText format = 0 +const formatBinary format = 1 + +// One result-column format code with the value 1 (i.e. all binary). +var colFmtDataAllBinary []byte = []byte{0, 1, 0, 1} + +// No result-column format codes (i.e. all text). +var colFmtDataAllText []byte = []byte{0, 0} + +type stmt struct { + cn *conn + name string + colNames []string + colFmts []format + colFmtData []byte + colTyps []oid.Oid + paramTyps []oid.Oid + closed bool +} + +func (st *stmt) Close() (err error) { + if st.closed { + return nil + } + if st.cn.bad { + return driver.ErrBadConn + } + defer st.cn.errRecover(&err) + + w := st.cn.writeBuf('C') + w.byte('S') + w.string(st.name) + st.cn.send(w) + + st.cn.send(st.cn.writeBuf('S')) + + t, _ := st.cn.recv1() + if t != '3' { + st.cn.bad = true + errorf("unexpected close response: %q", t) + } + st.closed = true + + t, r := st.cn.recv1() + if t != 'Z' { + st.cn.bad = true + errorf("expected ready for query, but got: %q", t) + } + st.cn.processReadyForQuery(r) + + return nil +} + +func (st *stmt) Query(v []driver.Value) (r driver.Rows, err error) { + if st.cn.bad { + return nil, driver.ErrBadConn + } + defer st.cn.errRecover(&err) + + st.exec(v) + return &rows{ + cn: st.cn, + colNames: st.colNames, + colTyps: st.colTyps, + colFmts: st.colFmts, + }, nil +} + +func (st *stmt) Exec(v []driver.Value) (res driver.Result, err error) { + if st.cn.bad { + return nil, driver.ErrBadConn + } + defer st.cn.errRecover(&err) + + st.exec(v) + res, _, err = st.cn.readExecuteResponse("simple query") + return res, err +} + +func (st *stmt) exec(v []driver.Value) { + if len(v) >= 65536 { + errorf("got %d parameters but PostgreSQL only supports 65535 parameters", len(v)) + } + if len(v) != len(st.paramTyps) { + errorf("got %d parameters but the statement requires %d", len(v), len(st.paramTyps)) + } + + cn := st.cn + w := cn.writeBuf('B') + w.byte(0) // unnamed portal + w.string(st.name) + + if cn.binaryParameters { + cn.sendBinaryParameters(w, v) + } else { + w.int16(0) + w.int16(len(v)) + for i, x := range v { + if x == nil { + w.int32(-1) + } else { + b := encode(&cn.parameterStatus, x, st.paramTyps[i]) + w.int32(len(b)) + w.bytes(b) + } + } + } + w.bytes(st.colFmtData) + + w.next('E') + w.byte(0) + w.int32(0) + + w.next('S') + cn.send(w) + + cn.readBindResponse() + cn.postExecuteWorkaround() + +} + +func (st *stmt) NumInput() int { + return len(st.paramTyps) +} + +// parseComplete parses the "command tag" from a CommandComplete message, and +// returns the number of rows affected (if applicable) and a string +// identifying only the command that was executed, e.g. "ALTER TABLE". If the +// command tag could not be parsed, parseComplete panics. +func (cn *conn) parseComplete(commandTag string) (driver.Result, string) { + commandsWithAffectedRows := []string{ + "SELECT ", + // INSERT is handled below + "UPDATE ", + "DELETE ", + "FETCH ", + "MOVE ", + "COPY ", + } + + var affectedRows *string + for _, tag := range commandsWithAffectedRows { + if strings.HasPrefix(commandTag, tag) { + t := commandTag[len(tag):] + affectedRows = &t + commandTag = tag[:len(tag)-1] + break + } + } + // INSERT also includes the oid of the inserted row in its command tag. + // Oids in user tables are deprecated, and the oid is only returned when + // exactly one row is inserted, so it's unlikely to be of value to any + // real-world application and we can ignore it. + if affectedRows == nil && strings.HasPrefix(commandTag, "INSERT ") { + parts := strings.Split(commandTag, " ") + if len(parts) != 3 { + cn.bad = true + errorf("unexpected INSERT command tag %s", commandTag) + } + affectedRows = &parts[len(parts)-1] + commandTag = "INSERT" + } + // There should be no affected rows attached to the tag, just return it + if affectedRows == nil { + return driver.RowsAffected(0), commandTag + } + n, err := strconv.ParseInt(*affectedRows, 10, 64) + if err != nil { + cn.bad = true + errorf("could not parse commandTag: %s", err) + } + return driver.RowsAffected(n), commandTag +} + +type rows struct { + cn *conn + colNames []string + colTyps []oid.Oid + colFmts []format + done bool + rb readBuf +} + +func (rs *rows) Close() error { + // no need to look at cn.bad as Next() will + for { + err := rs.Next(nil) + switch err { + case nil: + case io.EOF: + return nil + default: + return err + } + } +} + +func (rs *rows) Columns() []string { + return rs.colNames +} + +func (rs *rows) Next(dest []driver.Value) (err error) { + if rs.done { + return io.EOF + } + + conn := rs.cn + if conn.bad { + return driver.ErrBadConn + } + defer conn.errRecover(&err) + + for { + t := conn.recv1Buf(&rs.rb) + switch t { + case 'E': + err = parseError(&rs.rb) + case 'C', 'I': + continue + case 'Z': + conn.processReadyForQuery(&rs.rb) + rs.done = true + if err != nil { + return err + } + return io.EOF + case 'D': + n := rs.rb.int16() + if err != nil { + conn.bad = true + errorf("unexpected DataRow after error %s", err) + } + if n < len(dest) { + dest = dest[:n] + } + for i := range dest { + l := rs.rb.int32() + if l == -1 { + dest[i] = nil + continue + } + dest[i] = decode(&conn.parameterStatus, rs.rb.next(l), rs.colTyps[i], rs.colFmts[i]) + } + return + default: + errorf("unexpected message after execute: %q", t) + } + } +} + +// QuoteIdentifier quotes an "identifier" (e.g. a table or a column name) to be +// used as part of an SQL statement. For example: +// +// tblname := "my_table" +// data := "my_data" +// err = db.Exec(fmt.Sprintf("INSERT INTO %s VALUES ($1)", pq.QuoteIdentifier(tblname)), data) +// +// Any double quotes in name will be escaped. The quoted identifier will be +// case sensitive when used in a query. If the input string contains a zero +// byte, the result will be truncated immediately before it. +func QuoteIdentifier(name string) string { + end := strings.IndexRune(name, 0) + if end > -1 { + name = name[:end] + } + return `"` + strings.Replace(name, `"`, `""`, -1) + `"` +} + +func md5s(s string) string { + h := md5.New() + h.Write([]byte(s)) + return fmt.Sprintf("%x", h.Sum(nil)) +} + +func (cn *conn) sendBinaryParameters(b *writeBuf, args []driver.Value) { + // Do one pass over the parameters to see if we're going to send any of + // them over in binary. If we are, create a paramFormats array at the + // same time. + var paramFormats []int + for i, x := range args { + _, ok := x.([]byte) + if ok { + if paramFormats == nil { + paramFormats = make([]int, len(args)) + } + paramFormats[i] = 1 + } + } + if paramFormats == nil { + b.int16(0) + } else { + b.int16(len(paramFormats)) + for _, x := range paramFormats { + b.int16(x) + } + } + + b.int16(len(args)) + for _, x := range args { + if x == nil { + b.int32(-1) + } else { + datum := binaryEncode(&cn.parameterStatus, x) + b.int32(len(datum)) + b.bytes(datum) + } + } +} + +func (cn *conn) sendBinaryModeQuery(query string, args []driver.Value) { + if len(args) >= 65536 { + errorf("got %d parameters but PostgreSQL only supports 65535 parameters", len(args)) + } + + b := cn.writeBuf('P') + b.byte(0) // unnamed statement + b.string(query) + b.int16(0) + + b.next('B') + b.int16(0) // unnamed portal and statement + cn.sendBinaryParameters(b, args) + b.bytes(colFmtDataAllText) + + b.next('D') + b.byte('P') + b.byte(0) // unnamed portal + + b.next('E') + b.byte(0) + b.int32(0) + + b.next('S') + cn.send(b) +} + +func (c *conn) processParameterStatus(r *readBuf) { + var err error + + param := r.string() + switch param { + case "server_version": + var major1 int + var major2 int + var minor int + _, err = fmt.Sscanf(r.string(), "%d.%d.%d", &major1, &major2, &minor) + if err == nil { + c.parameterStatus.serverVersion = major1*10000 + major2*100 + minor + } + + case "TimeZone": + c.parameterStatus.currentLocation, err = time.LoadLocation(r.string()) + if err != nil { + c.parameterStatus.currentLocation = nil + } + + default: + // ignore + } +} + +func (c *conn) processReadyForQuery(r *readBuf) { + c.txnStatus = transactionStatus(r.byte()) +} + +func (cn *conn) readReadyForQuery() { + t, r := cn.recv1() + switch t { + case 'Z': + cn.processReadyForQuery(r) + return + default: + cn.bad = true + errorf("unexpected message %q; expected ReadyForQuery", t) + } +} + +func (cn *conn) readParseResponse() { + t, r := cn.recv1() + switch t { + case '1': + return + case 'E': + err := parseError(r) + cn.readReadyForQuery() + panic(err) + default: + cn.bad = true + errorf("unexpected Parse response %q", t) + } +} + +func (cn *conn) readStatementDescribeResponse() (paramTyps []oid.Oid, colNames []string, colTyps []oid.Oid) { + for { + t, r := cn.recv1() + switch t { + case 't': + nparams := r.int16() + paramTyps = make([]oid.Oid, nparams) + for i := range paramTyps { + paramTyps[i] = r.oid() + } + case 'n': + return paramTyps, nil, nil + case 'T': + colNames, colTyps = parseStatementRowDescribe(r) + return paramTyps, colNames, colTyps + case 'E': + err := parseError(r) + cn.readReadyForQuery() + panic(err) + default: + cn.bad = true + errorf("unexpected Describe statement response %q", t) + } + } +} + +func (cn *conn) readPortalDescribeResponse() (colNames []string, colFmts []format, colTyps []oid.Oid) { + t, r := cn.recv1() + switch t { + case 'T': + return parsePortalRowDescribe(r) + case 'n': + return nil, nil, nil + case 'E': + err := parseError(r) + cn.readReadyForQuery() + panic(err) + default: + cn.bad = true + errorf("unexpected Describe response %q", t) + } + panic("not reached") +} + +func (cn *conn) readBindResponse() { + t, r := cn.recv1() + switch t { + case '2': + return + case 'E': + err := parseError(r) + cn.readReadyForQuery() + panic(err) + default: + cn.bad = true + errorf("unexpected Bind response %q", t) + } +} + +func (cn *conn) postExecuteWorkaround() { + // Work around a bug in sql.DB.QueryRow: in Go 1.2 and earlier it ignores + // any errors from rows.Next, which masks errors that happened during the + // execution of the query. To avoid the problem in common cases, we wait + // here for one more message from the database. If it's not an error the + // query will likely succeed (or perhaps has already, if it's a + // CommandComplete), so we push the message into the conn struct; recv1 + // will return it as the next message for rows.Next or rows.Close. + // However, if it's an error, we wait until ReadyForQuery and then return + // the error to our caller. + for { + t, r := cn.recv1() + switch t { + case 'E': + err := parseError(r) + cn.readReadyForQuery() + panic(err) + case 'C', 'D', 'I': + // the query didn't fail, but we can't process this message + cn.saveMessage(t, r) + return + default: + cn.bad = true + errorf("unexpected message during extended query execution: %q", t) + } + } +} + +// Only for Exec(), since we ignore the returned data +func (cn *conn) readExecuteResponse(protocolState string) (res driver.Result, commandTag string, err error) { + for { + t, r := cn.recv1() + switch t { + case 'C': + if err != nil { + cn.bad = true + errorf("unexpected CommandComplete after error %s", err) + } + res, commandTag = cn.parseComplete(r.string()) + case 'Z': + cn.processReadyForQuery(r) + return res, commandTag, err + case 'E': + err = parseError(r) + case 'T', 'D', 'I': + if err != nil { + cn.bad = true + errorf("unexpected %q after error %s", t, err) + } + // ignore any results + default: + cn.bad = true + errorf("unknown %s response: %q", protocolState, t) + } + } +} + +func parseStatementRowDescribe(r *readBuf) (colNames []string, colTyps []oid.Oid) { + n := r.int16() + colNames = make([]string, n) + colTyps = make([]oid.Oid, n) + for i := range colNames { + colNames[i] = r.string() + r.next(6) + colTyps[i] = r.oid() + r.next(6) + // format code not known when describing a statement; always 0 + r.next(2) + } + return +} + +func parsePortalRowDescribe(r *readBuf) (colNames []string, colFmts []format, colTyps []oid.Oid) { + n := r.int16() + colNames = make([]string, n) + colFmts = make([]format, n) + colTyps = make([]oid.Oid, n) + for i := range colNames { + colNames[i] = r.string() + r.next(6) + colTyps[i] = r.oid() + r.next(6) + colFmts[i] = format(r.int16()) + } + return +} + +// parseEnviron tries to mimic some of libpq's environment handling +// +// To ease testing, it does not directly reference os.Environ, but is +// designed to accept its output. +// +// Environment-set connection information is intended to have a higher +// precedence than a library default but lower than any explicitly +// passed information (such as in the URL or connection string). +func parseEnviron(env []string) (out map[string]string) { + out = make(map[string]string) + + for _, v := range env { + parts := strings.SplitN(v, "=", 2) + + accrue := func(keyname string) { + out[keyname] = parts[1] + } + unsupported := func() { + panic(fmt.Sprintf("setting %v not supported", parts[0])) + } + + // The order of these is the same as is seen in the + // PostgreSQL 9.1 manual. Unsupported but well-defined + // keys cause a panic; these should be unset prior to + // execution. Options which pq expects to be set to a + // certain value are allowed, but must be set to that + // value if present (they can, of course, be absent). + switch parts[0] { + case "PGHOST": + accrue("host") + case "PGHOSTADDR": + unsupported() + case "PGPORT": + accrue("port") + case "PGDATABASE": + accrue("dbname") + case "PGUSER": + accrue("user") + case "PGPASSWORD": + accrue("password") + case "PGPASSFILE", "PGSERVICE", "PGSERVICEFILE", "PGREALM": + unsupported() + case "PGOPTIONS": + accrue("options") + case "PGAPPNAME": + accrue("application_name") + case "PGSSLMODE": + accrue("sslmode") + case "PGSSLCERT": + accrue("sslcert") + case "PGSSLKEY": + accrue("sslkey") + case "PGSSLROOTCERT": + accrue("sslrootcert") + case "PGREQUIRESSL", "PGSSLCRL": + unsupported() + case "PGREQUIREPEER": + unsupported() + case "PGKRBSRVNAME", "PGGSSLIB": + unsupported() + case "PGCONNECT_TIMEOUT": + accrue("connect_timeout") + case "PGCLIENTENCODING": + accrue("client_encoding") + case "PGDATESTYLE": + accrue("datestyle") + case "PGTZ": + accrue("timezone") + case "PGGEQO": + accrue("geqo") + case "PGSYSCONFDIR", "PGLOCALEDIR": + unsupported() + } + } + + return out +} + +// isUTF8 returns whether name is a fuzzy variation of the string "UTF-8". +func isUTF8(name string) bool { + // Recognize all sorts of silly things as "UTF-8", like Postgres does + s := strings.Map(alnumLowerASCII, name) + return s == "utf8" || s == "unicode" +} + +func alnumLowerASCII(ch rune) rune { + if 'A' <= ch && ch <= 'Z' { + return ch + ('a' - 'A') + } + if 'a' <= ch && ch <= 'z' || '0' <= ch && ch <= '9' { + return ch + } + return -1 // discard +} diff --git a/Godeps/_workspace/src/github.com/lib/pq/conn_test.go b/Godeps/_workspace/src/github.com/lib/pq/conn_test.go new file mode 100644 index 0000000..af07e55 --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/conn_test.go @@ -0,0 +1,1306 @@ +package pq + +import ( + "database/sql" + "database/sql/driver" + "fmt" + "io" + "os" + "reflect" + "strings" + "testing" + "time" +) + +type Fatalistic interface { + Fatal(args ...interface{}) +} + +func forceBinaryParameters() bool { + bp := os.Getenv("PQTEST_BINARY_PARAMETERS") + if bp == "yes" { + return true + } else if bp == "" || bp == "no" { + return false + } else { + panic("unexpected value for PQTEST_BINARY_PARAMETERS") + } +} + +func openTestConnConninfo(conninfo string) (*sql.DB, error) { + defaultTo := func(envvar string, value string) { + if os.Getenv(envvar) == "" { + os.Setenv(envvar, value) + } + } + defaultTo("PGDATABASE", "pqgotest") + defaultTo("PGSSLMODE", "disable") + defaultTo("PGCONNECT_TIMEOUT", "20") + + if forceBinaryParameters() && + !strings.HasPrefix(conninfo, "postgres://") && + !strings.HasPrefix(conninfo, "postgresql://") { + conninfo = conninfo + " binary_parameters=yes" + } + + return sql.Open("postgres", conninfo) +} + +func openTestConn(t Fatalistic) *sql.DB { + conn, err := openTestConnConninfo("") + if err != nil { + t.Fatal(err) + } + + return conn +} + +func getServerVersion(t *testing.T, db *sql.DB) int { + var version int + err := db.QueryRow("SHOW server_version_num").Scan(&version) + if err != nil { + t.Fatal(err) + } + return version +} + +func TestReconnect(t *testing.T) { + db1 := openTestConn(t) + defer db1.Close() + tx, err := db1.Begin() + if err != nil { + t.Fatal(err) + } + var pid1 int + err = tx.QueryRow("SELECT pg_backend_pid()").Scan(&pid1) + if err != nil { + t.Fatal(err) + } + db2 := openTestConn(t) + defer db2.Close() + _, err = db2.Exec("SELECT pg_terminate_backend($1)", pid1) + if err != nil { + t.Fatal(err) + } + // The rollback will probably "fail" because we just killed + // its connection above + _ = tx.Rollback() + + const expected int = 42 + var result int + err = db1.QueryRow(fmt.Sprintf("SELECT %d", expected)).Scan(&result) + if err != nil { + t.Fatal(err) + } + if result != expected { + t.Errorf("got %v; expected %v", result, expected) + } +} + +func TestCommitInFailedTransaction(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + txn, err := db.Begin() + if err != nil { + t.Fatal(err) + } + rows, err := txn.Query("SELECT error") + if err == nil { + rows.Close() + t.Fatal("expected failure") + } + err = txn.Commit() + if err != ErrInFailedTransaction { + t.Fatalf("expected ErrInFailedTransaction; got %#v", err) + } +} + +func TestOpenURL(t *testing.T) { + testURL := func(url string) { + db, err := openTestConnConninfo(url) + if err != nil { + t.Fatal(err) + } + defer db.Close() + // database/sql might not call our Open at all unless we do something with + // the connection + txn, err := db.Begin() + if err != nil { + t.Fatal(err) + } + txn.Rollback() + } + testURL("postgres://") + testURL("postgresql://") +} + +func TestExec(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + _, err := db.Exec("CREATE TEMP TABLE temp (a int)") + if err != nil { + t.Fatal(err) + } + + r, err := db.Exec("INSERT INTO temp VALUES (1)") + if err != nil { + t.Fatal(err) + } + + if n, _ := r.RowsAffected(); n != 1 { + t.Fatalf("expected 1 row affected, not %d", n) + } + + r, err = db.Exec("INSERT INTO temp VALUES ($1), ($2), ($3)", 1, 2, 3) + if err != nil { + t.Fatal(err) + } + + if n, _ := r.RowsAffected(); n != 3 { + t.Fatalf("expected 3 rows affected, not %d", n) + } + + // SELECT doesn't send the number of returned rows in the command tag + // before 9.0 + if getServerVersion(t, db) >= 90000 { + r, err = db.Exec("SELECT g FROM generate_series(1, 2) g") + if err != nil { + t.Fatal(err) + } + if n, _ := r.RowsAffected(); n != 2 { + t.Fatalf("expected 2 rows affected, not %d", n) + } + + r, err = db.Exec("SELECT g FROM generate_series(1, $1) g", 3) + if err != nil { + t.Fatal(err) + } + if n, _ := r.RowsAffected(); n != 3 { + t.Fatalf("expected 3 rows affected, not %d", n) + } + } +} + +func TestStatment(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + st, err := db.Prepare("SELECT 1") + if err != nil { + t.Fatal(err) + } + + st1, err := db.Prepare("SELECT 2") + if err != nil { + t.Fatal(err) + } + + r, err := st.Query() + if err != nil { + t.Fatal(err) + } + defer r.Close() + + if !r.Next() { + t.Fatal("expected row") + } + + var i int + err = r.Scan(&i) + if err != nil { + t.Fatal(err) + } + + if i != 1 { + t.Fatalf("expected 1, got %d", i) + } + + // st1 + + r1, err := st1.Query() + if err != nil { + t.Fatal(err) + } + defer r1.Close() + + if !r1.Next() { + if r.Err() != nil { + t.Fatal(r1.Err()) + } + t.Fatal("expected row") + } + + err = r1.Scan(&i) + if err != nil { + t.Fatal(err) + } + + if i != 2 { + t.Fatalf("expected 2, got %d", i) + } +} + +func TestRowsCloseBeforeDone(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + r, err := db.Query("SELECT 1") + if err != nil { + t.Fatal(err) + } + + err = r.Close() + if err != nil { + t.Fatal(err) + } + + if r.Next() { + t.Fatal("unexpected row") + } + + if r.Err() != nil { + t.Fatal(r.Err()) + } +} + +func TestParameterCountMismatch(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + var notused int + err := db.QueryRow("SELECT false", 1).Scan(¬used) + if err == nil { + t.Fatal("expected err") + } + // make sure we clean up correctly + err = db.QueryRow("SELECT 1").Scan(¬used) + if err != nil { + t.Fatal(err) + } + + err = db.QueryRow("SELECT $1").Scan(¬used) + if err == nil { + t.Fatal("expected err") + } + // make sure we clean up correctly + err = db.QueryRow("SELECT 1").Scan(¬used) + if err != nil { + t.Fatal(err) + } +} + +// Test that EmptyQueryResponses are handled correctly. +func TestEmptyQuery(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + _, err := db.Exec("") + if err != nil { + t.Fatal(err) + } + rows, err := db.Query("") + if err != nil { + t.Fatal(err) + } + cols, err := rows.Columns() + if err != nil { + t.Fatal(err) + } + if len(cols) != 0 { + t.Fatalf("unexpected number of columns %d in response to an empty query", len(cols)) + } + if rows.Next() { + t.Fatal("unexpected row") + } + if rows.Err() != nil { + t.Fatal(rows.Err()) + } + + stmt, err := db.Prepare("") + if err != nil { + t.Fatal(err) + } + _, err = stmt.Exec() + if err != nil { + t.Fatal(err) + } + rows, err = stmt.Query() + if err != nil { + t.Fatal(err) + } + cols, err = rows.Columns() + if err != nil { + t.Fatal(err) + } + if len(cols) != 0 { + t.Fatalf("unexpected number of columns %d in response to an empty query", len(cols)) + } + if rows.Next() { + t.Fatal("unexpected row") + } + if rows.Err() != nil { + t.Fatal(rows.Err()) + } +} + +func TestEncodeDecode(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + q := ` + SELECT + E'\\000\\001\\002'::bytea, + 'foobar'::text, + NULL::integer, + '2000-1-1 01:02:03.04-7'::timestamptz, + 0::boolean, + 123, + -321, + 3.14::float8 + WHERE + E'\\000\\001\\002'::bytea = $1 + AND 'foobar'::text = $2 + AND $3::integer is NULL + ` + // AND '2000-1-1 12:00:00.000000-7'::timestamp = $3 + + exp1 := []byte{0, 1, 2} + exp2 := "foobar" + + r, err := db.Query(q, exp1, exp2, nil) + if err != nil { + t.Fatal(err) + } + defer r.Close() + + if !r.Next() { + if r.Err() != nil { + t.Fatal(r.Err()) + } + t.Fatal("expected row") + } + + var got1 []byte + var got2 string + var got3 = sql.NullInt64{Valid: true} + var got4 time.Time + var got5, got6, got7, got8 interface{} + + err = r.Scan(&got1, &got2, &got3, &got4, &got5, &got6, &got7, &got8) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(exp1, got1) { + t.Errorf("expected %q byte: %q", exp1, got1) + } + + if !reflect.DeepEqual(exp2, got2) { + t.Errorf("expected %q byte: %q", exp2, got2) + } + + if got3.Valid { + t.Fatal("expected invalid") + } + + if got4.Year() != 2000 { + t.Fatal("wrong year") + } + + if got5 != false { + t.Fatalf("expected false, got %q", got5) + } + + if got6 != int64(123) { + t.Fatalf("expected 123, got %d", got6) + } + + if got7 != int64(-321) { + t.Fatalf("expected -321, got %d", got7) + } + + if got8 != float64(3.14) { + t.Fatalf("expected 3.14, got %f", got8) + } +} + +func TestNoData(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + st, err := db.Prepare("SELECT 1 WHERE true = false") + if err != nil { + t.Fatal(err) + } + defer st.Close() + + r, err := st.Query() + if err != nil { + t.Fatal(err) + } + defer r.Close() + + if r.Next() { + if r.Err() != nil { + t.Fatal(r.Err()) + } + t.Fatal("unexpected row") + } + + _, err = db.Query("SELECT * FROM nonexistenttable WHERE age=$1", 20) + if err == nil { + t.Fatal("Should have raised an error on non existent table") + } + + _, err = db.Query("SELECT * FROM nonexistenttable") + if err == nil { + t.Fatal("Should have raised an error on non existent table") + } +} + +func TestErrorDuringStartup(t *testing.T) { + // Don't use the normal connection setup, this is intended to + // blow up in the startup packet from a non-existent user. + db, err := openTestConnConninfo("user=thisuserreallydoesntexist") + if err != nil { + t.Fatal(err) + } + defer db.Close() + + _, err = db.Begin() + if err == nil { + t.Fatal("expected error") + } + + e, ok := err.(*Error) + if !ok { + t.Fatalf("expected Error, got %#v", err) + } else if e.Code.Name() != "invalid_authorization_specification" && e.Code.Name() != "invalid_password" { + t.Fatalf("expected invalid_authorization_specification or invalid_password, got %s (%+v)", e.Code.Name(), err) + } +} + +func TestBadConn(t *testing.T) { + var err error + + cn := conn{} + func() { + defer cn.errRecover(&err) + panic(io.EOF) + }() + if err != driver.ErrBadConn { + t.Fatalf("expected driver.ErrBadConn, got: %#v", err) + } + if !cn.bad { + t.Fatalf("expected cn.bad") + } + + cn = conn{} + func() { + defer cn.errRecover(&err) + e := &Error{Severity: Efatal} + panic(e) + }() + if err != driver.ErrBadConn { + t.Fatalf("expected driver.ErrBadConn, got: %#v", err) + } + if !cn.bad { + t.Fatalf("expected cn.bad") + } +} + +func TestErrorOnExec(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + txn, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer txn.Rollback() + + _, err = txn.Exec("CREATE TEMPORARY TABLE foo(f1 int PRIMARY KEY)") + if err != nil { + t.Fatal(err) + } + + _, err = txn.Exec("INSERT INTO foo VALUES (0), (0)") + if err == nil { + t.Fatal("Should have raised error") + } + + e, ok := err.(*Error) + if !ok { + t.Fatalf("expected Error, got %#v", err) + } else if e.Code.Name() != "unique_violation" { + t.Fatalf("expected unique_violation, got %s (%+v)", e.Code.Name(), err) + } +} + +func TestErrorOnQuery(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + txn, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer txn.Rollback() + + _, err = txn.Exec("CREATE TEMPORARY TABLE foo(f1 int PRIMARY KEY)") + if err != nil { + t.Fatal(err) + } + + _, err = txn.Query("INSERT INTO foo VALUES (0), (0)") + if err == nil { + t.Fatal("Should have raised error") + } + + e, ok := err.(*Error) + if !ok { + t.Fatalf("expected Error, got %#v", err) + } else if e.Code.Name() != "unique_violation" { + t.Fatalf("expected unique_violation, got %s (%+v)", e.Code.Name(), err) + } +} + +func TestErrorOnQueryRowSimpleQuery(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + txn, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer txn.Rollback() + + _, err = txn.Exec("CREATE TEMPORARY TABLE foo(f1 int PRIMARY KEY)") + if err != nil { + t.Fatal(err) + } + + var v int + err = txn.QueryRow("INSERT INTO foo VALUES (0), (0)").Scan(&v) + if err == nil { + t.Fatal("Should have raised error") + } + + e, ok := err.(*Error) + if !ok { + t.Fatalf("expected Error, got %#v", err) + } else if e.Code.Name() != "unique_violation" { + t.Fatalf("expected unique_violation, got %s (%+v)", e.Code.Name(), err) + } +} + +// Test the QueryRow bug workarounds in stmt.exec() and simpleQuery() +func TestQueryRowBugWorkaround(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + // stmt.exec() + _, err := db.Exec("CREATE TEMP TABLE notnulltemp (a varchar(10) not null)") + if err != nil { + t.Fatal(err) + } + + var a string + err = db.QueryRow("INSERT INTO notnulltemp(a) values($1) RETURNING a", nil).Scan(&a) + if err == sql.ErrNoRows { + t.Fatalf("expected constraint violation error; got: %v", err) + } + pge, ok := err.(*Error) + if !ok { + t.Fatalf("expected *Error; got: %#v", err) + } + if pge.Code.Name() != "not_null_violation" { + t.Fatalf("expected not_null_violation; got: %s (%+v)", pge.Code.Name(), err) + } + + // Test workaround in simpleQuery() + tx, err := db.Begin() + if err != nil { + t.Fatalf("unexpected error %s in Begin", err) + } + defer tx.Rollback() + + _, err = tx.Exec("SET LOCAL check_function_bodies TO FALSE") + if err != nil { + t.Fatalf("could not disable check_function_bodies: %s", err) + } + _, err = tx.Exec(` +CREATE OR REPLACE FUNCTION bad_function() +RETURNS integer +-- hack to prevent the function from being inlined +SET check_function_bodies TO TRUE +AS $$ + SELECT text 'bad' +$$ LANGUAGE sql`) + if err != nil { + t.Fatalf("could not create function: %s", err) + } + + err = tx.QueryRow("SELECT * FROM bad_function()").Scan(&a) + if err == nil { + t.Fatalf("expected error") + } + pge, ok = err.(*Error) + if !ok { + t.Fatalf("expected *Error; got: %#v", err) + } + if pge.Code.Name() != "invalid_function_definition" { + t.Fatalf("expected invalid_function_definition; got: %s (%+v)", pge.Code.Name(), err) + } + + err = tx.Rollback() + if err != nil { + t.Fatalf("unexpected error %s in Rollback", err) + } + + // Also test that simpleQuery()'s workaround works when the query fails + // after a row has been received. + rows, err := db.Query(` +select + (select generate_series(1, ss.i)) +from (select gs.i + from generate_series(1, 2) gs(i) + order by gs.i limit 2) ss`) + if err != nil { + t.Fatalf("query failed: %s", err) + } + if !rows.Next() { + t.Fatalf("expected at least one result row; got %s", rows.Err()) + } + var i int + err = rows.Scan(&i) + if err != nil { + t.Fatalf("rows.Scan() failed: %s", err) + } + if i != 1 { + t.Fatalf("unexpected value for i: %d", i) + } + if rows.Next() { + t.Fatalf("unexpected row") + } + pge, ok = rows.Err().(*Error) + if !ok { + t.Fatalf("expected *Error; got: %#v", err) + } + if pge.Code.Name() != "cardinality_violation" { + t.Fatalf("expected cardinality_violation; got: %s (%+v)", pge.Code.Name(), rows.Err()) + } +} + +func TestSimpleQuery(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + r, err := db.Query("select 1") + if err != nil { + t.Fatal(err) + } + defer r.Close() + + if !r.Next() { + t.Fatal("expected row") + } +} + +func TestBindError(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + _, err := db.Exec("create temp table test (i integer)") + if err != nil { + t.Fatal(err) + } + + _, err = db.Query("select * from test where i=$1", "hhh") + if err == nil { + t.Fatal("expected an error") + } + + // Should not get error here + r, err := db.Query("select * from test where i=$1", 1) + if err != nil { + t.Fatal(err) + } + defer r.Close() +} + +func TestParseErrorInExtendedQuery(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + rows, err := db.Query("PARSE_ERROR $1", 1) + if err == nil { + t.Fatal("expected error") + } + + rows, err = db.Query("SELECT 1") + if err != nil { + t.Fatal(err) + } + rows.Close() +} + +// TestReturning tests that an INSERT query using the RETURNING clause returns a row. +func TestReturning(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + _, err := db.Exec("CREATE TEMP TABLE distributors (did integer default 0, dname text)") + if err != nil { + t.Fatal(err) + } + + rows, err := db.Query("INSERT INTO distributors (did, dname) VALUES (DEFAULT, 'XYZ Widgets') " + + "RETURNING did;") + if err != nil { + t.Fatal(err) + } + if !rows.Next() { + t.Fatal("no rows") + } + var did int + err = rows.Scan(&did) + if err != nil { + t.Fatal(err) + } + if did != 0 { + t.Fatalf("bad value for did: got %d, want %d", did, 0) + } + + if rows.Next() { + t.Fatal("unexpected next row") + } + err = rows.Err() + if err != nil { + t.Fatal(err) + } +} + +func TestIssue186(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + // Exec() a query which returns results + _, err := db.Exec("VALUES (1), (2), (3)") + if err != nil { + t.Fatal(err) + } + + _, err = db.Exec("VALUES ($1), ($2), ($3)", 1, 2, 3) + if err != nil { + t.Fatal(err) + } + + // Query() a query which doesn't return any results + txn, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer txn.Rollback() + + rows, err := txn.Query("CREATE TEMP TABLE foo(f1 int)") + if err != nil { + t.Fatal(err) + } + if err = rows.Close(); err != nil { + t.Fatal(err) + } + + // small trick to get NoData from a parameterized query + _, err = txn.Exec("CREATE RULE nodata AS ON INSERT TO foo DO INSTEAD NOTHING") + if err != nil { + t.Fatal(err) + } + rows, err = txn.Query("INSERT INTO foo VALUES ($1)", 1) + if err != nil { + t.Fatal(err) + } + if err = rows.Close(); err != nil { + t.Fatal(err) + } +} + +func TestIssue196(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + row := db.QueryRow("SELECT float4 '0.10000122' = $1, float8 '35.03554004971999' = $2", + float32(0.10000122), float64(35.03554004971999)) + + var float4match, float8match bool + err := row.Scan(&float4match, &float8match) + if err != nil { + t.Fatal(err) + } + if !float4match { + t.Errorf("Expected float4 fidelity to be maintained; got no match") + } + if !float8match { + t.Errorf("Expected float8 fidelity to be maintained; got no match") + } +} + +// Test that any CommandComplete messages sent before the query results are +// ignored. +func TestIssue282(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + var search_path string + err := db.QueryRow(` + SET LOCAL search_path TO pg_catalog; + SET LOCAL search_path TO pg_catalog; + SHOW search_path`).Scan(&search_path) + if err != nil { + t.Fatal(err) + } + if search_path != "pg_catalog" { + t.Fatalf("unexpected search_path %s", search_path) + } +} + +func TestReadFloatPrecision(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + row := db.QueryRow("SELECT float4 '0.10000122', float8 '35.03554004971999'") + var float4val float32 + var float8val float64 + err := row.Scan(&float4val, &float8val) + if err != nil { + t.Fatal(err) + } + if float4val != float32(0.10000122) { + t.Errorf("Expected float4 fidelity to be maintained; got no match") + } + if float8val != float64(35.03554004971999) { + t.Errorf("Expected float8 fidelity to be maintained; got no match") + } +} + +func TestXactMultiStmt(t *testing.T) { + // minified test case based on bug reports from + // pico303@gmail.com and rangelspam@gmail.com + t.Skip("Skipping failing test") + db := openTestConn(t) + defer db.Close() + + tx, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer tx.Commit() + + rows, err := tx.Query("select 1") + if err != nil { + t.Fatal(err) + } + + if rows.Next() { + var val int32 + if err = rows.Scan(&val); err != nil { + t.Fatal(err) + } + } else { + t.Fatal("Expected at least one row in first query in xact") + } + + rows2, err := tx.Query("select 2") + if err != nil { + t.Fatal(err) + } + + if rows2.Next() { + var val2 int32 + if err := rows2.Scan(&val2); err != nil { + t.Fatal(err) + } + } else { + t.Fatal("Expected at least one row in second query in xact") + } + + if err = rows.Err(); err != nil { + t.Fatal(err) + } + + if err = rows2.Err(); err != nil { + t.Fatal(err) + } + + if err = tx.Commit(); err != nil { + t.Fatal(err) + } +} + +var envParseTests = []struct { + Expected map[string]string + Env []string +}{ + { + Env: []string{"PGDATABASE=hello", "PGUSER=goodbye"}, + Expected: map[string]string{"dbname": "hello", "user": "goodbye"}, + }, + { + Env: []string{"PGDATESTYLE=ISO, MDY"}, + Expected: map[string]string{"datestyle": "ISO, MDY"}, + }, + { + Env: []string{"PGCONNECT_TIMEOUT=30"}, + Expected: map[string]string{"connect_timeout": "30"}, + }, +} + +func TestParseEnviron(t *testing.T) { + for i, tt := range envParseTests { + results := parseEnviron(tt.Env) + if !reflect.DeepEqual(tt.Expected, results) { + t.Errorf("%d: Expected: %#v Got: %#v", i, tt.Expected, results) + } + } +} + +func TestParseComplete(t *testing.T) { + tpc := func(commandTag string, command string, affectedRows int64, shouldFail bool) { + defer func() { + if p := recover(); p != nil { + if !shouldFail { + t.Error(p) + } + } + }() + cn := &conn{} + res, c := cn.parseComplete(commandTag) + if c != command { + t.Errorf("Expected %v, got %v", command, c) + } + n, err := res.RowsAffected() + if err != nil { + t.Fatal(err) + } + if n != affectedRows { + t.Errorf("Expected %d, got %d", affectedRows, n) + } + } + + tpc("ALTER TABLE", "ALTER TABLE", 0, false) + tpc("INSERT 0 1", "INSERT", 1, false) + tpc("UPDATE 100", "UPDATE", 100, false) + tpc("SELECT 100", "SELECT", 100, false) + tpc("FETCH 100", "FETCH", 100, false) + // allow COPY (and others) without row count + tpc("COPY", "COPY", 0, false) + // don't fail on command tags we don't recognize + tpc("UNKNOWNCOMMANDTAG", "UNKNOWNCOMMANDTAG", 0, false) + + // failure cases + tpc("INSERT 1", "", 0, true) // missing oid + tpc("UPDATE 0 1", "", 0, true) // too many numbers + tpc("SELECT foo", "", 0, true) // invalid row count +} + +func TestExecerInterface(t *testing.T) { + // Gin up a straw man private struct just for the type check + cn := &conn{c: nil} + var cni interface{} = cn + + _, ok := cni.(driver.Execer) + if !ok { + t.Fatal("Driver doesn't implement Execer") + } +} + +func TestNullAfterNonNull(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + r, err := db.Query("SELECT 9::integer UNION SELECT NULL::integer") + if err != nil { + t.Fatal(err) + } + + var n sql.NullInt64 + + if !r.Next() { + if r.Err() != nil { + t.Fatal(err) + } + t.Fatal("expected row") + } + + if err := r.Scan(&n); err != nil { + t.Fatal(err) + } + + if n.Int64 != 9 { + t.Fatalf("expected 2, not %d", n.Int64) + } + + if !r.Next() { + if r.Err() != nil { + t.Fatal(err) + } + t.Fatal("expected row") + } + + if err := r.Scan(&n); err != nil { + t.Fatal(err) + } + + if n.Valid { + t.Fatal("expected n to be invalid") + } + + if n.Int64 != 0 { + t.Fatalf("expected n to 2, not %d", n.Int64) + } +} + +func Test64BitErrorChecking(t *testing.T) { + defer func() { + if err := recover(); err != nil { + t.Fatal("panic due to 0xFFFFFFFF != -1 " + + "when int is 64 bits") + } + }() + + db := openTestConn(t) + defer db.Close() + + r, err := db.Query(`SELECT * +FROM (VALUES (0::integer, NULL::text), (1, 'test string')) AS t;`) + + if err != nil { + t.Fatal(err) + } + + defer r.Close() + + for r.Next() { + } +} + +func TestCommit(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + _, err := db.Exec("CREATE TEMP TABLE temp (a int)") + if err != nil { + t.Fatal(err) + } + sqlInsert := "INSERT INTO temp VALUES (1)" + sqlSelect := "SELECT * FROM temp" + tx, err := db.Begin() + if err != nil { + t.Fatal(err) + } + _, err = tx.Exec(sqlInsert) + if err != nil { + t.Fatal(err) + } + err = tx.Commit() + if err != nil { + t.Fatal(err) + } + var i int + err = db.QueryRow(sqlSelect).Scan(&i) + if err != nil { + t.Fatal(err) + } + if i != 1 { + t.Fatalf("expected 1, got %d", i) + } +} + +func TestErrorClass(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + _, err := db.Query("SELECT int 'notint'") + if err == nil { + t.Fatal("expected error") + } + pge, ok := err.(*Error) + if !ok { + t.Fatalf("expected *pq.Error, got %#+v", err) + } + if pge.Code.Class() != "22" { + t.Fatalf("expected class 28, got %v", pge.Code.Class()) + } + if pge.Code.Class().Name() != "data_exception" { + t.Fatalf("expected data_exception, got %v", pge.Code.Class().Name()) + } +} + +func TestParseOpts(t *testing.T) { + tests := []struct { + in string + expected values + valid bool + }{ + {"dbname=hello user=goodbye", values{"dbname": "hello", "user": "goodbye"}, true}, + {"dbname=hello user=goodbye ", values{"dbname": "hello", "user": "goodbye"}, true}, + {"dbname = hello user=goodbye", values{"dbname": "hello", "user": "goodbye"}, true}, + {"dbname=hello user =goodbye", values{"dbname": "hello", "user": "goodbye"}, true}, + {"dbname=hello user= goodbye", values{"dbname": "hello", "user": "goodbye"}, true}, + {"host=localhost password='correct horse battery staple'", values{"host": "localhost", "password": "correct horse battery staple"}, true}, + {"dbname=データベース password=パスワード", values{"dbname": "データベース", "password": "パスワード"}, true}, + {"dbname=hello user=''", values{"dbname": "hello", "user": ""}, true}, + {"user='' dbname=hello", values{"dbname": "hello", "user": ""}, true}, + // The last option value is an empty string if there's no non-whitespace after its = + {"dbname=hello user= ", values{"dbname": "hello", "user": ""}, true}, + + // The parser ignores spaces after = and interprets the next set of non-whitespace characters as the value. + {"user= password=foo", values{"user": "password=foo"}, true}, + + // Backslash escapes next char + {`user=a\ \'\\b`, values{"user": `a '\b`}, true}, + {`user='a \'b'`, values{"user": `a 'b`}, true}, + + // Incomplete escape + {`user=x\`, values{}, false}, + + // No '=' after the key + {"postgre://marko@internet", values{}, false}, + {"dbname user=goodbye", values{}, false}, + {"user=foo blah", values{}, false}, + {"user=foo blah ", values{}, false}, + + // Unterminated quoted value + {"dbname=hello user='unterminated", values{}, false}, + } + + for _, test := range tests { + o := make(values) + err := parseOpts(test.in, o) + + switch { + case err != nil && test.valid: + t.Errorf("%q got unexpected error: %s", test.in, err) + case err == nil && test.valid && !reflect.DeepEqual(test.expected, o): + t.Errorf("%q got: %#v want: %#v", test.in, o, test.expected) + case err == nil && !test.valid: + t.Errorf("%q expected an error", test.in) + } + } +} + +func TestRuntimeParameters(t *testing.T) { + type RuntimeTestResult int + const ( + ResultUnknown RuntimeTestResult = iota + ResultSuccess + ResultError // other error + ) + + tests := []struct { + conninfo string + param string + expected string + expectedOutcome RuntimeTestResult + }{ + // invalid parameter + {"DOESNOTEXIST=foo", "", "", ResultError}, + // we can only work with a specific value for these two + {"client_encoding=SQL_ASCII", "", "", ResultError}, + {"datestyle='ISO, YDM'", "", "", ResultError}, + // "options" should work exactly as it does in libpq + {"options='-c search_path=pqgotest'", "search_path", "pqgotest", ResultSuccess}, + // pq should override client_encoding in this case + {"options='-c client_encoding=SQL_ASCII'", "client_encoding", "UTF8", ResultSuccess}, + // allow client_encoding to be set explicitly + {"client_encoding=UTF8", "client_encoding", "UTF8", ResultSuccess}, + // test a runtime parameter not supported by libpq + {"work_mem='139kB'", "work_mem", "139kB", ResultSuccess}, + // test fallback_application_name + {"application_name=foo fallback_application_name=bar", "application_name", "foo", ResultSuccess}, + {"application_name='' fallback_application_name=bar", "application_name", "", ResultSuccess}, + {"fallback_application_name=bar", "application_name", "bar", ResultSuccess}, + } + + for _, test := range tests { + db, err := openTestConnConninfo(test.conninfo) + if err != nil { + t.Fatal(err) + } + + // application_name didn't exist before 9.0 + if test.param == "application_name" && getServerVersion(t, db) < 90000 { + db.Close() + continue + } + + tryGetParameterValue := func() (value string, outcome RuntimeTestResult) { + defer db.Close() + row := db.QueryRow("SELECT current_setting($1)", test.param) + err = row.Scan(&value) + if err != nil { + return "", ResultError + } + return value, ResultSuccess + } + + value, outcome := tryGetParameterValue() + if outcome != test.expectedOutcome && outcome == ResultError { + t.Fatalf("%v: unexpected error: %v", test.conninfo, err) + } + if outcome != test.expectedOutcome { + t.Fatalf("unexpected outcome %v (was expecting %v) for conninfo \"%s\"", + outcome, test.expectedOutcome, test.conninfo) + } + if value != test.expected { + t.Fatalf("bad value for %s: got %s, want %s with conninfo \"%s\"", + test.param, value, test.expected, test.conninfo) + } + } +} + +func TestIsUTF8(t *testing.T) { + var cases = []struct { + name string + want bool + }{ + {"unicode", true}, + {"utf-8", true}, + {"utf_8", true}, + {"UTF-8", true}, + {"UTF8", true}, + {"utf8", true}, + {"u n ic_ode", true}, + {"ut_f%8", true}, + {"ubf8", false}, + {"punycode", false}, + } + + for _, test := range cases { + if g := isUTF8(test.name); g != test.want { + t.Errorf("isUTF8(%q) = %v want %v", test.name, g, test.want) + } + } +} + +func TestQuoteIdentifier(t *testing.T) { + var cases = []struct { + input string + want string + }{ + {`foo`, `"foo"`}, + {`foo bar baz`, `"foo bar baz"`}, + {`foo"bar`, `"foo""bar"`}, + {"foo\x00bar", `"foo"`}, + {"\x00foo", `""`}, + } + + for _, test := range cases { + got := QuoteIdentifier(test.input) + if got != test.want { + t.Errorf("QuoteIdentifier(%q) = %v want %v", test.input, got, test.want) + } + } +} diff --git a/Godeps/_workspace/src/github.com/lib/pq/copy.go b/Godeps/_workspace/src/github.com/lib/pq/copy.go new file mode 100644 index 0000000..e44fa48 --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/copy.go @@ -0,0 +1,268 @@ +package pq + +import ( + "database/sql/driver" + "encoding/binary" + "errors" + "fmt" + "sync" +) + +var ( + errCopyInClosed = errors.New("pq: copyin statement has already been closed") + errBinaryCopyNotSupported = errors.New("pq: only text format supported for COPY") + errCopyToNotSupported = errors.New("pq: COPY TO is not supported") + errCopyNotSupportedOutsideTxn = errors.New("pq: COPY is only allowed inside a transaction") +) + +// CopyIn creates a COPY FROM statement which can be prepared with +// Tx.Prepare(). The target table should be visible in search_path. +func CopyIn(table string, columns ...string) string { + stmt := "COPY " + QuoteIdentifier(table) + " (" + for i, col := range columns { + if i != 0 { + stmt += ", " + } + stmt += QuoteIdentifier(col) + } + stmt += ") FROM STDIN" + return stmt +} + +// CopyInSchema creates a COPY FROM statement which can be prepared with +// Tx.Prepare(). +func CopyInSchema(schema, table string, columns ...string) string { + stmt := "COPY " + QuoteIdentifier(schema) + "." + QuoteIdentifier(table) + " (" + for i, col := range columns { + if i != 0 { + stmt += ", " + } + stmt += QuoteIdentifier(col) + } + stmt += ") FROM STDIN" + return stmt +} + +type copyin struct { + cn *conn + buffer []byte + rowData chan []byte + done chan bool + + closed bool + + sync.Mutex // guards err + err error +} + +const ciBufferSize = 64 * 1024 + +// flush buffer before the buffer is filled up and needs reallocation +const ciBufferFlushSize = 63 * 1024 + +func (cn *conn) prepareCopyIn(q string) (_ driver.Stmt, err error) { + if !cn.isInTransaction() { + return nil, errCopyNotSupportedOutsideTxn + } + + ci := ©in{ + cn: cn, + buffer: make([]byte, 0, ciBufferSize), + rowData: make(chan []byte), + done: make(chan bool, 1), + } + // add CopyData identifier + 4 bytes for message length + ci.buffer = append(ci.buffer, 'd', 0, 0, 0, 0) + + b := cn.writeBuf('Q') + b.string(q) + cn.send(b) + +awaitCopyInResponse: + for { + t, r := cn.recv1() + switch t { + case 'G': + if r.byte() != 0 { + err = errBinaryCopyNotSupported + break awaitCopyInResponse + } + go ci.resploop() + return ci, nil + case 'H': + err = errCopyToNotSupported + break awaitCopyInResponse + case 'E': + err = parseError(r) + case 'Z': + if err == nil { + cn.bad = true + errorf("unexpected ReadyForQuery in response to COPY") + } + cn.processReadyForQuery(r) + return nil, err + default: + cn.bad = true + errorf("unknown response for copy query: %q", t) + } + } + + // something went wrong, abort COPY before we return + b = cn.writeBuf('f') + b.string(err.Error()) + cn.send(b) + + for { + t, r := cn.recv1() + switch t { + case 'c', 'C', 'E': + case 'Z': + // correctly aborted, we're done + cn.processReadyForQuery(r) + return nil, err + default: + cn.bad = true + errorf("unknown response for CopyFail: %q", t) + } + } +} + +func (ci *copyin) flush(buf []byte) { + // set message length (without message identifier) + binary.BigEndian.PutUint32(buf[1:], uint32(len(buf)-1)) + + _, err := ci.cn.c.Write(buf) + if err != nil { + panic(err) + } +} + +func (ci *copyin) resploop() { + for { + var r readBuf + t, err := ci.cn.recvMessage(&r) + if err != nil { + ci.cn.bad = true + ci.setError(err) + ci.done <- true + return + } + switch t { + case 'C': + // complete + case 'N': + // NoticeResponse + case 'Z': + ci.cn.processReadyForQuery(&r) + ci.done <- true + return + case 'E': + err := parseError(&r) + ci.setError(err) + default: + ci.cn.bad = true + ci.setError(fmt.Errorf("unknown response during CopyIn: %q", t)) + ci.done <- true + return + } + } +} + +func (ci *copyin) isErrorSet() bool { + ci.Lock() + isSet := (ci.err != nil) + ci.Unlock() + return isSet +} + +// setError() sets ci.err if one has not been set already. Caller must not be +// holding ci.Mutex. +func (ci *copyin) setError(err error) { + ci.Lock() + if ci.err == nil { + ci.err = err + } + ci.Unlock() +} + +func (ci *copyin) NumInput() int { + return -1 +} + +func (ci *copyin) Query(v []driver.Value) (r driver.Rows, err error) { + return nil, ErrNotSupported +} + +// Exec inserts values into the COPY stream. The insert is asynchronous +// and Exec can return errors from previous Exec calls to the same +// COPY stmt. +// +// You need to call Exec(nil) to sync the COPY stream and to get any +// errors from pending data, since Stmt.Close() doesn't return errors +// to the user. +func (ci *copyin) Exec(v []driver.Value) (r driver.Result, err error) { + if ci.closed { + return nil, errCopyInClosed + } + + if ci.cn.bad { + return nil, driver.ErrBadConn + } + defer ci.cn.errRecover(&err) + + if ci.isErrorSet() { + return nil, ci.err + } + + if len(v) == 0 { + err = ci.Close() + ci.closed = true + return nil, err + } + + numValues := len(v) + for i, value := range v { + ci.buffer = appendEncodedText(&ci.cn.parameterStatus, ci.buffer, value) + if i < numValues-1 { + ci.buffer = append(ci.buffer, '\t') + } + } + + ci.buffer = append(ci.buffer, '\n') + + if len(ci.buffer) > ciBufferFlushSize { + ci.flush(ci.buffer) + // reset buffer, keep bytes for message identifier and length + ci.buffer = ci.buffer[:5] + } + + return driver.RowsAffected(0), nil +} + +func (ci *copyin) Close() (err error) { + if ci.closed { + return errCopyInClosed + } + + if ci.cn.bad { + return driver.ErrBadConn + } + defer ci.cn.errRecover(&err) + + if len(ci.buffer) > 0 { + ci.flush(ci.buffer) + } + // Avoid touching the scratch buffer as resploop could be using it. + err = ci.cn.sendSimpleMessage('c') + if err != nil { + return err + } + + <-ci.done + + if ci.isErrorSet() { + err = ci.err + return err + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/lib/pq/copy_test.go b/Godeps/_workspace/src/github.com/lib/pq/copy_test.go new file mode 100644 index 0000000..6af4c9c --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/copy_test.go @@ -0,0 +1,462 @@ +package pq + +import ( + "bytes" + "database/sql" + "strings" + "testing" +) + +func TestCopyInStmt(t *testing.T) { + var stmt string + stmt = CopyIn("table name") + if stmt != `COPY "table name" () FROM STDIN` { + t.Fatal(stmt) + } + + stmt = CopyIn("table name", "column 1", "column 2") + if stmt != `COPY "table name" ("column 1", "column 2") FROM STDIN` { + t.Fatal(stmt) + } + + stmt = CopyIn(`table " name """`, `co"lumn""`) + if stmt != `COPY "table "" name """"""" ("co""lumn""""") FROM STDIN` { + t.Fatal(stmt) + } +} + +func TestCopyInSchemaStmt(t *testing.T) { + var stmt string + stmt = CopyInSchema("schema name", "table name") + if stmt != `COPY "schema name"."table name" () FROM STDIN` { + t.Fatal(stmt) + } + + stmt = CopyInSchema("schema name", "table name", "column 1", "column 2") + if stmt != `COPY "schema name"."table name" ("column 1", "column 2") FROM STDIN` { + t.Fatal(stmt) + } + + stmt = CopyInSchema(`schema " name """`, `table " name """`, `co"lumn""`) + if stmt != `COPY "schema "" name """"""".`+ + `"table "" name """"""" ("co""lumn""""") FROM STDIN` { + t.Fatal(stmt) + } +} + +func TestCopyInMultipleValues(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + txn, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer txn.Rollback() + + _, err = txn.Exec("CREATE TEMP TABLE temp (a int, b varchar)") + if err != nil { + t.Fatal(err) + } + + stmt, err := txn.Prepare(CopyIn("temp", "a", "b")) + if err != nil { + t.Fatal(err) + } + + longString := strings.Repeat("#", 500) + + for i := 0; i < 500; i++ { + _, err = stmt.Exec(int64(i), longString) + if err != nil { + t.Fatal(err) + } + } + + _, err = stmt.Exec() + if err != nil { + t.Fatal(err) + } + + err = stmt.Close() + if err != nil { + t.Fatal(err) + } + + var num int + err = txn.QueryRow("SELECT COUNT(*) FROM temp").Scan(&num) + if err != nil { + t.Fatal(err) + } + + if num != 500 { + t.Fatalf("expected 500 items, not %d", num) + } +} + +func TestCopyInRaiseStmtTrigger(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + if getServerVersion(t, db) < 90000 { + var exists int + err := db.QueryRow("SELECT 1 FROM pg_language WHERE lanname = 'plpgsql'").Scan(&exists) + if err == sql.ErrNoRows { + t.Skip("language PL/PgSQL does not exist; skipping TestCopyInRaiseStmtTrigger") + } else if err != nil { + t.Fatal(err) + } + } + + txn, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer txn.Rollback() + + _, err = txn.Exec("CREATE TEMP TABLE temp (a int, b varchar)") + if err != nil { + t.Fatal(err) + } + + _, err = txn.Exec(` + CREATE OR REPLACE FUNCTION pg_temp.temptest() + RETURNS trigger AS + $BODY$ begin + raise notice 'Hello world'; + return new; + end $BODY$ + LANGUAGE plpgsql`) + if err != nil { + t.Fatal(err) + } + + _, err = txn.Exec(` + CREATE TRIGGER temptest_trigger + BEFORE INSERT + ON temp + FOR EACH ROW + EXECUTE PROCEDURE pg_temp.temptest()`) + if err != nil { + t.Fatal(err) + } + + stmt, err := txn.Prepare(CopyIn("temp", "a", "b")) + if err != nil { + t.Fatal(err) + } + + longString := strings.Repeat("#", 500) + + _, err = stmt.Exec(int64(1), longString) + if err != nil { + t.Fatal(err) + } + + _, err = stmt.Exec() + if err != nil { + t.Fatal(err) + } + + err = stmt.Close() + if err != nil { + t.Fatal(err) + } + + var num int + err = txn.QueryRow("SELECT COUNT(*) FROM temp").Scan(&num) + if err != nil { + t.Fatal(err) + } + + if num != 1 { + t.Fatalf("expected 1 items, not %d", num) + } +} + +func TestCopyInTypes(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + txn, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer txn.Rollback() + + _, err = txn.Exec("CREATE TEMP TABLE temp (num INTEGER, text VARCHAR, blob BYTEA, nothing VARCHAR)") + if err != nil { + t.Fatal(err) + } + + stmt, err := txn.Prepare(CopyIn("temp", "num", "text", "blob", "nothing")) + if err != nil { + t.Fatal(err) + } + + _, err = stmt.Exec(int64(1234567890), "Héllö\n ☃!\r\t\\", []byte{0, 255, 9, 10, 13}, nil) + if err != nil { + t.Fatal(err) + } + + _, err = stmt.Exec() + if err != nil { + t.Fatal(err) + } + + err = stmt.Close() + if err != nil { + t.Fatal(err) + } + + var num int + var text string + var blob []byte + var nothing sql.NullString + + err = txn.QueryRow("SELECT * FROM temp").Scan(&num, &text, &blob, ¬hing) + if err != nil { + t.Fatal(err) + } + + if num != 1234567890 { + t.Fatal("unexpected result", num) + } + if text != "Héllö\n ☃!\r\t\\" { + t.Fatal("unexpected result", text) + } + if bytes.Compare(blob, []byte{0, 255, 9, 10, 13}) != 0 { + t.Fatal("unexpected result", blob) + } + if nothing.Valid { + t.Fatal("unexpected result", nothing.String) + } +} + +func TestCopyInWrongType(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + txn, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer txn.Rollback() + + _, err = txn.Exec("CREATE TEMP TABLE temp (num INTEGER)") + if err != nil { + t.Fatal(err) + } + + stmt, err := txn.Prepare(CopyIn("temp", "num")) + if err != nil { + t.Fatal(err) + } + defer stmt.Close() + + _, err = stmt.Exec("Héllö\n ☃!\r\t\\") + if err != nil { + t.Fatal(err) + } + + _, err = stmt.Exec() + if err == nil { + t.Fatal("expected error") + } + if pge := err.(*Error); pge.Code.Name() != "invalid_text_representation" { + t.Fatalf("expected 'invalid input syntax for integer' error, got %s (%+v)", pge.Code.Name(), pge) + } +} + +func TestCopyOutsideOfTxnError(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + _, err := db.Prepare(CopyIn("temp", "num")) + if err == nil { + t.Fatal("COPY outside of transaction did not return an error") + } + if err != errCopyNotSupportedOutsideTxn { + t.Fatalf("expected %s, got %s", err, err.Error()) + } +} + +func TestCopyInBinaryError(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + txn, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer txn.Rollback() + + _, err = txn.Exec("CREATE TEMP TABLE temp (num INTEGER)") + if err != nil { + t.Fatal(err) + } + _, err = txn.Prepare("COPY temp (num) FROM STDIN WITH binary") + if err != errBinaryCopyNotSupported { + t.Fatalf("expected %s, got %+v", errBinaryCopyNotSupported, err) + } + // check that the protocol is in a valid state + err = txn.Rollback() + if err != nil { + t.Fatal(err) + } +} + +func TestCopyFromError(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + txn, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer txn.Rollback() + + _, err = txn.Exec("CREATE TEMP TABLE temp (num INTEGER)") + if err != nil { + t.Fatal(err) + } + _, err = txn.Prepare("COPY temp (num) TO STDOUT") + if err != errCopyToNotSupported { + t.Fatalf("expected %s, got %+v", errCopyToNotSupported, err) + } + // check that the protocol is in a valid state + err = txn.Rollback() + if err != nil { + t.Fatal(err) + } +} + +func TestCopySyntaxError(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + txn, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer txn.Rollback() + + _, err = txn.Prepare("COPY ") + if err == nil { + t.Fatal("expected error") + } + if pge := err.(*Error); pge.Code.Name() != "syntax_error" { + t.Fatalf("expected syntax error, got %s (%+v)", pge.Code.Name(), pge) + } + // check that the protocol is in a valid state + err = txn.Rollback() + if err != nil { + t.Fatal(err) + } +} + +// Tests for connection errors in copyin.resploop() +func TestCopyRespLoopConnectionError(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + txn, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer txn.Rollback() + + var pid int + err = txn.QueryRow("SELECT pg_backend_pid()").Scan(&pid) + if err != nil { + t.Fatal(err) + } + + _, err = txn.Exec("CREATE TEMP TABLE temp (a int)") + if err != nil { + t.Fatal(err) + } + + stmt, err := txn.Prepare(CopyIn("temp", "a")) + if err != nil { + t.Fatal(err) + } + + _, err = db.Exec("SELECT pg_terminate_backend($1)", pid) + if err != nil { + t.Fatal(err) + } + + if getServerVersion(t, db) < 90500 { + // We have to try and send something over, since postgres before + // version 9.5 won't process SIGTERMs while it's waiting for + // CopyData/CopyEnd messages; see tcop/postgres.c. + _, err = stmt.Exec(1) + if err != nil { + t.Fatal(err) + } + } + _, err = stmt.Exec() + if err == nil { + t.Fatalf("expected error") + } + pge, ok := err.(*Error) + if !ok { + t.Fatalf("expected *pq.Error, got %+#v", err) + } else if pge.Code.Name() != "admin_shutdown" { + t.Fatalf("expected admin_shutdown, got %s", pge.Code.Name()) + } + + err = stmt.Close() + if err != nil { + t.Fatal(err) + } +} + +func BenchmarkCopyIn(b *testing.B) { + db := openTestConn(b) + defer db.Close() + + txn, err := db.Begin() + if err != nil { + b.Fatal(err) + } + defer txn.Rollback() + + _, err = txn.Exec("CREATE TEMP TABLE temp (a int, b varchar)") + if err != nil { + b.Fatal(err) + } + + stmt, err := txn.Prepare(CopyIn("temp", "a", "b")) + if err != nil { + b.Fatal(err) + } + + for i := 0; i < b.N; i++ { + _, err = stmt.Exec(int64(i), "hello world!") + if err != nil { + b.Fatal(err) + } + } + + _, err = stmt.Exec() + if err != nil { + b.Fatal(err) + } + + err = stmt.Close() + if err != nil { + b.Fatal(err) + } + + var num int + err = txn.QueryRow("SELECT COUNT(*) FROM temp").Scan(&num) + if err != nil { + b.Fatal(err) + } + + if num != b.N { + b.Fatalf("expected %d items, not %d", b.N, num) + } +} diff --git a/Godeps/_workspace/src/github.com/lib/pq/doc.go b/Godeps/_workspace/src/github.com/lib/pq/doc.go new file mode 100644 index 0000000..f772117 --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/doc.go @@ -0,0 +1,210 @@ +/* +Package pq is a pure Go Postgres driver for the database/sql package. + +In most cases clients will use the database/sql package instead of +using this package directly. For example: + + import ( + "database/sql" + + _ "github.com/lib/pq" + ) + + func main() { + db, err := sql.Open("postgres", "user=pqgotest dbname=pqgotest sslmode=verify-full") + if err != nil { + log.Fatal(err) + } + + age := 21 + rows, err := db.Query("SELECT name FROM users WHERE age = $1", age) + … + } + +You can also connect to a database using a URL. For example: + + db, err := sql.Open("postgres", "postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full") + + +Connection String Parameters + + +Similarly to libpq, when establishing a connection using pq you are expected to +supply a connection string containing zero or more parameters. +A subset of the connection parameters supported by libpq are also supported by pq. +Additionally, pq also lets you specify run-time parameters (such as search_path or work_mem) +directly in the connection string. This is different from libpq, which does not allow +run-time parameters in the connection string, instead requiring you to supply +them in the options parameter. + +For compatibility with libpq, the following special connection parameters are +supported: + + * dbname - The name of the database to connect to + * user - The user to sign in as + * password - The user's password + * host - The host to connect to. Values that start with / are for unix domain sockets. (default is localhost) + * port - The port to bind to. (default is 5432) + * sslmode - Whether or not to use SSL (default is require, this is not the default for libpq) + * fallback_application_name - An application_name to fall back to if one isn't provided. + * connect_timeout - Maximum wait for connection, in seconds. Zero or not specified means wait indefinitely. + * sslcert - Cert file location. The file must contain PEM encoded data. + * sslkey - Key file location. The file must contain PEM encoded data. + * sslrootcert - The location of the root certificate file. The file must contain PEM encoded data. + +Valid values for sslmode are: + + * disable - No SSL + * require - Always SSL (skip verification) + * verify-ca - Always SSL (verify that the certificate presented by the server was signed by a trusted CA) + * verify-full - Always SSL (verify that the certification presented by the server was signed by a trusted CA and the server host name matches the one in the certificate) + +See http://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING +for more information about connection string parameters. + +Use single quotes for values that contain whitespace: + + "user=pqgotest password='with spaces'" + +A backslash will escape the next character in values: + + "user=space\ man password='it\'s valid' + +Note that the connection parameter client_encoding (which sets the +text encoding for the connection) may be set but must be "UTF8", +matching with the same rules as Postgres. It is an error to provide +any other value. + +In addition to the parameters listed above, any run-time parameter that can be +set at backend start time can be set in the connection string. For more +information, see +http://www.postgresql.org/docs/current/static/runtime-config.html. + +Most environment variables as specified at http://www.postgresql.org/docs/current/static/libpq-envars.html +supported by libpq are also supported by pq. If any of the environment +variables not supported by pq are set, pq will panic during connection +establishment. Environment variables have a lower precedence than explicitly +provided connection parameters. + + +Queries + +database/sql does not dictate any specific format for parameter +markers in query strings, and pq uses the Postgres-native ordinal markers, +as shown above. The same marker can be reused for the same parameter: + + rows, err := db.Query(`SELECT name FROM users WHERE favorite_fruit = $1 + OR age BETWEEN $2 AND $2 + 3`, "orange", 64) + +pq does not support the LastInsertId() method of the Result type in database/sql. +To return the identifier of an INSERT (or UPDATE or DELETE), use the Postgres +RETURNING clause with a standard Query or QueryRow call: + + var userid int + err := db.QueryRow(`INSERT INTO users(name, favorite_fruit, age) + VALUES('beatrice', 'starfruit', 93) RETURNING id`).Scan(&userid) + +For more details on RETURNING, see the Postgres documentation: + + http://www.postgresql.org/docs/current/static/sql-insert.html + http://www.postgresql.org/docs/current/static/sql-update.html + http://www.postgresql.org/docs/current/static/sql-delete.html + +For additional instructions on querying see the documentation for the database/sql package. + +Errors + +pq may return errors of type *pq.Error which can be interrogated for error details: + + if err, ok := err.(*pq.Error); ok { + fmt.Println("pq error:", err.Code.Name()) + } + +See the pq.Error type for details. + + +Bulk imports + +You can perform bulk imports by preparing a statement returned by pq.CopyIn (or +pq.CopyInSchema) in an explicit transaction (sql.Tx). The returned statement +handle can then be repeatedly "executed" to copy data into the target table. +After all data has been processed you should call Exec() once with no arguments +to flush all buffered data. Any call to Exec() might return an error which +should be handled appropriately, but because of the internal buffering an error +returned by Exec() might not be related to the data passed in the call that +failed. + +CopyIn uses COPY FROM internally. It is not possible to COPY outside of an +explicit transaction in pq. + +Usage example: + + txn, err := db.Begin() + if err != nil { + log.Fatal(err) + } + + stmt, err := txn.Prepare(pq.CopyIn("users", "name", "age")) + if err != nil { + log.Fatal(err) + } + + for _, user := range users { + _, err = stmt.Exec(user.Name, int64(user.Age)) + if err != nil { + log.Fatal(err) + } + } + + _, err = stmt.Exec() + if err != nil { + log.Fatal(err) + } + + err = stmt.Close() + if err != nil { + log.Fatal(err) + } + + err = txn.Commit() + if err != nil { + log.Fatal(err) + } + + +Notifications + + +PostgreSQL supports a simple publish/subscribe model over database +connections. See http://www.postgresql.org/docs/current/static/sql-notify.html +for more information about the general mechanism. + +To start listening for notifications, you first have to open a new connection +to the database by calling NewListener. This connection can not be used for +anything other than LISTEN / NOTIFY. Calling Listen will open a "notification +channel"; once a notification channel is open, a notification generated on that +channel will effect a send on the Listener.Notify channel. A notification +channel will remain open until Unlisten is called, though connection loss might +result in some notifications being lost. To solve this problem, Listener sends +a nil pointer over the Notify channel any time the connection is re-established +following a connection loss. The application can get information about the +state of the underlying connection by setting an event callback in the call to +NewListener. + +A single Listener can safely be used from concurrent goroutines, which means +that there is often no need to create more than one Listener in your +application. However, a Listener is always connected to a single database, so +you will need to create a new Listener instance for every database you want to +receive notifications in. + +The channel name in both Listen and Unlisten is case sensitive, and can contain +any characters legal in an identifier (see +http://www.postgresql.org/docs/current/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS +for more information). Note that the channel name will be truncated to 63 +bytes by the PostgreSQL server. + +You can find a complete, working example of Listener usage at +http://godoc.org/github.com/lib/pq/listen_example. + +*/ +package pq diff --git a/Godeps/_workspace/src/github.com/lib/pq/encode.go b/Godeps/_workspace/src/github.com/lib/pq/encode.go new file mode 100644 index 0000000..88422eb --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/encode.go @@ -0,0 +1,538 @@ +package pq + +import ( + "bytes" + "database/sql/driver" + "encoding/binary" + "encoding/hex" + "fmt" + "math" + "strconv" + "strings" + "sync" + "time" + + "github.com/lib/pq/oid" +) + +func binaryEncode(parameterStatus *parameterStatus, x interface{}) []byte { + switch v := x.(type) { + case []byte: + return v + default: + return encode(parameterStatus, x, oid.T_unknown) + } + panic("not reached") +} + +func encode(parameterStatus *parameterStatus, x interface{}, pgtypOid oid.Oid) []byte { + switch v := x.(type) { + case int64: + return strconv.AppendInt(nil, v, 10) + case float64: + return strconv.AppendFloat(nil, v, 'f', -1, 64) + case []byte: + if pgtypOid == oid.T_bytea { + return encodeBytea(parameterStatus.serverVersion, v) + } + + return v + case string: + if pgtypOid == oid.T_bytea { + return encodeBytea(parameterStatus.serverVersion, []byte(v)) + } + + return []byte(v) + case bool: + return strconv.AppendBool(nil, v) + case time.Time: + return formatTs(v) + + default: + errorf("encode: unknown type for %T", v) + } + + panic("not reached") +} + +func decode(parameterStatus *parameterStatus, s []byte, typ oid.Oid, f format) interface{} { + if f == formatBinary { + return binaryDecode(parameterStatus, s, typ) + } else { + return textDecode(parameterStatus, s, typ) + } +} + +func binaryDecode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{} { + switch typ { + case oid.T_bytea: + return s + case oid.T_int8: + return int64(binary.BigEndian.Uint64(s)) + case oid.T_int4: + return int64(int32(binary.BigEndian.Uint32(s))) + case oid.T_int2: + return int64(int16(binary.BigEndian.Uint16(s))) + + default: + errorf("don't know how to decode binary parameter of type %u", uint32(typ)) + } + + panic("not reached") +} + +func textDecode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{} { + switch typ { + case oid.T_bytea: + return parseBytea(s) + case oid.T_timestamptz: + return parseTs(parameterStatus.currentLocation, string(s)) + case oid.T_timestamp, oid.T_date: + return parseTs(nil, string(s)) + case oid.T_time: + return mustParse("15:04:05", typ, s) + case oid.T_timetz: + return mustParse("15:04:05-07", typ, s) + case oid.T_bool: + return s[0] == 't' + case oid.T_int8, oid.T_int4, oid.T_int2: + i, err := strconv.ParseInt(string(s), 10, 64) + if err != nil { + errorf("%s", err) + } + return i + case oid.T_float4, oid.T_float8: + bits := 64 + if typ == oid.T_float4 { + bits = 32 + } + f, err := strconv.ParseFloat(string(s), bits) + if err != nil { + errorf("%s", err) + } + return f + } + + return s +} + +// appendEncodedText encodes item in text format as required by COPY +// and appends to buf +func appendEncodedText(parameterStatus *parameterStatus, buf []byte, x interface{}) []byte { + switch v := x.(type) { + case int64: + return strconv.AppendInt(buf, v, 10) + case float64: + return strconv.AppendFloat(buf, v, 'f', -1, 64) + case []byte: + encodedBytea := encodeBytea(parameterStatus.serverVersion, v) + return appendEscapedText(buf, string(encodedBytea)) + case string: + return appendEscapedText(buf, v) + case bool: + return strconv.AppendBool(buf, v) + case time.Time: + return append(buf, formatTs(v)...) + case nil: + return append(buf, "\\N"...) + default: + errorf("encode: unknown type for %T", v) + } + + panic("not reached") +} + +func appendEscapedText(buf []byte, text string) []byte { + escapeNeeded := false + startPos := 0 + var c byte + + // check if we need to escape + for i := 0; i < len(text); i++ { + c = text[i] + if c == '\\' || c == '\n' || c == '\r' || c == '\t' { + escapeNeeded = true + startPos = i + break + } + } + if !escapeNeeded { + return append(buf, text...) + } + + // copy till first char to escape, iterate the rest + result := append(buf, text[:startPos]...) + for i := startPos; i < len(text); i++ { + c = text[i] + switch c { + case '\\': + result = append(result, '\\', '\\') + case '\n': + result = append(result, '\\', 'n') + case '\r': + result = append(result, '\\', 'r') + case '\t': + result = append(result, '\\', 't') + default: + result = append(result, c) + } + } + return result +} + +func mustParse(f string, typ oid.Oid, s []byte) time.Time { + str := string(s) + + // check for a 30-minute-offset timezone + if (typ == oid.T_timestamptz || typ == oid.T_timetz) && + str[len(str)-3] == ':' { + f += ":00" + } + t, err := time.Parse(f, str) + if err != nil { + errorf("decode: %s", err) + } + return t +} + +func expect(str, char string, pos int) { + if c := str[pos : pos+1]; c != char { + errorf("expected '%v' at position %v; got '%v'", char, pos, c) + } +} + +func mustAtoi(str string) int { + result, err := strconv.Atoi(str) + if err != nil { + errorf("expected number; got '%v'", str) + } + return result +} + +// The location cache caches the time zones typically used by the client. +type locationCache struct { + cache map[int]*time.Location + lock sync.Mutex +} + +// All connections share the same list of timezones. Benchmarking shows that +// about 5% speed could be gained by putting the cache in the connection and +// losing the mutex, at the cost of a small amount of memory and a somewhat +// significant increase in code complexity. +var globalLocationCache *locationCache = newLocationCache() + +func newLocationCache() *locationCache { + return &locationCache{cache: make(map[int]*time.Location)} +} + +// Returns the cached timezone for the specified offset, creating and caching +// it if necessary. +func (c *locationCache) getLocation(offset int) *time.Location { + c.lock.Lock() + defer c.lock.Unlock() + + location, ok := c.cache[offset] + if !ok { + location = time.FixedZone("", offset) + c.cache[offset] = location + } + + return location +} + +var infinityTsEnabled = false +var infinityTsNegative time.Time +var infinityTsPositive time.Time + +const ( + infinityTsEnabledAlready = "pq: infinity timestamp enabled already" + infinityTsNegativeMustBeSmaller = "pq: infinity timestamp: negative value must be smaller (before) than positive" +) + +/* + * If EnableInfinityTs is not called, "-infinity" and "infinity" will return + * []byte("-infinity") and []byte("infinity") respectively, and potentially + * cause error "sql: Scan error on column index 0: unsupported driver -> Scan pair: []uint8 -> *time.Time", + * when scanning into a time.Time value. + * + * Once EnableInfinityTs has been called, all connections created using this + * driver will decode Postgres' "-infinity" and "infinity" for "timestamp", + * "timestamp with time zone" and "date" types to the predefined minimum and + * maximum times, respectively. When encoding time.Time values, any time which + * equals or preceeds the predefined minimum time will be encoded to + * "-infinity". Any values at or past the maximum time will similarly be + * encoded to "infinity". + * + * + * If EnableInfinityTs is called with negative >= positive, it will panic. + * Calling EnableInfinityTs after a connection has been established results in + * undefined behavior. If EnableInfinityTs is called more than once, it will + * panic. + */ +func EnableInfinityTs(negative time.Time, positive time.Time) { + if infinityTsEnabled { + panic(infinityTsEnabledAlready) + } + if !negative.Before(positive) { + panic(infinityTsNegativeMustBeSmaller) + } + infinityTsEnabled = true + infinityTsNegative = negative + infinityTsPositive = positive +} + +/* + * Testing might want to toggle infinityTsEnabled + */ +func disableInfinityTs() { + infinityTsEnabled = false +} + +// This is a time function specific to the Postgres default DateStyle +// setting ("ISO, MDY"), the only one we currently support. This +// accounts for the discrepancies between the parsing available with +// time.Parse and the Postgres date formatting quirks. +func parseTs(currentLocation *time.Location, str string) interface{} { + switch str { + case "-infinity": + if infinityTsEnabled { + return infinityTsNegative + } + return []byte(str) + case "infinity": + if infinityTsEnabled { + return infinityTsPositive + } + return []byte(str) + } + + monSep := strings.IndexRune(str, '-') + // this is Gregorian year, not ISO Year + // In Gregorian system, the year 1 BC is followed by AD 1 + year := mustAtoi(str[:monSep]) + daySep := monSep + 3 + month := mustAtoi(str[monSep+1 : daySep]) + expect(str, "-", daySep) + timeSep := daySep + 3 + day := mustAtoi(str[daySep+1 : timeSep]) + + var hour, minute, second int + if len(str) > monSep+len("01-01")+1 { + expect(str, " ", timeSep) + minSep := timeSep + 3 + expect(str, ":", minSep) + hour = mustAtoi(str[timeSep+1 : minSep]) + secSep := minSep + 3 + expect(str, ":", secSep) + minute = mustAtoi(str[minSep+1 : secSep]) + secEnd := secSep + 3 + second = mustAtoi(str[secSep+1 : secEnd]) + } + remainderIdx := monSep + len("01-01 00:00:00") + 1 + // Three optional (but ordered) sections follow: the + // fractional seconds, the time zone offset, and the BC + // designation. We set them up here and adjust the other + // offsets if the preceding sections exist. + + nanoSec := 0 + tzOff := 0 + + if remainderIdx < len(str) && str[remainderIdx:remainderIdx+1] == "." { + fracStart := remainderIdx + 1 + fracOff := strings.IndexAny(str[fracStart:], "-+ ") + if fracOff < 0 { + fracOff = len(str) - fracStart + } + fracSec := mustAtoi(str[fracStart : fracStart+fracOff]) + nanoSec = fracSec * (1000000000 / int(math.Pow(10, float64(fracOff)))) + + remainderIdx += fracOff + 1 + } + if tzStart := remainderIdx; tzStart < len(str) && (str[tzStart:tzStart+1] == "-" || str[tzStart:tzStart+1] == "+") { + // time zone separator is always '-' or '+' (UTC is +00) + var tzSign int + if c := str[tzStart : tzStart+1]; c == "-" { + tzSign = -1 + } else if c == "+" { + tzSign = +1 + } else { + errorf("expected '-' or '+' at position %v; got %v", tzStart, c) + } + tzHours := mustAtoi(str[tzStart+1 : tzStart+3]) + remainderIdx += 3 + var tzMin, tzSec int + if tzStart+3 < len(str) && str[tzStart+3:tzStart+4] == ":" { + tzMin = mustAtoi(str[tzStart+4 : tzStart+6]) + remainderIdx += 3 + } + if tzStart+6 < len(str) && str[tzStart+6:tzStart+7] == ":" { + tzSec = mustAtoi(str[tzStart+7 : tzStart+9]) + remainderIdx += 3 + } + tzOff = tzSign * ((tzHours * 60 * 60) + (tzMin * 60) + tzSec) + } + var isoYear int + if remainderIdx < len(str) && str[remainderIdx:remainderIdx+3] == " BC" { + isoYear = 1 - year + remainderIdx += 3 + } else { + isoYear = year + } + if remainderIdx < len(str) { + errorf("expected end of input, got %v", str[remainderIdx:]) + } + t := time.Date(isoYear, time.Month(month), day, + hour, minute, second, nanoSec, + globalLocationCache.getLocation(tzOff)) + + if currentLocation != nil { + // Set the location of the returned Time based on the session's + // TimeZone value, but only if the local time zone database agrees with + // the remote database on the offset. + lt := t.In(currentLocation) + _, newOff := lt.Zone() + if newOff == tzOff { + t = lt + } + } + + return t +} + +// formatTs formats t into a format postgres understands. +func formatTs(t time.Time) (b []byte) { + if infinityTsEnabled { + // t <= -infinity : ! (t > -infinity) + if !t.After(infinityTsNegative) { + return []byte("-infinity") + } + // t >= infinity : ! (!t < infinity) + if !t.Before(infinityTsPositive) { + return []byte("infinity") + } + } + // Need to send dates before 0001 A.D. with " BC" suffix, instead of the + // minus sign preferred by Go. + // Beware, "0000" in ISO is "1 BC", "-0001" is "2 BC" and so on + bc := false + if t.Year() <= 0 { + // flip year sign, and add 1, e.g: "0" will be "1", and "-10" will be "11" + t = t.AddDate((-t.Year())*2+1, 0, 0) + bc = true + } + b = []byte(t.Format(time.RFC3339Nano)) + + _, offset := t.Zone() + offset = offset % 60 + if offset != 0 { + // RFC3339Nano already printed the minus sign + if offset < 0 { + offset = -offset + } + + b = append(b, ':') + if offset < 10 { + b = append(b, '0') + } + b = strconv.AppendInt(b, int64(offset), 10) + } + + if bc { + b = append(b, " BC"...) + } + return b +} + +// Parse a bytea value received from the server. Both "hex" and the legacy +// "escape" format are supported. +func parseBytea(s []byte) (result []byte) { + if len(s) >= 2 && bytes.Equal(s[:2], []byte("\\x")) { + // bytea_output = hex + s = s[2:] // trim off leading "\\x" + result = make([]byte, hex.DecodedLen(len(s))) + _, err := hex.Decode(result, s) + if err != nil { + errorf("%s", err) + } + } else { + // bytea_output = escape + for len(s) > 0 { + if s[0] == '\\' { + // escaped '\\' + if len(s) >= 2 && s[1] == '\\' { + result = append(result, '\\') + s = s[2:] + continue + } + + // '\\' followed by an octal number + if len(s) < 4 { + errorf("invalid bytea sequence %v", s) + } + r, err := strconv.ParseInt(string(s[1:4]), 8, 9) + if err != nil { + errorf("could not parse bytea value: %s", err.Error()) + } + result = append(result, byte(r)) + s = s[4:] + } else { + // We hit an unescaped, raw byte. Try to read in as many as + // possible in one go. + i := bytes.IndexByte(s, '\\') + if i == -1 { + result = append(result, s...) + break + } + result = append(result, s[:i]...) + s = s[i:] + } + } + } + + return result +} + +func encodeBytea(serverVersion int, v []byte) (result []byte) { + if serverVersion >= 90000 { + // Use the hex format if we know that the server supports it + result = make([]byte, 2+hex.EncodedLen(len(v))) + result[0] = '\\' + result[1] = 'x' + hex.Encode(result[2:], v) + } else { + // .. or resort to "escape" + for _, b := range v { + if b == '\\' { + result = append(result, '\\', '\\') + } else if b < 0x20 || b > 0x7e { + result = append(result, []byte(fmt.Sprintf("\\%03o", b))...) + } else { + result = append(result, b) + } + } + } + + return result +} + +// NullTime represents a time.Time that may be null. NullTime implements the +// sql.Scanner interface so it can be used as a scan destination, similar to +// sql.NullString. +type NullTime struct { + Time time.Time + Valid bool // Valid is true if Time is not NULL +} + +// Scan implements the Scanner interface. +func (nt *NullTime) Scan(value interface{}) error { + nt.Time, nt.Valid = value.(time.Time) + return nil +} + +// Value implements the driver Valuer interface. +func (nt NullTime) Value() (driver.Value, error) { + if !nt.Valid { + return nil, nil + } + return nt.Time, nil +} diff --git a/Godeps/_workspace/src/github.com/lib/pq/encode_test.go b/Godeps/_workspace/src/github.com/lib/pq/encode_test.go new file mode 100644 index 0000000..97b6638 --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/encode_test.go @@ -0,0 +1,719 @@ +package pq + +import ( + "bytes" + "database/sql" + "fmt" + "testing" + "time" + + "github.com/lib/pq/oid" +) + +func TestScanTimestamp(t *testing.T) { + var nt NullTime + tn := time.Now() + nt.Scan(tn) + if !nt.Valid { + t.Errorf("Expected Valid=false") + } + if nt.Time != tn { + t.Errorf("Time value mismatch") + } +} + +func TestScanNilTimestamp(t *testing.T) { + var nt NullTime + nt.Scan(nil) + if nt.Valid { + t.Errorf("Expected Valid=false") + } +} + +var timeTests = []struct { + str string + timeval time.Time +}{ + {"22001-02-03", time.Date(22001, time.February, 3, 0, 0, 0, 0, time.FixedZone("", 0))}, + {"2001-02-03", time.Date(2001, time.February, 3, 0, 0, 0, 0, time.FixedZone("", 0))}, + {"2001-02-03 04:05:06", time.Date(2001, time.February, 3, 4, 5, 6, 0, time.FixedZone("", 0))}, + {"2001-02-03 04:05:06.000001", time.Date(2001, time.February, 3, 4, 5, 6, 1000, time.FixedZone("", 0))}, + {"2001-02-03 04:05:06.00001", time.Date(2001, time.February, 3, 4, 5, 6, 10000, time.FixedZone("", 0))}, + {"2001-02-03 04:05:06.0001", time.Date(2001, time.February, 3, 4, 5, 6, 100000, time.FixedZone("", 0))}, + {"2001-02-03 04:05:06.001", time.Date(2001, time.February, 3, 4, 5, 6, 1000000, time.FixedZone("", 0))}, + {"2001-02-03 04:05:06.01", time.Date(2001, time.February, 3, 4, 5, 6, 10000000, time.FixedZone("", 0))}, + {"2001-02-03 04:05:06.1", time.Date(2001, time.February, 3, 4, 5, 6, 100000000, time.FixedZone("", 0))}, + {"2001-02-03 04:05:06.12", time.Date(2001, time.February, 3, 4, 5, 6, 120000000, time.FixedZone("", 0))}, + {"2001-02-03 04:05:06.123", time.Date(2001, time.February, 3, 4, 5, 6, 123000000, time.FixedZone("", 0))}, + {"2001-02-03 04:05:06.1234", time.Date(2001, time.February, 3, 4, 5, 6, 123400000, time.FixedZone("", 0))}, + {"2001-02-03 04:05:06.12345", time.Date(2001, time.February, 3, 4, 5, 6, 123450000, time.FixedZone("", 0))}, + {"2001-02-03 04:05:06.123456", time.Date(2001, time.February, 3, 4, 5, 6, 123456000, time.FixedZone("", 0))}, + {"2001-02-03 04:05:06.123-07", time.Date(2001, time.February, 3, 4, 5, 6, 123000000, + time.FixedZone("", -7*60*60))}, + {"2001-02-03 04:05:06-07", time.Date(2001, time.February, 3, 4, 5, 6, 0, + time.FixedZone("", -7*60*60))}, + {"2001-02-03 04:05:06-07:42", time.Date(2001, time.February, 3, 4, 5, 6, 0, + time.FixedZone("", -(7*60*60+42*60)))}, + {"2001-02-03 04:05:06-07:30:09", time.Date(2001, time.February, 3, 4, 5, 6, 0, + time.FixedZone("", -(7*60*60+30*60+9)))}, + {"2001-02-03 04:05:06+07", time.Date(2001, time.February, 3, 4, 5, 6, 0, + time.FixedZone("", 7*60*60))}, + {"0011-02-03 04:05:06 BC", time.Date(-10, time.February, 3, 4, 5, 6, 0, time.FixedZone("", 0))}, + {"0011-02-03 04:05:06.123 BC", time.Date(-10, time.February, 3, 4, 5, 6, 123000000, time.FixedZone("", 0))}, + {"0011-02-03 04:05:06.123-07 BC", time.Date(-10, time.February, 3, 4, 5, 6, 123000000, + time.FixedZone("", -7*60*60))}, + {"0001-02-03 04:05:06.123", time.Date(1, time.February, 3, 4, 5, 6, 123000000, time.FixedZone("", 0))}, + {"0001-02-03 04:05:06.123 BC", time.Date(1, time.February, 3, 4, 5, 6, 123000000, time.FixedZone("", 0)).AddDate(-1, 0, 0)}, + {"0001-02-03 04:05:06.123 BC", time.Date(0, time.February, 3, 4, 5, 6, 123000000, time.FixedZone("", 0))}, + {"0002-02-03 04:05:06.123 BC", time.Date(0, time.February, 3, 4, 5, 6, 123000000, time.FixedZone("", 0)).AddDate(-1, 0, 0)}, + {"0002-02-03 04:05:06.123 BC", time.Date(-1, time.February, 3, 4, 5, 6, 123000000, time.FixedZone("", 0))}, + {"12345-02-03 04:05:06.1", time.Date(12345, time.February, 3, 4, 5, 6, 100000000, time.FixedZone("", 0))}, + {"123456-02-03 04:05:06.1", time.Date(123456, time.February, 3, 4, 5, 6, 100000000, time.FixedZone("", 0))}, +} + +// Helper function for the two tests below +func tryParse(str string) (t time.Time, err error) { + defer func() { + if p := recover(); p != nil { + err = fmt.Errorf("%v", p) + return + } + }() + i := parseTs(nil, str) + t, ok := i.(time.Time) + if !ok { + err = fmt.Errorf("Not a time.Time type, got %#v", i) + } + return +} + +// Test that parsing the string results in the expected value. +func TestParseTs(t *testing.T) { + for i, tt := range timeTests { + val, err := tryParse(tt.str) + if err != nil { + t.Errorf("%d: got error: %v", i, err) + } else if val.String() != tt.timeval.String() { + t.Errorf("%d: expected to parse %q into %q; got %q", + i, tt.str, tt.timeval, val) + } + } +} + +// Now test that sending the value into the database and parsing it back +// returns the same time.Time value. +func TestEncodeAndParseTs(t *testing.T) { + db, err := openTestConnConninfo("timezone='Etc/UTC'") + if err != nil { + t.Fatal(err) + } + defer db.Close() + + for i, tt := range timeTests { + var dbstr string + err = db.QueryRow("SELECT ($1::timestamptz)::text", tt.timeval).Scan(&dbstr) + if err != nil { + t.Errorf("%d: could not send value %q to the database: %s", i, tt.timeval, err) + continue + } + + val, err := tryParse(dbstr) + if err != nil { + t.Errorf("%d: could not parse value %q: %s", i, dbstr, err) + continue + } + val = val.In(tt.timeval.Location()) + if val.String() != tt.timeval.String() { + t.Errorf("%d: expected to parse %q into %q; got %q", i, dbstr, tt.timeval, val) + } + } +} + +var formatTimeTests = []struct { + time time.Time + expected string +}{ + {time.Time{}, "0001-01-01T00:00:00Z"}, + {time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "2001-02-03T04:05:06.123456789Z"}, + {time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "2001-02-03T04:05:06.123456789+02:00"}, + {time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "2001-02-03T04:05:06.123456789-06:00"}, + {time.Date(2001, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "2001-02-03T04:05:06-07:30:09"}, + + {time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "0001-02-03T04:05:06.123456789Z"}, + {time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "0001-02-03T04:05:06.123456789+02:00"}, + {time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "0001-02-03T04:05:06.123456789-06:00"}, + + {time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "0001-02-03T04:05:06.123456789Z BC"}, + {time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "0001-02-03T04:05:06.123456789+02:00 BC"}, + {time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "0001-02-03T04:05:06.123456789-06:00 BC"}, + + {time.Date(1, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "0001-02-03T04:05:06-07:30:09"}, + {time.Date(0, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "0001-02-03T04:05:06-07:30:09 BC"}, +} + +func TestFormatTs(t *testing.T) { + for i, tt := range formatTimeTests { + val := string(formatTs(tt.time)) + if val != tt.expected { + t.Errorf("%d: incorrect time format %q, want %q", i, val, tt.expected) + } + } +} + +func TestTimestampWithTimeZone(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + tx, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer tx.Rollback() + + // try several different locations, all included in Go's zoneinfo.zip + for _, locName := range []string{ + "UTC", + "America/Chicago", + "America/New_York", + "Australia/Darwin", + "Australia/Perth", + } { + loc, err := time.LoadLocation(locName) + if err != nil { + t.Logf("Could not load time zone %s - skipping", locName) + continue + } + + // Postgres timestamps have a resolution of 1 microsecond, so don't + // use the full range of the Nanosecond argument + refTime := time.Date(2012, 11, 6, 10, 23, 42, 123456000, loc) + + for _, pgTimeZone := range []string{"US/Eastern", "Australia/Darwin"} { + // Switch Postgres's timezone to test different output timestamp formats + _, err = tx.Exec(fmt.Sprintf("set time zone '%s'", pgTimeZone)) + if err != nil { + t.Fatal(err) + } + + var gotTime time.Time + row := tx.QueryRow("select $1::timestamp with time zone", refTime) + err = row.Scan(&gotTime) + if err != nil { + t.Fatal(err) + } + + if !refTime.Equal(gotTime) { + t.Errorf("timestamps not equal: %s != %s", refTime, gotTime) + } + + // check that the time zone is set correctly based on TimeZone + pgLoc, err := time.LoadLocation(pgTimeZone) + if err != nil { + t.Logf("Could not load time zone %s - skipping", pgLoc) + continue + } + translated := refTime.In(pgLoc) + if translated.String() != gotTime.String() { + t.Errorf("timestamps not equal: %s != %s", translated, gotTime) + } + } + } +} + +func TestTimestampWithOutTimezone(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + test := func(ts, pgts string) { + r, err := db.Query("SELECT $1::timestamp", pgts) + if err != nil { + t.Fatalf("Could not run query: %v", err) + } + + n := r.Next() + + if n != true { + t.Fatal("Expected at least one row") + } + + var result time.Time + err = r.Scan(&result) + if err != nil { + t.Fatalf("Did not expect error scanning row: %v", err) + } + + expected, err := time.Parse(time.RFC3339, ts) + if err != nil { + t.Fatalf("Could not parse test time literal: %v", err) + } + + if !result.Equal(expected) { + t.Fatalf("Expected time to match %v: got mismatch %v", + expected, result) + } + + n = r.Next() + if n != false { + t.Fatal("Expected only one row") + } + } + + test("2000-01-01T00:00:00Z", "2000-01-01T00:00:00") + + // Test higher precision time + test("2013-01-04T20:14:58.80033Z", "2013-01-04 20:14:58.80033") +} + +func TestInfinityTimestamp(t *testing.T) { + db := openTestConn(t) + defer db.Close() + var err error + var resultT time.Time + + expectedError := fmt.Errorf(`sql: Scan error on column index 0: unsupported driver -> Scan pair: []uint8 -> *time.Time`) + type testCases []struct { + Query string + Param string + ExpectedErr error + ExpectedVal interface{} + } + tc := testCases{ + {"SELECT $1::timestamp", "-infinity", expectedError, "-infinity"}, + {"SELECT $1::timestamptz", "-infinity", expectedError, "-infinity"}, + {"SELECT $1::timestamp", "infinity", expectedError, "infinity"}, + {"SELECT $1::timestamptz", "infinity", expectedError, "infinity"}, + } + // try to assert []byte to time.Time + for _, q := range tc { + err = db.QueryRow(q.Query, q.Param).Scan(&resultT) + if err.Error() != q.ExpectedErr.Error() { + t.Errorf("Scanning -/+infinity, expected error, %q, got %q", q.ExpectedErr, err) + } + } + // yield []byte + for _, q := range tc { + var resultI interface{} + err = db.QueryRow(q.Query, q.Param).Scan(&resultI) + if err != nil { + t.Errorf("Scanning -/+infinity, expected no error, got %q", err) + } + result, ok := resultI.([]byte) + if !ok { + t.Errorf("Scanning -/+infinity, expected []byte, got %#v", resultI) + } + if string(result) != q.ExpectedVal { + t.Errorf("Scanning -/+infinity, expected %q, got %q", q.ExpectedVal, result) + } + } + + y1500 := time.Date(1500, time.January, 1, 0, 0, 0, 0, time.UTC) + y2500 := time.Date(2500, time.January, 1, 0, 0, 0, 0, time.UTC) + EnableInfinityTs(y1500, y2500) + + err = db.QueryRow("SELECT $1::timestamp", "infinity").Scan(&resultT) + if err != nil { + t.Errorf("Scanning infinity, expected no error, got %q", err) + } + if !resultT.Equal(y2500) { + t.Errorf("Scanning infinity, expected %q, got %q", y2500, resultT) + } + + err = db.QueryRow("SELECT $1::timestamptz", "infinity").Scan(&resultT) + if err != nil { + t.Errorf("Scanning infinity, expected no error, got %q", err) + } + if !resultT.Equal(y2500) { + t.Errorf("Scanning Infinity, expected time %q, got %q", y2500, resultT.String()) + } + + err = db.QueryRow("SELECT $1::timestamp", "-infinity").Scan(&resultT) + if err != nil { + t.Errorf("Scanning -infinity, expected no error, got %q", err) + } + if !resultT.Equal(y1500) { + t.Errorf("Scanning -infinity, expected time %q, got %q", y1500, resultT.String()) + } + + err = db.QueryRow("SELECT $1::timestamptz", "-infinity").Scan(&resultT) + if err != nil { + t.Errorf("Scanning -infinity, expected no error, got %q", err) + } + if !resultT.Equal(y1500) { + t.Errorf("Scanning -infinity, expected time %q, got %q", y1500, resultT.String()) + } + + y_1500 := time.Date(-1500, time.January, 1, 0, 0, 0, 0, time.UTC) + y11500 := time.Date(11500, time.January, 1, 0, 0, 0, 0, time.UTC) + var s string + err = db.QueryRow("SELECT $1::timestamp::text", y_1500).Scan(&s) + if err != nil { + t.Errorf("Encoding -infinity, expected no error, got %q", err) + } + if s != "-infinity" { + t.Errorf("Encoding -infinity, expected %q, got %q", "-infinity", s) + } + err = db.QueryRow("SELECT $1::timestamptz::text", y_1500).Scan(&s) + if err != nil { + t.Errorf("Encoding -infinity, expected no error, got %q", err) + } + if s != "-infinity" { + t.Errorf("Encoding -infinity, expected %q, got %q", "-infinity", s) + } + + err = db.QueryRow("SELECT $1::timestamp::text", y11500).Scan(&s) + if err != nil { + t.Errorf("Encoding infinity, expected no error, got %q", err) + } + if s != "infinity" { + t.Errorf("Encoding infinity, expected %q, got %q", "infinity", s) + } + err = db.QueryRow("SELECT $1::timestamptz::text", y11500).Scan(&s) + if err != nil { + t.Errorf("Encoding infinity, expected no error, got %q", err) + } + if s != "infinity" { + t.Errorf("Encoding infinity, expected %q, got %q", "infinity", s) + } + + disableInfinityTs() + + var panicErrorString string + func() { + defer func() { + panicErrorString, _ = recover().(string) + }() + EnableInfinityTs(y2500, y1500) + }() + if panicErrorString != infinityTsNegativeMustBeSmaller { + t.Errorf("Expected error, %q, got %q", infinityTsNegativeMustBeSmaller, panicErrorString) + } +} + +func TestStringWithNul(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + hello0world := string("hello\x00world") + _, err := db.Query("SELECT $1::text", &hello0world) + if err == nil { + t.Fatal("Postgres accepts a string with nul in it; " + + "injection attacks may be plausible") + } +} + +func TestByteSliceToText(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + b := []byte("hello world") + row := db.QueryRow("SELECT $1::text", b) + + var result []byte + err := row.Scan(&result) + if err != nil { + t.Fatal(err) + } + + if string(result) != string(b) { + t.Fatalf("expected %v but got %v", b, result) + } +} + +func TestStringToBytea(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + b := "hello world" + row := db.QueryRow("SELECT $1::bytea", b) + + var result []byte + err := row.Scan(&result) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(result, []byte(b)) { + t.Fatalf("expected %v but got %v", b, result) + } +} + +func TestTextByteSliceToUUID(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + b := []byte("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11") + row := db.QueryRow("SELECT $1::uuid", b) + + var result string + err := row.Scan(&result) + if forceBinaryParameters() { + pqErr := err.(*Error) + if pqErr == nil { + t.Errorf("Expected to get error") + } else if pqErr.Code != "22P03" { + t.Fatalf("Expected to get invalid binary encoding error (22P03), got %s", pqErr.Code) + } + } else { + if err != nil { + t.Fatal(err) + } + + if result != string(b) { + t.Fatalf("expected %v but got %v", b, result) + } + } +} + +func TestBinaryByteSlicetoUUID(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + b := []byte{'\xa0','\xee','\xbc','\x99', + '\x9c', '\x0b', + '\x4e', '\xf8', + '\xbb', '\x00', '\x6b', + '\xb9', '\xbd', '\x38', '\x0a', '\x11'} + row := db.QueryRow("SELECT $1::uuid", b) + + var result string + err := row.Scan(&result) + if forceBinaryParameters() { + if err != nil { + t.Fatal(err) + } + + if result != string("a0eebc99-9c0b-4ef8-bb00-6bb9bd380a11") { + t.Fatalf("expected %v but got %v", b, result) + } + } else { + pqErr := err.(*Error) + if pqErr == nil { + t.Errorf("Expected to get error") + } else if pqErr.Code != "22021" { + t.Fatalf("Expected to get invalid byte sequence for encoding error (22021), got %s", pqErr.Code) + } + } +} + +func TestStringToUUID(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + s := "a0eebc99-9c0b-4ef8-bb00-6bb9bd380a11" + row := db.QueryRow("SELECT $1::uuid", s) + + var result string + err := row.Scan(&result) + if err != nil { + t.Fatal(err) + } + + if result != s { + t.Fatalf("expected %v but got %v", s, result) + } +} + +func TestTextByteSliceToInt(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + expected := 12345678 + b := []byte(fmt.Sprintf("%d", expected)) + row := db.QueryRow("SELECT $1::int", b) + + var result int + err := row.Scan(&result) + if forceBinaryParameters() { + pqErr := err.(*Error) + if pqErr == nil { + t.Errorf("Expected to get error") + } else if pqErr.Code != "22P03" { + t.Fatalf("Expected to get invalid binary encoding error (22P03), got %s", pqErr.Code) + } + } else { + if err != nil { + t.Fatal(err) + } + if result != expected { + t.Fatalf("expected %v but got %v", expected, result) + } + } +} + +func TestBinaryByteSliceToInt(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + expected := 12345678 + b := []byte{'\x00', '\xbc', '\x61', '\x4e'} + row := db.QueryRow("SELECT $1::int", b) + + var result int + err := row.Scan(&result) + if forceBinaryParameters() { + if err != nil { + t.Fatal(err) + } + if result != expected { + t.Fatalf("expected %v but got %v", expected, result) + } + } else { + pqErr := err.(*Error) + if pqErr == nil { + t.Errorf("Expected to get error") + } else if pqErr.Code != "22021" { + t.Fatalf("Expected to get invalid byte sequence for encoding error (22021), got %s", pqErr.Code) + } + } +} + +func TestByteaOutputFormatEncoding(t *testing.T) { + input := []byte("\\x\x00\x01\x02\xFF\xFEabcdefg0123") + want := []byte("\\x5c78000102fffe6162636465666730313233") + got := encode(¶meterStatus{serverVersion: 90000}, input, oid.T_bytea) + if !bytes.Equal(want, got) { + t.Errorf("invalid hex bytea output, got %v but expected %v", got, want) + } + + want = []byte("\\\\x\\000\\001\\002\\377\\376abcdefg0123") + got = encode(¶meterStatus{serverVersion: 84000}, input, oid.T_bytea) + if !bytes.Equal(want, got) { + t.Errorf("invalid escape bytea output, got %v but expected %v", got, want) + } +} + +func TestByteaOutputFormats(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + if getServerVersion(t, db) < 90000 { + // skip + return + } + + testByteaOutputFormat := func(f string, usePrepared bool) { + expectedData := []byte("\x5c\x78\x00\xff\x61\x62\x63\x01\x08") + sqlQuery := "SELECT decode('5c7800ff6162630108', 'hex')" + + var data []byte + + // use a txn to avoid relying on getting the same connection + txn, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer txn.Rollback() + + _, err = txn.Exec("SET LOCAL bytea_output TO " + f) + if err != nil { + t.Fatal(err) + } + var rows *sql.Rows + var stmt *sql.Stmt + if usePrepared { + stmt, err = txn.Prepare(sqlQuery) + if err != nil { + t.Fatal(err) + } + rows, err = stmt.Query() + } else { + // use Query; QueryRow would hide the actual error + rows, err = txn.Query(sqlQuery) + } + if err != nil { + t.Fatal(err) + } + if !rows.Next() { + if rows.Err() != nil { + t.Fatal(rows.Err()) + } + t.Fatal("shouldn't happen") + } + err = rows.Scan(&data) + if err != nil { + t.Fatal(err) + } + err = rows.Close() + if err != nil { + t.Fatal(err) + } + if stmt != nil { + err = stmt.Close() + if err != nil { + t.Fatal(err) + } + } + if !bytes.Equal(data, expectedData) { + t.Errorf("unexpected bytea value %v for format %s; expected %v", data, f, expectedData) + } + } + + testByteaOutputFormat("hex", false) + testByteaOutputFormat("escape", false) + testByteaOutputFormat("hex", true) + testByteaOutputFormat("escape", true) +} + +func TestAppendEncodedText(t *testing.T) { + var buf []byte + + buf = appendEncodedText(¶meterStatus{serverVersion: 90000}, buf, int64(10)) + buf = append(buf, '\t') + buf = appendEncodedText(¶meterStatus{serverVersion: 90000}, buf, 42.0000000001) + buf = append(buf, '\t') + buf = appendEncodedText(¶meterStatus{serverVersion: 90000}, buf, "hello\tworld") + buf = append(buf, '\t') + buf = appendEncodedText(¶meterStatus{serverVersion: 90000}, buf, []byte{0, 128, 255}) + + if string(buf) != "10\t42.0000000001\thello\\tworld\t\\\\x0080ff" { + t.Fatal(string(buf)) + } +} + +func TestAppendEscapedText(t *testing.T) { + if esc := appendEscapedText(nil, "hallo\tescape"); string(esc) != "hallo\\tescape" { + t.Fatal(string(esc)) + } + if esc := appendEscapedText(nil, "hallo\\tescape\n"); string(esc) != "hallo\\\\tescape\\n" { + t.Fatal(string(esc)) + } + if esc := appendEscapedText(nil, "\n\r\t\f"); string(esc) != "\\n\\r\\t\f" { + t.Fatal(string(esc)) + } +} + +func TestAppendEscapedTextExistingBuffer(t *testing.T) { + var buf []byte + buf = []byte("123\t") + if esc := appendEscapedText(buf, "hallo\tescape"); string(esc) != "123\thallo\\tescape" { + t.Fatal(string(esc)) + } + buf = []byte("123\t") + if esc := appendEscapedText(buf, "hallo\\tescape\n"); string(esc) != "123\thallo\\\\tescape\\n" { + t.Fatal(string(esc)) + } + buf = []byte("123\t") + if esc := appendEscapedText(buf, "\n\r\t\f"); string(esc) != "123\t\\n\\r\\t\f" { + t.Fatal(string(esc)) + } +} + +func BenchmarkAppendEscapedText(b *testing.B) { + longString := "" + for i := 0; i < 100; i++ { + longString += "123456789\n" + } + for i := 0; i < b.N; i++ { + appendEscapedText(nil, longString) + } +} + +func BenchmarkAppendEscapedTextNoEscape(b *testing.B) { + longString := "" + for i := 0; i < 100; i++ { + longString += "1234567890" + } + for i := 0; i < b.N; i++ { + appendEscapedText(nil, longString) + } +} diff --git a/Godeps/_workspace/src/github.com/lib/pq/error.go b/Godeps/_workspace/src/github.com/lib/pq/error.go new file mode 100644 index 0000000..b4bb44c --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/error.go @@ -0,0 +1,508 @@ +package pq + +import ( + "database/sql/driver" + "fmt" + "io" + "net" + "runtime" +) + +// Error severities +const ( + Efatal = "FATAL" + Epanic = "PANIC" + Ewarning = "WARNING" + Enotice = "NOTICE" + Edebug = "DEBUG" + Einfo = "INFO" + Elog = "LOG" +) + +// Error represents an error communicating with the server. +// +// See http://www.postgresql.org/docs/current/static/protocol-error-fields.html for details of the fields +type Error struct { + Severity string + Code ErrorCode + Message string + Detail string + Hint string + Position string + InternalPosition string + InternalQuery string + Where string + Schema string + Table string + Column string + DataTypeName string + Constraint string + File string + Line string + Routine string +} + +// ErrorCode is a five-character error code. +type ErrorCode string + +// Name returns a more human friendly rendering of the error code, namely the +// "condition name". +// +// See http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html for +// details. +func (ec ErrorCode) Name() string { + return errorCodeNames[ec] +} + +// ErrorClass is only the class part of an error code. +type ErrorClass string + +// Name returns the condition name of an error class. It is equivalent to the +// condition name of the "standard" error code (i.e. the one having the last +// three characters "000"). +func (ec ErrorClass) Name() string { + return errorCodeNames[ErrorCode(ec+"000")] +} + +// Class returns the error class, e.g. "28". +// +// See http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html for +// details. +func (ec ErrorCode) Class() ErrorClass { + return ErrorClass(ec[0:2]) +} + +// errorCodeNames is a mapping between the five-character error codes and the +// human readable "condition names". It is derived from the list at +// http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html +var errorCodeNames = map[ErrorCode]string{ + // Class 00 - Successful Completion + "00000": "successful_completion", + // Class 01 - Warning + "01000": "warning", + "0100C": "dynamic_result_sets_returned", + "01008": "implicit_zero_bit_padding", + "01003": "null_value_eliminated_in_set_function", + "01007": "privilege_not_granted", + "01006": "privilege_not_revoked", + "01004": "string_data_right_truncation", + "01P01": "deprecated_feature", + // Class 02 - No Data (this is also a warning class per the SQL standard) + "02000": "no_data", + "02001": "no_additional_dynamic_result_sets_returned", + // Class 03 - SQL Statement Not Yet Complete + "03000": "sql_statement_not_yet_complete", + // Class 08 - Connection Exception + "08000": "connection_exception", + "08003": "connection_does_not_exist", + "08006": "connection_failure", + "08001": "sqlclient_unable_to_establish_sqlconnection", + "08004": "sqlserver_rejected_establishment_of_sqlconnection", + "08007": "transaction_resolution_unknown", + "08P01": "protocol_violation", + // Class 09 - Triggered Action Exception + "09000": "triggered_action_exception", + // Class 0A - Feature Not Supported + "0A000": "feature_not_supported", + // Class 0B - Invalid Transaction Initiation + "0B000": "invalid_transaction_initiation", + // Class 0F - Locator Exception + "0F000": "locator_exception", + "0F001": "invalid_locator_specification", + // Class 0L - Invalid Grantor + "0L000": "invalid_grantor", + "0LP01": "invalid_grant_operation", + // Class 0P - Invalid Role Specification + "0P000": "invalid_role_specification", + // Class 0Z - Diagnostics Exception + "0Z000": "diagnostics_exception", + "0Z002": "stacked_diagnostics_accessed_without_active_handler", + // Class 20 - Case Not Found + "20000": "case_not_found", + // Class 21 - Cardinality Violation + "21000": "cardinality_violation", + // Class 22 - Data Exception + "22000": "data_exception", + "2202E": "array_subscript_error", + "22021": "character_not_in_repertoire", + "22008": "datetime_field_overflow", + "22012": "division_by_zero", + "22005": "error_in_assignment", + "2200B": "escape_character_conflict", + "22022": "indicator_overflow", + "22015": "interval_field_overflow", + "2201E": "invalid_argument_for_logarithm", + "22014": "invalid_argument_for_ntile_function", + "22016": "invalid_argument_for_nth_value_function", + "2201F": "invalid_argument_for_power_function", + "2201G": "invalid_argument_for_width_bucket_function", + "22018": "invalid_character_value_for_cast", + "22007": "invalid_datetime_format", + "22019": "invalid_escape_character", + "2200D": "invalid_escape_octet", + "22025": "invalid_escape_sequence", + "22P06": "nonstandard_use_of_escape_character", + "22010": "invalid_indicator_parameter_value", + "22023": "invalid_parameter_value", + "2201B": "invalid_regular_expression", + "2201W": "invalid_row_count_in_limit_clause", + "2201X": "invalid_row_count_in_result_offset_clause", + "22009": "invalid_time_zone_displacement_value", + "2200C": "invalid_use_of_escape_character", + "2200G": "most_specific_type_mismatch", + "22004": "null_value_not_allowed", + "22002": "null_value_no_indicator_parameter", + "22003": "numeric_value_out_of_range", + "22026": "string_data_length_mismatch", + "22001": "string_data_right_truncation", + "22011": "substring_error", + "22027": "trim_error", + "22024": "unterminated_c_string", + "2200F": "zero_length_character_string", + "22P01": "floating_point_exception", + "22P02": "invalid_text_representation", + "22P03": "invalid_binary_representation", + "22P04": "bad_copy_file_format", + "22P05": "untranslatable_character", + "2200L": "not_an_xml_document", + "2200M": "invalid_xml_document", + "2200N": "invalid_xml_content", + "2200S": "invalid_xml_comment", + "2200T": "invalid_xml_processing_instruction", + // Class 23 - Integrity Constraint Violation + "23000": "integrity_constraint_violation", + "23001": "restrict_violation", + "23502": "not_null_violation", + "23503": "foreign_key_violation", + "23505": "unique_violation", + "23514": "check_violation", + "23P01": "exclusion_violation", + // Class 24 - Invalid Cursor State + "24000": "invalid_cursor_state", + // Class 25 - Invalid Transaction State + "25000": "invalid_transaction_state", + "25001": "active_sql_transaction", + "25002": "branch_transaction_already_active", + "25008": "held_cursor_requires_same_isolation_level", + "25003": "inappropriate_access_mode_for_branch_transaction", + "25004": "inappropriate_isolation_level_for_branch_transaction", + "25005": "no_active_sql_transaction_for_branch_transaction", + "25006": "read_only_sql_transaction", + "25007": "schema_and_data_statement_mixing_not_supported", + "25P01": "no_active_sql_transaction", + "25P02": "in_failed_sql_transaction", + // Class 26 - Invalid SQL Statement Name + "26000": "invalid_sql_statement_name", + // Class 27 - Triggered Data Change Violation + "27000": "triggered_data_change_violation", + // Class 28 - Invalid Authorization Specification + "28000": "invalid_authorization_specification", + "28P01": "invalid_password", + // Class 2B - Dependent Privilege Descriptors Still Exist + "2B000": "dependent_privilege_descriptors_still_exist", + "2BP01": "dependent_objects_still_exist", + // Class 2D - Invalid Transaction Termination + "2D000": "invalid_transaction_termination", + // Class 2F - SQL Routine Exception + "2F000": "sql_routine_exception", + "2F005": "function_executed_no_return_statement", + "2F002": "modifying_sql_data_not_permitted", + "2F003": "prohibited_sql_statement_attempted", + "2F004": "reading_sql_data_not_permitted", + // Class 34 - Invalid Cursor Name + "34000": "invalid_cursor_name", + // Class 38 - External Routine Exception + "38000": "external_routine_exception", + "38001": "containing_sql_not_permitted", + "38002": "modifying_sql_data_not_permitted", + "38003": "prohibited_sql_statement_attempted", + "38004": "reading_sql_data_not_permitted", + // Class 39 - External Routine Invocation Exception + "39000": "external_routine_invocation_exception", + "39001": "invalid_sqlstate_returned", + "39004": "null_value_not_allowed", + "39P01": "trigger_protocol_violated", + "39P02": "srf_protocol_violated", + // Class 3B - Savepoint Exception + "3B000": "savepoint_exception", + "3B001": "invalid_savepoint_specification", + // Class 3D - Invalid Catalog Name + "3D000": "invalid_catalog_name", + // Class 3F - Invalid Schema Name + "3F000": "invalid_schema_name", + // Class 40 - Transaction Rollback + "40000": "transaction_rollback", + "40002": "transaction_integrity_constraint_violation", + "40001": "serialization_failure", + "40003": "statement_completion_unknown", + "40P01": "deadlock_detected", + // Class 42 - Syntax Error or Access Rule Violation + "42000": "syntax_error_or_access_rule_violation", + "42601": "syntax_error", + "42501": "insufficient_privilege", + "42846": "cannot_coerce", + "42803": "grouping_error", + "42P20": "windowing_error", + "42P19": "invalid_recursion", + "42830": "invalid_foreign_key", + "42602": "invalid_name", + "42622": "name_too_long", + "42939": "reserved_name", + "42804": "datatype_mismatch", + "42P18": "indeterminate_datatype", + "42P21": "collation_mismatch", + "42P22": "indeterminate_collation", + "42809": "wrong_object_type", + "42703": "undefined_column", + "42883": "undefined_function", + "42P01": "undefined_table", + "42P02": "undefined_parameter", + "42704": "undefined_object", + "42701": "duplicate_column", + "42P03": "duplicate_cursor", + "42P04": "duplicate_database", + "42723": "duplicate_function", + "42P05": "duplicate_prepared_statement", + "42P06": "duplicate_schema", + "42P07": "duplicate_table", + "42712": "duplicate_alias", + "42710": "duplicate_object", + "42702": "ambiguous_column", + "42725": "ambiguous_function", + "42P08": "ambiguous_parameter", + "42P09": "ambiguous_alias", + "42P10": "invalid_column_reference", + "42611": "invalid_column_definition", + "42P11": "invalid_cursor_definition", + "42P12": "invalid_database_definition", + "42P13": "invalid_function_definition", + "42P14": "invalid_prepared_statement_definition", + "42P15": "invalid_schema_definition", + "42P16": "invalid_table_definition", + "42P17": "invalid_object_definition", + // Class 44 - WITH CHECK OPTION Violation + "44000": "with_check_option_violation", + // Class 53 - Insufficient Resources + "53000": "insufficient_resources", + "53100": "disk_full", + "53200": "out_of_memory", + "53300": "too_many_connections", + "53400": "configuration_limit_exceeded", + // Class 54 - Program Limit Exceeded + "54000": "program_limit_exceeded", + "54001": "statement_too_complex", + "54011": "too_many_columns", + "54023": "too_many_arguments", + // Class 55 - Object Not In Prerequisite State + "55000": "object_not_in_prerequisite_state", + "55006": "object_in_use", + "55P02": "cant_change_runtime_param", + "55P03": "lock_not_available", + // Class 57 - Operator Intervention + "57000": "operator_intervention", + "57014": "query_canceled", + "57P01": "admin_shutdown", + "57P02": "crash_shutdown", + "57P03": "cannot_connect_now", + "57P04": "database_dropped", + // Class 58 - System Error (errors external to PostgreSQL itself) + "58000": "system_error", + "58030": "io_error", + "58P01": "undefined_file", + "58P02": "duplicate_file", + // Class F0 - Configuration File Error + "F0000": "config_file_error", + "F0001": "lock_file_exists", + // Class HV - Foreign Data Wrapper Error (SQL/MED) + "HV000": "fdw_error", + "HV005": "fdw_column_name_not_found", + "HV002": "fdw_dynamic_parameter_value_needed", + "HV010": "fdw_function_sequence_error", + "HV021": "fdw_inconsistent_descriptor_information", + "HV024": "fdw_invalid_attribute_value", + "HV007": "fdw_invalid_column_name", + "HV008": "fdw_invalid_column_number", + "HV004": "fdw_invalid_data_type", + "HV006": "fdw_invalid_data_type_descriptors", + "HV091": "fdw_invalid_descriptor_field_identifier", + "HV00B": "fdw_invalid_handle", + "HV00C": "fdw_invalid_option_index", + "HV00D": "fdw_invalid_option_name", + "HV090": "fdw_invalid_string_length_or_buffer_length", + "HV00A": "fdw_invalid_string_format", + "HV009": "fdw_invalid_use_of_null_pointer", + "HV014": "fdw_too_many_handles", + "HV001": "fdw_out_of_memory", + "HV00P": "fdw_no_schemas", + "HV00J": "fdw_option_name_not_found", + "HV00K": "fdw_reply_handle", + "HV00Q": "fdw_schema_not_found", + "HV00R": "fdw_table_not_found", + "HV00L": "fdw_unable_to_create_execution", + "HV00M": "fdw_unable_to_create_reply", + "HV00N": "fdw_unable_to_establish_connection", + // Class P0 - PL/pgSQL Error + "P0000": "plpgsql_error", + "P0001": "raise_exception", + "P0002": "no_data_found", + "P0003": "too_many_rows", + // Class XX - Internal Error + "XX000": "internal_error", + "XX001": "data_corrupted", + "XX002": "index_corrupted", +} + +func parseError(r *readBuf) *Error { + err := new(Error) + for t := r.byte(); t != 0; t = r.byte() { + msg := r.string() + switch t { + case 'S': + err.Severity = msg + case 'C': + err.Code = ErrorCode(msg) + case 'M': + err.Message = msg + case 'D': + err.Detail = msg + case 'H': + err.Hint = msg + case 'P': + err.Position = msg + case 'p': + err.InternalPosition = msg + case 'q': + err.InternalQuery = msg + case 'W': + err.Where = msg + case 's': + err.Schema = msg + case 't': + err.Table = msg + case 'c': + err.Column = msg + case 'd': + err.DataTypeName = msg + case 'n': + err.Constraint = msg + case 'F': + err.File = msg + case 'L': + err.Line = msg + case 'R': + err.Routine = msg + } + } + return err +} + +// Fatal returns true if the Error Severity is fatal. +func (err *Error) Fatal() bool { + return err.Severity == Efatal +} + +// Get implements the legacy PGError interface. New code should use the fields +// of the Error struct directly. +func (err *Error) Get(k byte) (v string) { + switch k { + case 'S': + return err.Severity + case 'C': + return string(err.Code) + case 'M': + return err.Message + case 'D': + return err.Detail + case 'H': + return err.Hint + case 'P': + return err.Position + case 'p': + return err.InternalPosition + case 'q': + return err.InternalQuery + case 'W': + return err.Where + case 's': + return err.Schema + case 't': + return err.Table + case 'c': + return err.Column + case 'd': + return err.DataTypeName + case 'n': + return err.Constraint + case 'F': + return err.File + case 'L': + return err.Line + case 'R': + return err.Routine + } + return "" +} + +func (err Error) Error() string { + return "pq: " + err.Message +} + +// PGError is an interface used by previous versions of pq. It is provided +// only to support legacy code. New code should use the Error type. +type PGError interface { + Error() string + Fatal() bool + Get(k byte) (v string) +} + +func errorf(s string, args ...interface{}) { + panic(fmt.Errorf("pq: %s", fmt.Sprintf(s, args...))) +} + +func errRecoverNoErrBadConn(err *error) { + e := recover() + if e == nil { + // Do nothing + return + } + var ok bool + *err, ok = e.(error) + if !ok { + *err = fmt.Errorf("pq: unexpected error: %#v", e) + } +} + +func (c *conn) errRecover(err *error) { + e := recover() + switch v := e.(type) { + case nil: + // Do nothing + case runtime.Error: + c.bad = true + panic(v) + case *Error: + if v.Fatal() { + *err = driver.ErrBadConn + } else { + *err = v + } + case *net.OpError: + *err = driver.ErrBadConn + case error: + if v == io.EOF || v.(error).Error() == "remote error: handshake failure" { + *err = driver.ErrBadConn + } else { + *err = v + } + + default: + c.bad = true + panic(fmt.Sprintf("unknown error: %#v", e)) + } + + // Any time we return ErrBadConn, we need to remember it since *Tx doesn't + // mark the connection bad in database/sql. + if *err == driver.ErrBadConn { + c.bad = true + } +} diff --git a/Godeps/_workspace/src/github.com/lib/pq/hstore/hstore.go b/Godeps/_workspace/src/github.com/lib/pq/hstore/hstore.go new file mode 100644 index 0000000..72d5abf --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/hstore/hstore.go @@ -0,0 +1,118 @@ +package hstore + +import ( + "database/sql" + "database/sql/driver" + "strings" +) + +// A wrapper for transferring Hstore values back and forth easily. +type Hstore struct { + Map map[string]sql.NullString +} + +// escapes and quotes hstore keys/values +// s should be a sql.NullString or string +func hQuote(s interface{}) string { + var str string + switch v := s.(type) { + case sql.NullString: + if !v.Valid { + return "NULL" + } + str = v.String + case string: + str = v + default: + panic("not a string or sql.NullString") + } + + str = strings.Replace(str, "\\", "\\\\", -1) + return `"` + strings.Replace(str, "\"", "\\\"", -1) + `"` +} + +// Scan implements the Scanner interface. +// +// Note h.Map is reallocated before the scan to clear existing values. If the +// hstore column's database value is NULL, then h.Map is set to nil instead. +func (h *Hstore) Scan(value interface{}) error { + if value == nil { + h.Map = nil + return nil + } + h.Map = make(map[string]sql.NullString) + var b byte + pair := [][]byte{{}, {}} + pi := 0 + inQuote := false + didQuote := false + sawSlash := false + bindex := 0 + for bindex, b = range value.([]byte) { + if sawSlash { + pair[pi] = append(pair[pi], b) + sawSlash = false + continue + } + + switch b { + case '\\': + sawSlash = true + continue + case '"': + inQuote = !inQuote + if !didQuote { + didQuote = true + } + continue + default: + if !inQuote { + switch b { + case ' ', '\t', '\n', '\r': + continue + case '=': + continue + case '>': + pi = 1 + didQuote = false + continue + case ',': + s := string(pair[1]) + if !didQuote && len(s) == 4 && strings.ToLower(s) == "null" { + h.Map[string(pair[0])] = sql.NullString{String: "", Valid: false} + } else { + h.Map[string(pair[0])] = sql.NullString{String: string(pair[1]), Valid: true} + } + pair[0] = []byte{} + pair[1] = []byte{} + pi = 0 + continue + } + } + } + pair[pi] = append(pair[pi], b) + } + if bindex > 0 { + s := string(pair[1]) + if !didQuote && len(s) == 4 && strings.ToLower(s) == "null" { + h.Map[string(pair[0])] = sql.NullString{String: "", Valid: false} + } else { + h.Map[string(pair[0])] = sql.NullString{String: string(pair[1]), Valid: true} + } + } + return nil +} + +// Value implements the driver Valuer interface. Note if h.Map is nil, the +// database column value will be set to NULL. +func (h Hstore) Value() (driver.Value, error) { + if h.Map == nil { + return nil, nil + } + parts := []string{} + for key, val := range h.Map { + thispart := hQuote(key) + "=>" + hQuote(val) + parts = append(parts, thispart) + } + return []byte(strings.Join(parts, ",")), nil +} diff --git a/Godeps/_workspace/src/github.com/lib/pq/hstore/hstore_test.go b/Godeps/_workspace/src/github.com/lib/pq/hstore/hstore_test.go new file mode 100644 index 0000000..c9c108f --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/hstore/hstore_test.go @@ -0,0 +1,148 @@ +package hstore + +import ( + "database/sql" + "os" + "testing" + + _ "github.com/lib/pq" +) + +type Fatalistic interface { + Fatal(args ...interface{}) +} + +func openTestConn(t Fatalistic) *sql.DB { + datname := os.Getenv("PGDATABASE") + sslmode := os.Getenv("PGSSLMODE") + + if datname == "" { + os.Setenv("PGDATABASE", "pqgotest") + } + + if sslmode == "" { + os.Setenv("PGSSLMODE", "disable") + } + + conn, err := sql.Open("postgres", "") + if err != nil { + t.Fatal(err) + } + + return conn +} + +func TestHstore(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + // quitely create hstore if it doesn't exist + _, err := db.Exec("CREATE EXTENSION IF NOT EXISTS hstore") + if err != nil { + t.Skipf("Skipping hstore tests - hstore extension create failed: %s", err.Error()) + } + + hs := Hstore{} + + // test for null-valued hstores + err = db.QueryRow("SELECT NULL::hstore").Scan(&hs) + if err != nil { + t.Fatal(err) + } + if hs.Map != nil { + t.Fatalf("expected null map") + } + + err = db.QueryRow("SELECT $1::hstore", hs).Scan(&hs) + if err != nil { + t.Fatalf("re-query null map failed: %s", err.Error()) + } + if hs.Map != nil { + t.Fatalf("expected null map") + } + + // test for empty hstores + err = db.QueryRow("SELECT ''::hstore").Scan(&hs) + if err != nil { + t.Fatal(err) + } + if hs.Map == nil { + t.Fatalf("expected empty map, got null map") + } + if len(hs.Map) != 0 { + t.Fatalf("expected empty map, got len(map)=%d", len(hs.Map)) + } + + err = db.QueryRow("SELECT $1::hstore", hs).Scan(&hs) + if err != nil { + t.Fatalf("re-query empty map failed: %s", err.Error()) + } + if hs.Map == nil { + t.Fatalf("expected empty map, got null map") + } + if len(hs.Map) != 0 { + t.Fatalf("expected empty map, got len(map)=%d", len(hs.Map)) + } + + // a few example maps to test out + hsOnePair := Hstore{ + Map: map[string]sql.NullString{ + "key1": {"value1", true}, + }, + } + + hsThreePairs := Hstore{ + Map: map[string]sql.NullString{ + "key1": {"value1", true}, + "key2": {"value2", true}, + "key3": {"value3", true}, + }, + } + + hsSmorgasbord := Hstore{ + Map: map[string]sql.NullString{ + "nullstring": {"NULL", true}, + "actuallynull": {"", false}, + "NULL": {"NULL string key", true}, + "withbracket": {"value>42", true}, + "withequal": {"value=42", true}, + `"withquotes1"`: {`this "should" be fine`, true}, + `"withquotes"2"`: {`this "should\" also be fine`, true}, + "embedded1": {"value1=>x1", true}, + "embedded2": {`"value2"=>x2`, true}, + "withnewlines": {"\n\nvalue\t=>2", true}, + "<>": {`this, "should,\" also, => be fine`, true}, + }, + } + + // test encoding in query params, then decoding during Scan + testBidirectional := func(h Hstore) { + err = db.QueryRow("SELECT $1::hstore", h).Scan(&hs) + if err != nil { + t.Fatalf("re-query %d-pair map failed: %s", len(h.Map), err.Error()) + } + if hs.Map == nil { + t.Fatalf("expected %d-pair map, got null map", len(h.Map)) + } + if len(hs.Map) != len(h.Map) { + t.Fatalf("expected %d-pair map, got len(map)=%d", len(h.Map), len(hs.Map)) + } + + for key, val := range hs.Map { + otherval, found := h.Map[key] + if !found { + t.Fatalf(" key '%v' not found in %d-pair map", key, len(h.Map)) + } + if otherval.Valid != val.Valid { + t.Fatalf(" value %v <> %v in %d-pair map", otherval, val, len(h.Map)) + } + if otherval.String != val.String { + t.Fatalf(" value '%v' <> '%v' in %d-pair map", otherval.String, val.String, len(h.Map)) + } + } + } + + testBidirectional(hsOnePair) + testBidirectional(hsThreePairs) + testBidirectional(hsSmorgasbord) +} diff --git a/Godeps/_workspace/src/github.com/lib/pq/listen_example/doc.go b/Godeps/_workspace/src/github.com/lib/pq/listen_example/doc.go new file mode 100644 index 0000000..5bc99f5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/listen_example/doc.go @@ -0,0 +1,102 @@ +/* + +Below you will find a self-contained Go program which uses the LISTEN / NOTIFY +mechanism to avoid polling the database while waiting for more work to arrive. + + // + // You can see the program in action by defining a function similar to + // the following: + // + // CREATE OR REPLACE FUNCTION public.get_work() + // RETURNS bigint + // LANGUAGE sql + // AS $$ + // SELECT CASE WHEN random() >= 0.2 THEN int8 '1' END + // $$ + // ; + + package main + + import ( + "database/sql" + "fmt" + "time" + + "github.com/lib/pq" + ) + + func doWork(db *sql.DB, work int64) { + // work here + } + + func getWork(db *sql.DB) { + for { + // get work from the database here + var work sql.NullInt64 + err := db.QueryRow("SELECT get_work()").Scan(&work) + if err != nil { + fmt.Println("call to get_work() failed: ", err) + time.Sleep(10 * time.Second) + continue + } + if !work.Valid { + // no more work to do + fmt.Println("ran out of work") + return + } + + fmt.Println("starting work on ", work.Int64) + go doWork(db, work.Int64) + } + } + + func waitForNotification(l *pq.Listener) { + for { + select { + case <-l.Notify: + fmt.Println("received notification, new work available") + return + case <-time.After(90 * time.Second): + go func() { + l.Ping() + }() + // Check if there's more work available, just in case it takes + // a while for the Listener to notice connection loss and + // reconnect. + fmt.Println("received no work for 90 seconds, checking for new work") + return + } + } + } + + func main() { + var conninfo string = "" + + db, err := sql.Open("postgres", conninfo) + if err != nil { + panic(err) + } + + reportProblem := func(ev pq.ListenerEventType, err error) { + if err != nil { + fmt.Println(err.Error()) + } + } + + listener := pq.NewListener(conninfo, 10 * time.Second, time.Minute, reportProblem) + err = listener.Listen("getwork") + if err != nil { + panic(err) + } + + fmt.Println("entering main loop") + for { + // process all available work before waiting for notifications + getWork(db) + waitForNotification(listener) + } + } + + +*/ +package listen_example diff --git a/Godeps/_workspace/src/github.com/lib/pq/notify.go b/Godeps/_workspace/src/github.com/lib/pq/notify.go new file mode 100644 index 0000000..8cad578 --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/notify.go @@ -0,0 +1,766 @@ +package pq + +// Package pq is a pure Go Postgres driver for the database/sql package. +// This module contains support for Postgres LISTEN/NOTIFY. + +import ( + "errors" + "fmt" + "sync" + "sync/atomic" + "time" +) + +// Notification represents a single notification from the database. +type Notification struct { + // Process ID (PID) of the notifying postgres backend. + BePid int + // Name of the channel the notification was sent on. + Channel string + // Payload, or the empty string if unspecified. + Extra string +} + +func recvNotification(r *readBuf) *Notification { + bePid := r.int32() + channel := r.string() + extra := r.string() + + return &Notification{bePid, channel, extra} +} + +const ( + connStateIdle int32 = iota + connStateExpectResponse + connStateExpectReadyForQuery +) + +type message struct { + typ byte + err error +} + +var errListenerConnClosed = errors.New("pq: ListenerConn has been closed") + +// ListenerConn is a low-level interface for waiting for notifications. You +// should use Listener instead. +type ListenerConn struct { + // guards cn and err + connectionLock sync.Mutex + cn *conn + err error + + connState int32 + + // the sending goroutine will be holding this lock + senderLock sync.Mutex + + notificationChan chan<- *Notification + + replyChan chan message +} + +// Creates a new ListenerConn. Use NewListener instead. +func NewListenerConn(name string, notificationChan chan<- *Notification) (*ListenerConn, error) { + cn, err := Open(name) + if err != nil { + return nil, err + } + + l := &ListenerConn{ + cn: cn.(*conn), + notificationChan: notificationChan, + connState: connStateIdle, + replyChan: make(chan message, 2), + } + + go l.listenerConnMain() + + return l, nil +} + +// We can only allow one goroutine at a time to be running a query on the +// connection for various reasons, so the goroutine sending on the connection +// must be holding senderLock. +// +// Returns an error if an unrecoverable error has occurred and the ListenerConn +// should be abandoned. +func (l *ListenerConn) acquireSenderLock() error { + // we must acquire senderLock first to avoid deadlocks; see ExecSimpleQuery + l.senderLock.Lock() + + l.connectionLock.Lock() + err := l.err + l.connectionLock.Unlock() + if err != nil { + l.senderLock.Unlock() + return err + } + return nil +} + +func (l *ListenerConn) releaseSenderLock() { + l.senderLock.Unlock() +} + +// setState advances the protocol state to newState. Returns false if moving +// to that state from the current state is not allowed. +func (l *ListenerConn) setState(newState int32) bool { + var expectedState int32 + + switch newState { + case connStateIdle: + expectedState = connStateExpectReadyForQuery + case connStateExpectResponse: + expectedState = connStateIdle + case connStateExpectReadyForQuery: + expectedState = connStateExpectResponse + default: + panic(fmt.Sprintf("unexpected listenerConnState %d", newState)) + } + + return atomic.CompareAndSwapInt32(&l.connState, expectedState, newState) +} + +// Main logic is here: receive messages from the postgres backend, forward +// notifications and query replies and keep the internal state in sync with the +// protocol state. Returns when the connection has been lost, is about to go +// away or should be discarded because we couldn't agree on the state with the +// server backend. +func (l *ListenerConn) listenerConnLoop() (err error) { + defer errRecoverNoErrBadConn(&err) + + r := &readBuf{} + for { + t, err := l.cn.recvMessage(r) + if err != nil { + return err + } + + switch t { + case 'A': + // recvNotification copies all the data so we don't need to worry + // about the scratch buffer being overwritten. + l.notificationChan <- recvNotification(r) + + case 'T', 'D': + // only used by tests; ignore + + case 'E': + // We might receive an ErrorResponse even when not in a query; it + // is expected that the server will close the connection after + // that, but we should make sure that the error we display is the + // one from the stray ErrorResponse, not io.ErrUnexpectedEOF. + if !l.setState(connStateExpectReadyForQuery) { + return parseError(r) + } + l.replyChan <- message{t, parseError(r)} + + case 'C', 'I': + if !l.setState(connStateExpectReadyForQuery) { + // protocol out of sync + return fmt.Errorf("unexpected CommandComplete") + } + // ExecSimpleQuery doesn't need to know about this message + + case 'Z': + if !l.setState(connStateIdle) { + // protocol out of sync + return fmt.Errorf("unexpected ReadyForQuery") + } + l.replyChan <- message{t, nil} + + case 'N', 'S': + // ignore + default: + return fmt.Errorf("unexpected message %q from server in listenerConnLoop", t) + } + } +} + +// This is the main routine for the goroutine receiving on the database +// connection. Most of the main logic is in listenerConnLoop. +func (l *ListenerConn) listenerConnMain() { + err := l.listenerConnLoop() + + // listenerConnLoop terminated; we're done, but we still have to clean up. + // Make sure nobody tries to start any new queries by making sure the err + // pointer is set. It is important that we do not overwrite its value; a + // connection could be closed by either this goroutine or one sending on + // the connection -- whoever closes the connection is assumed to have the + // more meaningful error message (as the other one will probably get + // net.errClosed), so that goroutine sets the error we expose while the + // other error is discarded. If the connection is lost while two + // goroutines are operating on the socket, it probably doesn't matter which + // error we expose so we don't try to do anything more complex. + l.connectionLock.Lock() + if l.err == nil { + l.err = err + } + l.cn.Close() + l.connectionLock.Unlock() + + // There might be a query in-flight; make sure nobody's waiting for a + // response to it, since there's not going to be one. + close(l.replyChan) + + // let the listener know we're done + close(l.notificationChan) + + // this ListenerConn is done +} + +// Send a LISTEN query to the server. See ExecSimpleQuery. +func (l *ListenerConn) Listen(channel string) (bool, error) { + return l.ExecSimpleQuery("LISTEN " + QuoteIdentifier(channel)) +} + +// Send an UNLISTEN query to the server. See ExecSimpleQuery. +func (l *ListenerConn) Unlisten(channel string) (bool, error) { + return l.ExecSimpleQuery("UNLISTEN " + QuoteIdentifier(channel)) +} + +// Send `UNLISTEN *` to the server. See ExecSimpleQuery. +func (l *ListenerConn) UnlistenAll() (bool, error) { + return l.ExecSimpleQuery("UNLISTEN *") +} + +// Ping the remote server to make sure it's alive. Non-nil error means the +// connection has failed and should be abandoned. +func (l *ListenerConn) Ping() error { + sent, err := l.ExecSimpleQuery("") + if !sent { + return err + } + if err != nil { + // shouldn't happen + panic(err) + } + return nil +} + +// Attempt to send a query on the connection. Returns an error if sending the +// query failed, and the caller should initiate closure of this connection. +// The caller must be holding senderLock (see acquireSenderLock and +// releaseSenderLock). +func (l *ListenerConn) sendSimpleQuery(q string) (err error) { + defer errRecoverNoErrBadConn(&err) + + // must set connection state before sending the query + if !l.setState(connStateExpectResponse) { + panic("two queries running at the same time") + } + + // Can't use l.cn.writeBuf here because it uses the scratch buffer which + // might get overwritten by listenerConnLoop. + b := &writeBuf{ + buf: []byte("Q\x00\x00\x00\x00"), + pos: 1, + } + b.string(q) + l.cn.send(b) + + return nil +} + +// Execute a "simple query" (i.e. one with no bindable parameters) on the +// connection. The possible return values are: +// 1) "executed" is true; the query was executed to completion on the +// database server. If the query failed, err will be set to the error +// returned by the database, otherwise err will be nil. +// 2) If "executed" is false, the query could not be executed on the remote +// server. err will be non-nil. +// +// After a call to ExecSimpleQuery has returned an executed=false value, the +// connection has either been closed or will be closed shortly thereafter, and +// all subsequently executed queries will return an error. +func (l *ListenerConn) ExecSimpleQuery(q string) (executed bool, err error) { + if err = l.acquireSenderLock(); err != nil { + return false, err + } + defer l.releaseSenderLock() + + err = l.sendSimpleQuery(q) + if err != nil { + // We can't know what state the protocol is in, so we need to abandon + // this connection. + l.connectionLock.Lock() + // Set the error pointer if it hasn't been set already; see + // listenerConnMain. + if l.err == nil { + l.err = err + } + l.connectionLock.Unlock() + l.cn.c.Close() + return false, err + } + + // now we just wait for a reply.. + for { + m, ok := <-l.replyChan + if !ok { + // We lost the connection to server, don't bother waiting for a + // a response. err should have been set already. + l.connectionLock.Lock() + err := l.err + l.connectionLock.Unlock() + return false, err + } + switch m.typ { + case 'Z': + // sanity check + if m.err != nil { + panic("m.err != nil") + } + // done; err might or might not be set + return true, err + + case 'E': + // sanity check + if m.err == nil { + panic("m.err == nil") + } + // server responded with an error; ReadyForQuery to follow + err = m.err + + default: + return false, fmt.Errorf("unknown response for simple query: %q", m.typ) + } + } +} + +func (l *ListenerConn) Close() error { + l.connectionLock.Lock() + if l.err != nil { + l.connectionLock.Unlock() + return errListenerConnClosed + } + l.err = errListenerConnClosed + l.connectionLock.Unlock() + // We can't send anything on the connection without holding senderLock. + // Simply close the net.Conn to wake up everyone operating on it. + return l.cn.c.Close() +} + +// Err() returns the reason the connection was closed. It is not safe to call +// this function until l.Notify has been closed. +func (l *ListenerConn) Err() error { + return l.err +} + +var errListenerClosed = errors.New("pq: Listener has been closed") + +var ErrChannelAlreadyOpen = errors.New("pq: channel is already open") +var ErrChannelNotOpen = errors.New("pq: channel is not open") + +type ListenerEventType int + +const ( + // Emitted only when the database connection has been initially + // initialized. err will always be nil. + ListenerEventConnected ListenerEventType = iota + + // Emitted after a database connection has been lost, either because of an + // error or because Close has been called. err will be set to the reason + // the database connection was lost. + ListenerEventDisconnected + + // Emitted after a database connection has been re-established after + // connection loss. err will always be nil. After this event has been + // emitted, a nil pq.Notification is sent on the Listener.Notify channel. + ListenerEventReconnected + + // Emitted after a connection to the database was attempted, but failed. + // err will be set to an error describing why the connection attempt did + // not succeed. + ListenerEventConnectionAttemptFailed +) + +type EventCallbackType func(event ListenerEventType, err error) + +// Listener provides an interface for listening to notifications from a +// PostgreSQL database. For general usage information, see section +// "Notifications". +// +// Listener can safely be used from concurrently running goroutines. +type Listener struct { + // Channel for receiving notifications from the database. In some cases a + // nil value will be sent. See section "Notifications" above. + Notify chan *Notification + + name string + minReconnectInterval time.Duration + maxReconnectInterval time.Duration + eventCallback EventCallbackType + + lock sync.Mutex + isClosed bool + reconnectCond *sync.Cond + cn *ListenerConn + connNotificationChan <-chan *Notification + channels map[string]struct{} +} + +// NewListener creates a new database connection dedicated to LISTEN / NOTIFY. +// +// name should be set to a connection string to be used to establish the +// database connection (see section "Connection String Parameters" above). +// +// minReconnectInterval controls the duration to wait before trying to +// re-establish the database connection after connection loss. After each +// consecutive failure this interval is doubled, until maxReconnectInterval is +// reached. Successfully completing the connection establishment procedure +// resets the interval back to minReconnectInterval. +// +// The last parameter eventCallback can be set to a function which will be +// called by the Listener when the state of the underlying database connection +// changes. This callback will be called by the goroutine which dispatches the +// notifications over the Notify channel, so you should try to avoid doing +// potentially time-consuming operations from the callback. +func NewListener(name string, + minReconnectInterval time.Duration, + maxReconnectInterval time.Duration, + eventCallback EventCallbackType) *Listener { + l := &Listener{ + name: name, + minReconnectInterval: minReconnectInterval, + maxReconnectInterval: maxReconnectInterval, + eventCallback: eventCallback, + + channels: make(map[string]struct{}), + + Notify: make(chan *Notification, 32), + } + l.reconnectCond = sync.NewCond(&l.lock) + + go l.listenerMain() + + return l +} + +// Returns the notification channel for this listener. This is the same +// channel as Notify, and will not be recreated during the life time of the +// Listener. +func (l *Listener) NotificationChannel() <-chan *Notification { + return l.Notify +} + +// Listen starts listening for notifications on a channel. Calls to this +// function will block until an acknowledgement has been received from the +// server. Note that Listener automatically re-establishes the connection +// after connection loss, so this function may block indefinitely if the +// connection can not be re-established. +// +// Listen will only fail in three conditions: +// 1) The channel is already open. The returned error will be +// ErrChannelAlreadyOpen. +// 2) The query was executed on the remote server, but PostgreSQL returned an +// error message in response to the query. The returned error will be a +// pq.Error containing the information the server supplied. +// 3) Close is called on the Listener before the request could be completed. +// +// The channel name is case-sensitive. +func (l *Listener) Listen(channel string) error { + l.lock.Lock() + defer l.lock.Unlock() + + if l.isClosed { + return errListenerClosed + } + + // The server allows you to issue a LISTEN on a channel which is already + // open, but it seems useful to be able to detect this case to spot for + // mistakes in application logic. If the application genuinely does't + // care, it can check the exported error and ignore it. + _, exists := l.channels[channel] + if exists { + return ErrChannelAlreadyOpen + } + + if l.cn != nil { + // If gotResponse is true but error is set, the query was executed on + // the remote server, but resulted in an error. This should be + // relatively rare, so it's fine if we just pass the error to our + // caller. However, if gotResponse is false, we could not complete the + // query on the remote server and our underlying connection is about + // to go away, so we only add relname to l.channels, and wait for + // resync() to take care of the rest. + gotResponse, err := l.cn.Listen(channel) + if gotResponse && err != nil { + return err + } + } + + l.channels[channel] = struct{}{} + for l.cn == nil { + l.reconnectCond.Wait() + // we let go of the mutex for a while + if l.isClosed { + return errListenerClosed + } + } + + return nil +} + +// Unlisten removes a channel from the Listener's channel list. Returns +// ErrChannelNotOpen if the Listener is not listening on the specified channel. +// Returns immediately with no error if there is no connection. Note that you +// might still get notifications for this channel even after Unlisten has +// returned. +// +// The channel name is case-sensitive. +func (l *Listener) Unlisten(channel string) error { + l.lock.Lock() + defer l.lock.Unlock() + + if l.isClosed { + return errListenerClosed + } + + // Similarly to LISTEN, this is not an error in Postgres, but it seems + // useful to distinguish from the normal conditions. + _, exists := l.channels[channel] + if !exists { + return ErrChannelNotOpen + } + + if l.cn != nil { + // Similarly to Listen (see comment in that function), the caller + // should only be bothered with an error if it came from the backend as + // a response to our query. + gotResponse, err := l.cn.Unlisten(channel) + if gotResponse && err != nil { + return err + } + } + + // Don't bother waiting for resync if there's no connection. + delete(l.channels, channel) + return nil +} + +// UnlistenAll removes all channels from the Listener's channel list. Returns +// immediately with no error if there is no connection. Note that you might +// still get notifications for any of the deleted channels even after +// UnlistenAll has returned. +func (l *Listener) UnlistenAll() error { + l.lock.Lock() + defer l.lock.Unlock() + + if l.isClosed { + return errListenerClosed + } + + if l.cn != nil { + // Similarly to Listen (see comment in that function), the caller + // should only be bothered with an error if it came from the backend as + // a response to our query. + gotResponse, err := l.cn.UnlistenAll() + if gotResponse && err != nil { + return err + } + } + + // Don't bother waiting for resync if there's no connection. + l.channels = make(map[string]struct{}) + return nil +} + +// Ping the remote server to make sure it's alive. Non-nil return value means +// that there is no active connection. +func (l *Listener) Ping() error { + l.lock.Lock() + defer l.lock.Unlock() + + if l.isClosed { + return errListenerClosed + } + if l.cn == nil { + return errors.New("no connection") + } + + return l.cn.Ping() +} + +// Clean up after losing the server connection. Returns l.cn.Err(), which +// should have the reason the connection was lost. +func (l *Listener) disconnectCleanup() error { + l.lock.Lock() + defer l.lock.Unlock() + + // sanity check; can't look at Err() until the channel has been closed + select { + case _, ok := <-l.connNotificationChan: + if ok { + panic("connNotificationChan not closed") + } + default: + panic("connNotificationChan not closed") + } + + err := l.cn.Err() + l.cn.Close() + l.cn = nil + return err +} + +// Synchronize the list of channels we want to be listening on with the server +// after the connection has been established. +func (l *Listener) resync(cn *ListenerConn, notificationChan <-chan *Notification) error { + doneChan := make(chan error) + go func() { + for channel := range l.channels { + // If we got a response, return that error to our caller as it's + // going to be more descriptive than cn.Err(). + gotResponse, err := cn.Listen(channel) + if gotResponse && err != nil { + doneChan <- err + return + } + + // If we couldn't reach the server, wait for notificationChan to + // close and then return the error message from the connection, as + // per ListenerConn's interface. + if err != nil { + for _ = range notificationChan { + } + doneChan <- cn.Err() + return + } + } + doneChan <- nil + }() + + // Ignore notifications while synchronization is going on to avoid + // deadlocks. We have to send a nil notification over Notify anyway as + // we can't possibly know which notifications (if any) were lost while + // the connection was down, so there's no reason to try and process + // these messages at all. + for { + select { + case _, ok := <-notificationChan: + if !ok { + notificationChan = nil + } + + case err := <-doneChan: + return err + } + } +} + +// caller should NOT be holding l.lock +func (l *Listener) closed() bool { + l.lock.Lock() + defer l.lock.Unlock() + + return l.isClosed +} + +func (l *Listener) connect() error { + notificationChan := make(chan *Notification, 32) + cn, err := NewListenerConn(l.name, notificationChan) + if err != nil { + return err + } + + l.lock.Lock() + defer l.lock.Unlock() + + err = l.resync(cn, notificationChan) + if err != nil { + cn.Close() + return err + } + + l.cn = cn + l.connNotificationChan = notificationChan + l.reconnectCond.Broadcast() + + return nil +} + +// Close disconnects the Listener from the database and shuts it down. +// Subsequent calls to its methods will return an error. Close returns an +// error if the connection has already been closed. +func (l *Listener) Close() error { + l.lock.Lock() + defer l.lock.Unlock() + + if l.isClosed { + return errListenerClosed + } + + if l.cn != nil { + l.cn.Close() + } + l.isClosed = true + + return nil +} + +func (l *Listener) emitEvent(event ListenerEventType, err error) { + if l.eventCallback != nil { + l.eventCallback(event, err) + } +} + +// Main logic here: maintain a connection to the server when possible, wait +// for notifications and emit events. +func (l *Listener) listenerConnLoop() { + var nextReconnect time.Time + + reconnectInterval := l.minReconnectInterval + for { + for { + err := l.connect() + if err == nil { + break + } + + if l.closed() { + return + } + l.emitEvent(ListenerEventConnectionAttemptFailed, err) + + time.Sleep(reconnectInterval) + reconnectInterval *= 2 + if reconnectInterval > l.maxReconnectInterval { + reconnectInterval = l.maxReconnectInterval + } + } + + if nextReconnect.IsZero() { + l.emitEvent(ListenerEventConnected, nil) + } else { + l.emitEvent(ListenerEventReconnected, nil) + l.Notify <- nil + } + + reconnectInterval = l.minReconnectInterval + nextReconnect = time.Now().Add(reconnectInterval) + + for { + notification, ok := <-l.connNotificationChan + if !ok { + // lost connection, loop again + break + } + l.Notify <- notification + } + + err := l.disconnectCleanup() + if l.closed() { + return + } + l.emitEvent(ListenerEventDisconnected, err) + + time.Sleep(nextReconnect.Sub(time.Now())) + } +} + +func (l *Listener) listenerMain() { + l.listenerConnLoop() + close(l.Notify) +} diff --git a/Godeps/_workspace/src/github.com/lib/pq/notify_test.go b/Godeps/_workspace/src/github.com/lib/pq/notify_test.go new file mode 100644 index 0000000..fe8941a --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/notify_test.go @@ -0,0 +1,574 @@ +package pq + +import ( + "errors" + "fmt" + "io" + "os" + "runtime" + "sync" + "sync/atomic" + "testing" + "time" +) + +var errNilNotification = errors.New("nil notification") + +func expectNotification(t *testing.T, ch <-chan *Notification, relname string, extra string) error { + select { + case n := <-ch: + if n == nil { + return errNilNotification + } + if n.Channel != relname || n.Extra != extra { + return fmt.Errorf("unexpected notification %v", n) + } + return nil + case <-time.After(1500 * time.Millisecond): + return fmt.Errorf("timeout") + } +} + +func expectNoNotification(t *testing.T, ch <-chan *Notification) error { + select { + case n := <-ch: + return fmt.Errorf("unexpected notification %v", n) + case <-time.After(100 * time.Millisecond): + return nil + } +} + +func expectEvent(t *testing.T, eventch <-chan ListenerEventType, et ListenerEventType) error { + select { + case e := <-eventch: + if e != et { + return fmt.Errorf("unexpected event %v", e) + } + return nil + case <-time.After(1500 * time.Millisecond): + panic("expectEvent timeout") + } +} + +func expectNoEvent(t *testing.T, eventch <-chan ListenerEventType) error { + select { + case e := <-eventch: + return fmt.Errorf("unexpected event %v", e) + case <-time.After(100 * time.Millisecond): + return nil + } +} + +func newTestListenerConn(t *testing.T) (*ListenerConn, <-chan *Notification) { + datname := os.Getenv("PGDATABASE") + sslmode := os.Getenv("PGSSLMODE") + + if datname == "" { + os.Setenv("PGDATABASE", "pqgotest") + } + + if sslmode == "" { + os.Setenv("PGSSLMODE", "disable") + } + + notificationChan := make(chan *Notification) + l, err := NewListenerConn("", notificationChan) + if err != nil { + t.Fatal(err) + } + + return l, notificationChan +} + +func TestNewListenerConn(t *testing.T) { + l, _ := newTestListenerConn(t) + + defer l.Close() +} + +func TestConnListen(t *testing.T) { + l, channel := newTestListenerConn(t) + + defer l.Close() + + db := openTestConn(t) + defer db.Close() + + ok, err := l.Listen("notify_test") + if !ok || err != nil { + t.Fatal(err) + } + + _, err = db.Exec("NOTIFY notify_test") + if err != nil { + t.Fatal(err) + } + + err = expectNotification(t, channel, "notify_test", "") + if err != nil { + t.Fatal(err) + } +} + +func TestConnUnlisten(t *testing.T) { + l, channel := newTestListenerConn(t) + + defer l.Close() + + db := openTestConn(t) + defer db.Close() + + ok, err := l.Listen("notify_test") + if !ok || err != nil { + t.Fatal(err) + } + + _, err = db.Exec("NOTIFY notify_test") + + err = expectNotification(t, channel, "notify_test", "") + if err != nil { + t.Fatal(err) + } + + ok, err = l.Unlisten("notify_test") + if !ok || err != nil { + t.Fatal(err) + } + + _, err = db.Exec("NOTIFY notify_test") + if err != nil { + t.Fatal(err) + } + + err = expectNoNotification(t, channel) + if err != nil { + t.Fatal(err) + } +} + +func TestConnUnlistenAll(t *testing.T) { + l, channel := newTestListenerConn(t) + + defer l.Close() + + db := openTestConn(t) + defer db.Close() + + ok, err := l.Listen("notify_test") + if !ok || err != nil { + t.Fatal(err) + } + + _, err = db.Exec("NOTIFY notify_test") + + err = expectNotification(t, channel, "notify_test", "") + if err != nil { + t.Fatal(err) + } + + ok, err = l.UnlistenAll() + if !ok || err != nil { + t.Fatal(err) + } + + _, err = db.Exec("NOTIFY notify_test") + if err != nil { + t.Fatal(err) + } + + err = expectNoNotification(t, channel) + if err != nil { + t.Fatal(err) + } +} + +func TestConnClose(t *testing.T) { + l, _ := newTestListenerConn(t) + defer l.Close() + + err := l.Close() + if err != nil { + t.Fatal(err) + } + err = l.Close() + if err != errListenerConnClosed { + t.Fatalf("expected errListenerConnClosed; got %v", err) + } +} + +func TestConnPing(t *testing.T) { + l, _ := newTestListenerConn(t) + defer l.Close() + err := l.Ping() + if err != nil { + t.Fatal(err) + } + err = l.Close() + if err != nil { + t.Fatal(err) + } + err = l.Ping() + if err != errListenerConnClosed { + t.Fatalf("expected errListenerConnClosed; got %v", err) + } +} + +// Test for deadlock where a query fails while another one is queued +func TestConnExecDeadlock(t *testing.T) { + l, _ := newTestListenerConn(t) + defer l.Close() + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + l.ExecSimpleQuery("SELECT pg_sleep(60)") + wg.Done() + }() + runtime.Gosched() + go func() { + l.ExecSimpleQuery("SELECT 1") + wg.Done() + }() + // give the two goroutines some time to get into position + runtime.Gosched() + // calls Close on the net.Conn; equivalent to a network failure + l.Close() + + var done int32 = 0 + go func() { + time.Sleep(10 * time.Second) + if atomic.LoadInt32(&done) != 1 { + panic("timed out") + } + }() + wg.Wait() + atomic.StoreInt32(&done, 1) +} + +// Test for ListenerConn being closed while a slow query is executing +func TestListenerConnCloseWhileQueryIsExecuting(t *testing.T) { + l, _ := newTestListenerConn(t) + defer l.Close() + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + sent, err := l.ExecSimpleQuery("SELECT pg_sleep(60)") + if sent { + panic("expected sent=false") + } + // could be any of a number of errors + if err == nil { + panic("expected error") + } + wg.Done() + }() + // give the above goroutine some time to get into position + runtime.Gosched() + err := l.Close() + if err != nil { + t.Fatal(err) + } + var done int32 = 0 + go func() { + time.Sleep(10 * time.Second) + if atomic.LoadInt32(&done) != 1 { + panic("timed out") + } + }() + wg.Wait() + atomic.StoreInt32(&done, 1) +} + +func TestNotifyExtra(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + if getServerVersion(t, db) < 90000 { + t.Skip("skipping NOTIFY payload test since the server does not appear to support it") + } + + l, channel := newTestListenerConn(t) + defer l.Close() + + ok, err := l.Listen("notify_test") + if !ok || err != nil { + t.Fatal(err) + } + + _, err = db.Exec("NOTIFY notify_test, 'something'") + if err != nil { + t.Fatal(err) + } + + err = expectNotification(t, channel, "notify_test", "something") + if err != nil { + t.Fatal(err) + } +} + +// create a new test listener and also set the timeouts +func newTestListenerTimeout(t *testing.T, min time.Duration, max time.Duration) (*Listener, <-chan ListenerEventType) { + datname := os.Getenv("PGDATABASE") + sslmode := os.Getenv("PGSSLMODE") + + if datname == "" { + os.Setenv("PGDATABASE", "pqgotest") + } + + if sslmode == "" { + os.Setenv("PGSSLMODE", "disable") + } + + eventch := make(chan ListenerEventType, 16) + l := NewListener("", min, max, func(t ListenerEventType, err error) { eventch <- t }) + err := expectEvent(t, eventch, ListenerEventConnected) + if err != nil { + t.Fatal(err) + } + return l, eventch +} + +func newTestListener(t *testing.T) (*Listener, <-chan ListenerEventType) { + return newTestListenerTimeout(t, time.Hour, time.Hour) +} + +func TestListenerListen(t *testing.T) { + l, _ := newTestListener(t) + defer l.Close() + + db := openTestConn(t) + defer db.Close() + + err := l.Listen("notify_listen_test") + if err != nil { + t.Fatal(err) + } + + _, err = db.Exec("NOTIFY notify_listen_test") + if err != nil { + t.Fatal(err) + } + + err = expectNotification(t, l.Notify, "notify_listen_test", "") + if err != nil { + t.Fatal(err) + } +} + +func TestListenerUnlisten(t *testing.T) { + l, _ := newTestListener(t) + defer l.Close() + + db := openTestConn(t) + defer db.Close() + + err := l.Listen("notify_listen_test") + if err != nil { + t.Fatal(err) + } + + _, err = db.Exec("NOTIFY notify_listen_test") + if err != nil { + t.Fatal(err) + } + + err = l.Unlisten("notify_listen_test") + if err != nil { + t.Fatal(err) + } + + err = expectNotification(t, l.Notify, "notify_listen_test", "") + if err != nil { + t.Fatal(err) + } + + _, err = db.Exec("NOTIFY notify_listen_test") + if err != nil { + t.Fatal(err) + } + + err = expectNoNotification(t, l.Notify) + if err != nil { + t.Fatal(err) + } +} + +func TestListenerUnlistenAll(t *testing.T) { + l, _ := newTestListener(t) + defer l.Close() + + db := openTestConn(t) + defer db.Close() + + err := l.Listen("notify_listen_test") + if err != nil { + t.Fatal(err) + } + + _, err = db.Exec("NOTIFY notify_listen_test") + if err != nil { + t.Fatal(err) + } + + err = l.UnlistenAll() + if err != nil { + t.Fatal(err) + } + + err = expectNotification(t, l.Notify, "notify_listen_test", "") + if err != nil { + t.Fatal(err) + } + + _, err = db.Exec("NOTIFY notify_listen_test") + if err != nil { + t.Fatal(err) + } + + err = expectNoNotification(t, l.Notify) + if err != nil { + t.Fatal(err) + } +} + +func TestListenerFailedQuery(t *testing.T) { + l, eventch := newTestListener(t) + defer l.Close() + + db := openTestConn(t) + defer db.Close() + + err := l.Listen("notify_listen_test") + if err != nil { + t.Fatal(err) + } + + _, err = db.Exec("NOTIFY notify_listen_test") + if err != nil { + t.Fatal(err) + } + + err = expectNotification(t, l.Notify, "notify_listen_test", "") + if err != nil { + t.Fatal(err) + } + + // shouldn't cause a disconnect + ok, err := l.cn.ExecSimpleQuery("SELECT error") + if !ok { + t.Fatalf("could not send query to server: %v", err) + } + _, ok = err.(PGError) + if !ok { + t.Fatalf("unexpected error %v", err) + } + err = expectNoEvent(t, eventch) + if err != nil { + t.Fatal(err) + } + + // should still work + _, err = db.Exec("NOTIFY notify_listen_test") + if err != nil { + t.Fatal(err) + } + + err = expectNotification(t, l.Notify, "notify_listen_test", "") + if err != nil { + t.Fatal(err) + } +} + +func TestListenerReconnect(t *testing.T) { + l, eventch := newTestListenerTimeout(t, 20*time.Millisecond, time.Hour) + defer l.Close() + + db := openTestConn(t) + defer db.Close() + + err := l.Listen("notify_listen_test") + if err != nil { + t.Fatal(err) + } + + _, err = db.Exec("NOTIFY notify_listen_test") + if err != nil { + t.Fatal(err) + } + + err = expectNotification(t, l.Notify, "notify_listen_test", "") + if err != nil { + t.Fatal(err) + } + + // kill the connection and make sure it comes back up + ok, err := l.cn.ExecSimpleQuery("SELECT pg_terminate_backend(pg_backend_pid())") + if ok { + t.Fatalf("could not kill the connection: %v", err) + } + if err != io.EOF { + t.Fatalf("unexpected error %v", err) + } + err = expectEvent(t, eventch, ListenerEventDisconnected) + if err != nil { + t.Fatal(err) + } + err = expectEvent(t, eventch, ListenerEventReconnected) + if err != nil { + t.Fatal(err) + } + + // should still work + _, err = db.Exec("NOTIFY notify_listen_test") + if err != nil { + t.Fatal(err) + } + + // should get nil after Reconnected + err = expectNotification(t, l.Notify, "", "") + if err != errNilNotification { + t.Fatal(err) + } + + err = expectNotification(t, l.Notify, "notify_listen_test", "") + if err != nil { + t.Fatal(err) + } +} + +func TestListenerClose(t *testing.T) { + l, _ := newTestListenerTimeout(t, 20*time.Millisecond, time.Hour) + defer l.Close() + + err := l.Close() + if err != nil { + t.Fatal(err) + } + err = l.Close() + if err != errListenerClosed { + t.Fatalf("expected errListenerClosed; got %v", err) + } +} + +func TestListenerPing(t *testing.T) { + l, _ := newTestListenerTimeout(t, 20*time.Millisecond, time.Hour) + defer l.Close() + + err := l.Ping() + if err != nil { + t.Fatal(err) + } + + err = l.Close() + if err != nil { + t.Fatal(err) + } + + err = l.Ping() + if err != errListenerClosed { + t.Fatalf("expected errListenerClosed; got %v", err) + } +} diff --git a/Godeps/_workspace/src/github.com/lib/pq/oid/doc.go b/Godeps/_workspace/src/github.com/lib/pq/oid/doc.go new file mode 100644 index 0000000..caaede2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/oid/doc.go @@ -0,0 +1,6 @@ +// Package oid contains OID constants +// as defined by the Postgres server. +package oid + +// Oid is a Postgres Object ID. +type Oid uint32 diff --git a/Godeps/_workspace/src/github.com/lib/pq/oid/gen.go b/Godeps/_workspace/src/github.com/lib/pq/oid/gen.go new file mode 100644 index 0000000..cd4aea8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/oid/gen.go @@ -0,0 +1,74 @@ +// +build ignore + +// Generate the table of OID values +// Run with 'go run gen.go'. +package main + +import ( + "database/sql" + "fmt" + "log" + "os" + "os/exec" + + _ "github.com/lib/pq" +) + +func main() { + datname := os.Getenv("PGDATABASE") + sslmode := os.Getenv("PGSSLMODE") + + if datname == "" { + os.Setenv("PGDATABASE", "pqgotest") + } + + if sslmode == "" { + os.Setenv("PGSSLMODE", "disable") + } + + db, err := sql.Open("postgres", "") + if err != nil { + log.Fatal(err) + } + cmd := exec.Command("gofmt") + cmd.Stderr = os.Stderr + w, err := cmd.StdinPipe() + if err != nil { + log.Fatal(err) + } + f, err := os.Create("types.go") + if err != nil { + log.Fatal(err) + } + cmd.Stdout = f + err = cmd.Start() + if err != nil { + log.Fatal(err) + } + fmt.Fprintln(w, "// generated by 'go run gen.go'; do not edit") + fmt.Fprintln(w, "\npackage oid") + fmt.Fprintln(w, "const (") + rows, err := db.Query(` + SELECT typname, oid + FROM pg_type WHERE oid < 10000 + ORDER BY oid; + `) + if err != nil { + log.Fatal(err) + } + var name string + var oid int + for rows.Next() { + err = rows.Scan(&name, &oid) + if err != nil { + log.Fatal(err) + } + fmt.Fprintf(w, "T_%s Oid = %d\n", name, oid) + } + if err = rows.Err(); err != nil { + log.Fatal(err) + } + fmt.Fprintln(w, ")") + w.Close() + cmd.Wait() +} diff --git a/Godeps/_workspace/src/github.com/lib/pq/oid/types.go b/Godeps/_workspace/src/github.com/lib/pq/oid/types.go new file mode 100644 index 0000000..03df05a --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/oid/types.go @@ -0,0 +1,161 @@ +// generated by 'go run gen.go'; do not edit + +package oid + +const ( + T_bool Oid = 16 + T_bytea Oid = 17 + T_char Oid = 18 + T_name Oid = 19 + T_int8 Oid = 20 + T_int2 Oid = 21 + T_int2vector Oid = 22 + T_int4 Oid = 23 + T_regproc Oid = 24 + T_text Oid = 25 + T_oid Oid = 26 + T_tid Oid = 27 + T_xid Oid = 28 + T_cid Oid = 29 + T_oidvector Oid = 30 + T_pg_type Oid = 71 + T_pg_attribute Oid = 75 + T_pg_proc Oid = 81 + T_pg_class Oid = 83 + T_json Oid = 114 + T_xml Oid = 142 + T__xml Oid = 143 + T_pg_node_tree Oid = 194 + T__json Oid = 199 + T_smgr Oid = 210 + T_point Oid = 600 + T_lseg Oid = 601 + T_path Oid = 602 + T_box Oid = 603 + T_polygon Oid = 604 + T_line Oid = 628 + T__line Oid = 629 + T_cidr Oid = 650 + T__cidr Oid = 651 + T_float4 Oid = 700 + T_float8 Oid = 701 + T_abstime Oid = 702 + T_reltime Oid = 703 + T_tinterval Oid = 704 + T_unknown Oid = 705 + T_circle Oid = 718 + T__circle Oid = 719 + T_money Oid = 790 + T__money Oid = 791 + T_macaddr Oid = 829 + T_inet Oid = 869 + T__bool Oid = 1000 + T__bytea Oid = 1001 + T__char Oid = 1002 + T__name Oid = 1003 + T__int2 Oid = 1005 + T__int2vector Oid = 1006 + T__int4 Oid = 1007 + T__regproc Oid = 1008 + T__text Oid = 1009 + T__tid Oid = 1010 + T__xid Oid = 1011 + T__cid Oid = 1012 + T__oidvector Oid = 1013 + T__bpchar Oid = 1014 + T__varchar Oid = 1015 + T__int8 Oid = 1016 + T__point Oid = 1017 + T__lseg Oid = 1018 + T__path Oid = 1019 + T__box Oid = 1020 + T__float4 Oid = 1021 + T__float8 Oid = 1022 + T__abstime Oid = 1023 + T__reltime Oid = 1024 + T__tinterval Oid = 1025 + T__polygon Oid = 1027 + T__oid Oid = 1028 + T_aclitem Oid = 1033 + T__aclitem Oid = 1034 + T__macaddr Oid = 1040 + T__inet Oid = 1041 + T_bpchar Oid = 1042 + T_varchar Oid = 1043 + T_date Oid = 1082 + T_time Oid = 1083 + T_timestamp Oid = 1114 + T__timestamp Oid = 1115 + T__date Oid = 1182 + T__time Oid = 1183 + T_timestamptz Oid = 1184 + T__timestamptz Oid = 1185 + T_interval Oid = 1186 + T__interval Oid = 1187 + T__numeric Oid = 1231 + T_pg_database Oid = 1248 + T__cstring Oid = 1263 + T_timetz Oid = 1266 + T__timetz Oid = 1270 + T_bit Oid = 1560 + T__bit Oid = 1561 + T_varbit Oid = 1562 + T__varbit Oid = 1563 + T_numeric Oid = 1700 + T_refcursor Oid = 1790 + T__refcursor Oid = 2201 + T_regprocedure Oid = 2202 + T_regoper Oid = 2203 + T_regoperator Oid = 2204 + T_regclass Oid = 2205 + T_regtype Oid = 2206 + T__regprocedure Oid = 2207 + T__regoper Oid = 2208 + T__regoperator Oid = 2209 + T__regclass Oid = 2210 + T__regtype Oid = 2211 + T_record Oid = 2249 + T_cstring Oid = 2275 + T_any Oid = 2276 + T_anyarray Oid = 2277 + T_void Oid = 2278 + T_trigger Oid = 2279 + T_language_handler Oid = 2280 + T_internal Oid = 2281 + T_opaque Oid = 2282 + T_anyelement Oid = 2283 + T__record Oid = 2287 + T_anynonarray Oid = 2776 + T_pg_authid Oid = 2842 + T_pg_auth_members Oid = 2843 + T__txid_snapshot Oid = 2949 + T_uuid Oid = 2950 + T__uuid Oid = 2951 + T_txid_snapshot Oid = 2970 + T_fdw_handler Oid = 3115 + T_anyenum Oid = 3500 + T_tsvector Oid = 3614 + T_tsquery Oid = 3615 + T_gtsvector Oid = 3642 + T__tsvector Oid = 3643 + T__gtsvector Oid = 3644 + T__tsquery Oid = 3645 + T_regconfig Oid = 3734 + T__regconfig Oid = 3735 + T_regdictionary Oid = 3769 + T__regdictionary Oid = 3770 + T_anyrange Oid = 3831 + T_event_trigger Oid = 3838 + T_int4range Oid = 3904 + T__int4range Oid = 3905 + T_numrange Oid = 3906 + T__numrange Oid = 3907 + T_tsrange Oid = 3908 + T__tsrange Oid = 3909 + T_tstzrange Oid = 3910 + T__tstzrange Oid = 3911 + T_daterange Oid = 3912 + T__daterange Oid = 3913 + T_int8range Oid = 3926 + T__int8range Oid = 3927 +) diff --git a/Godeps/_workspace/src/github.com/lib/pq/ssl_test.go b/Godeps/_workspace/src/github.com/lib/pq/ssl_test.go new file mode 100644 index 0000000..932b336 --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/ssl_test.go @@ -0,0 +1,226 @@ +package pq + +// This file contains SSL tests + +import ( + _ "crypto/sha256" + "crypto/x509" + "database/sql" + "fmt" + "os" + "path/filepath" + "testing" +) + +func maybeSkipSSLTests(t *testing.T) { + // Require some special variables for testing certificates + if os.Getenv("PQSSLCERTTEST_PATH") == "" { + t.Skip("PQSSLCERTTEST_PATH not set, skipping SSL tests") + } + + value := os.Getenv("PQGOSSLTESTS") + if value == "" || value == "0" { + t.Skip("PQGOSSLTESTS not enabled, skipping SSL tests") + } else if value != "1" { + t.Fatalf("unexpected value %q for PQGOSSLTESTS", value) + } +} + +func openSSLConn(t *testing.T, conninfo string) (*sql.DB, error) { + db, err := openTestConnConninfo(conninfo) + if err != nil { + // should never fail + t.Fatal(err) + } + // Do something with the connection to see whether it's working or not. + tx, err := db.Begin() + if err == nil { + return db, tx.Rollback() + } + _ = db.Close() + return nil, err +} + +func checkSSLSetup(t *testing.T, conninfo string) { + db, err := openSSLConn(t, conninfo) + if err == nil { + db.Close() + t.Fatalf("expected error with conninfo=%q", conninfo) + } +} + +// Connect over SSL and run a simple query to test the basics +func TestSSLConnection(t *testing.T) { + maybeSkipSSLTests(t) + // Environment sanity check: should fail without SSL + checkSSLSetup(t, "sslmode=disable user=pqgossltest") + + db, err := openSSLConn(t, "sslmode=require user=pqgossltest") + if err != nil { + t.Fatal(err) + } + rows, err := db.Query("SELECT 1") + if err != nil { + t.Fatal(err) + } + rows.Close() +} + +// Test sslmode=verify-full +func TestSSLVerifyFull(t *testing.T) { + maybeSkipSSLTests(t) + // Environment sanity check: should fail without SSL + checkSSLSetup(t, "sslmode=disable user=pqgossltest") + + // Not OK according to the system CA + _, err := openSSLConn(t, "host=postgres sslmode=verify-full user=pqgossltest") + if err == nil { + t.Fatal("expected error") + } + _, ok := err.(x509.UnknownAuthorityError) + if !ok { + t.Fatalf("expected x509.UnknownAuthorityError, got %#+v", err) + } + + rootCertPath := filepath.Join(os.Getenv("PQSSLCERTTEST_PATH"), "root.crt") + rootCert := "sslrootcert=" + rootCertPath + " " + // No match on Common Name + _, err = openSSLConn(t, rootCert+"host=127.0.0.1 sslmode=verify-full user=pqgossltest") + if err == nil { + t.Fatal("expected error") + } + _, ok = err.(x509.HostnameError) + if !ok { + t.Fatalf("expected x509.HostnameError, got %#+v", err) + } + // OK + _, err = openSSLConn(t, rootCert+"host=postgres sslmode=verify-full user=pqgossltest") + if err != nil { + t.Fatal(err) + } +} + +// Test sslmode=verify-ca +func TestSSLVerifyCA(t *testing.T) { + maybeSkipSSLTests(t) + // Environment sanity check: should fail without SSL + checkSSLSetup(t, "sslmode=disable user=pqgossltest") + + // Not OK according to the system CA + _, err := openSSLConn(t, "host=postgres sslmode=verify-ca user=pqgossltest") + if err == nil { + t.Fatal("expected error") + } + _, ok := err.(x509.UnknownAuthorityError) + if !ok { + t.Fatalf("expected x509.UnknownAuthorityError, got %#+v", err) + } + + rootCertPath := filepath.Join(os.Getenv("PQSSLCERTTEST_PATH"), "root.crt") + rootCert := "sslrootcert=" + rootCertPath + " " + // No match on Common Name, but that's OK + _, err = openSSLConn(t, rootCert+"host=127.0.0.1 sslmode=verify-ca user=pqgossltest") + if err != nil { + t.Fatal(err) + } + // Everything OK + _, err = openSSLConn(t, rootCert+"host=postgres sslmode=verify-ca user=pqgossltest") + if err != nil { + t.Fatal(err) + } +} + +func getCertConninfo(t *testing.T, source string) string { + var sslkey string + var sslcert string + + certpath := os.Getenv("PQSSLCERTTEST_PATH") + + switch source { + case "missingkey": + sslkey = "/tmp/filedoesnotexist" + sslcert = filepath.Join(certpath, "postgresql.crt") + case "missingcert": + sslkey = filepath.Join(certpath, "postgresql.key") + sslcert = "/tmp/filedoesnotexist" + case "certtwice": + sslkey = filepath.Join(certpath, "postgresql.crt") + sslcert = filepath.Join(certpath, "postgresql.crt") + case "valid": + sslkey = filepath.Join(certpath, "postgresql.key") + sslcert = filepath.Join(certpath, "postgresql.crt") + default: + t.Fatalf("invalid source %q", source) + } + return fmt.Sprintf("sslmode=require user=pqgosslcert sslkey=%s sslcert=%s", sslkey, sslcert) +} + +// Authenticate over SSL using client certificates +func TestSSLClientCertificates(t *testing.T) { + maybeSkipSSLTests(t) + // Environment sanity check: should fail without SSL + checkSSLSetup(t, "sslmode=disable user=pqgossltest") + + // Should also fail without a valid certificate + db, err := openSSLConn(t, "sslmode=require user=pqgosslcert") + if err == nil { + db.Close() + t.Fatal("expected error") + } + pge, ok := err.(*Error) + if !ok { + t.Fatal("expected pq.Error") + } + if pge.Code.Name() != "invalid_authorization_specification" { + t.Fatalf("unexpected error code %q", pge.Code.Name()) + } + + // Should work + db, err = openSSLConn(t, getCertConninfo(t, "valid")) + if err != nil { + t.Fatal(err) + } + rows, err := db.Query("SELECT 1") + if err != nil { + t.Fatal(err) + } + rows.Close() +} + +// Test errors with ssl certificates +func TestSSLClientCertificatesMissingFiles(t *testing.T) { + maybeSkipSSLTests(t) + // Environment sanity check: should fail without SSL + checkSSLSetup(t, "sslmode=disable user=pqgossltest") + + // Key missing, should fail + _, err := openSSLConn(t, getCertConninfo(t, "missingkey")) + if err == nil { + t.Fatal("expected error") + } + // should be a PathError + _, ok := err.(*os.PathError) + if !ok { + t.Fatalf("expected PathError, got %#+v", err) + } + + // Cert missing, should fail + _, err = openSSLConn(t, getCertConninfo(t, "missingcert")) + if err == nil { + t.Fatal("expected error") + } + // should be a PathError + _, ok = err.(*os.PathError) + if !ok { + t.Fatalf("expected PathError, got %#+v", err) + } + + // Key has wrong permissions, should fail + _, err = openSSLConn(t, getCertConninfo(t, "certtwice")) + if err == nil { + t.Fatal("expected error") + } + if err != ErrSSLKeyHasWorldPermissions { + t.Fatalf("expected ErrSSLKeyHasWorldPermissions, got %#+v", err) + } +} diff --git a/Godeps/_workspace/src/github.com/lib/pq/url.go b/Godeps/_workspace/src/github.com/lib/pq/url.go new file mode 100644 index 0000000..9bac95c --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/url.go @@ -0,0 +1,76 @@ +package pq + +import ( + "fmt" + nurl "net/url" + "sort" + "strings" +) + +// ParseURL no longer needs to be used by clients of this library since supplying a URL as a +// connection string to sql.Open() is now supported: +// +// sql.Open("postgres", "postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full") +// +// It remains exported here for backwards-compatibility. +// +// ParseURL converts a url to a connection string for driver.Open. +// Example: +// +// "postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full" +// +// converts to: +// +// "user=bob password=secret host=1.2.3.4 port=5432 dbname=mydb sslmode=verify-full" +// +// A minimal example: +// +// "postgres://" +// +// This will be blank, causing driver.Open to use all of the defaults +func ParseURL(url string) (string, error) { + u, err := nurl.Parse(url) + if err != nil { + return "", err + } + + if u.Scheme != "postgres" && u.Scheme != "postgresql" { + return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme) + } + + var kvs []string + escaper := strings.NewReplacer(` `, `\ `, `'`, `\'`, `\`, `\\`) + accrue := func(k, v string) { + if v != "" { + kvs = append(kvs, k+"="+escaper.Replace(v)) + } + } + + if u.User != nil { + v := u.User.Username() + accrue("user", v) + + v, _ = u.User.Password() + accrue("password", v) + } + + i := strings.Index(u.Host, ":") + if i < 0 { + accrue("host", u.Host) + } else { + accrue("host", u.Host[:i]) + accrue("port", u.Host[i+1:]) + } + + if u.Path != "" { + accrue("dbname", u.Path[1:]) + } + + q := u.Query() + for k := range q { + accrue(k, q.Get(k)) + } + + sort.Strings(kvs) // Makes testing easier (not a performance concern) + return strings.Join(kvs, " "), nil +} diff --git a/Godeps/_workspace/src/github.com/lib/pq/url_test.go b/Godeps/_workspace/src/github.com/lib/pq/url_test.go new file mode 100644 index 0000000..29f4a7c --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/url_test.go @@ -0,0 +1,54 @@ +package pq + +import ( + "testing" +) + +func TestSimpleParseURL(t *testing.T) { + expected := "host=hostname.remote" + str, err := ParseURL("postgres://hostname.remote") + if err != nil { + t.Fatal(err) + } + + if str != expected { + t.Fatalf("unexpected result from ParseURL:\n+ %v\n- %v", str, expected) + } +} + +func TestFullParseURL(t *testing.T) { + expected := `dbname=database host=hostname.remote password=top\ secret port=1234 user=username` + str, err := ParseURL("postgres://username:top%20secret@hostname.remote:1234/database") + if err != nil { + t.Fatal(err) + } + + if str != expected { + t.Fatalf("unexpected result from ParseURL:\n+ %s\n- %s", str, expected) + } +} + +func TestInvalidProtocolParseURL(t *testing.T) { + _, err := ParseURL("http://hostname.remote") + switch err { + case nil: + t.Fatal("Expected an error from parsing invalid protocol") + default: + msg := "invalid connection protocol: http" + if err.Error() != msg { + t.Fatalf("Unexpected error message:\n+ %s\n- %s", + err.Error(), msg) + } + } +} + +func TestMinimalURL(t *testing.T) { + cs, err := ParseURL("postgres://") + if err != nil { + t.Fatal(err) + } + + if cs != "" { + t.Fatalf("expected blank connection string, got: %q", cs) + } +} diff --git a/Godeps/_workspace/src/github.com/lib/pq/user_posix.go b/Godeps/_workspace/src/github.com/lib/pq/user_posix.go new file mode 100644 index 0000000..e937d7d --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/user_posix.go @@ -0,0 +1,24 @@ +// Package pq is a pure Go Postgres driver for the database/sql package. + +// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris + +package pq + +import ( + "os" + "os/user" +) + +func userCurrent() (string, error) { + u, err := user.Current() + if err == nil { + return u.Username, nil + } + + name := os.Getenv("USER") + if name != "" { + return name, nil + } + + return "", ErrCouldNotDetectUsername +} diff --git a/Godeps/_workspace/src/github.com/lib/pq/user_windows.go b/Godeps/_workspace/src/github.com/lib/pq/user_windows.go new file mode 100644 index 0000000..2b69126 --- /dev/null +++ b/Godeps/_workspace/src/github.com/lib/pq/user_windows.go @@ -0,0 +1,27 @@ +// Package pq is a pure Go Postgres driver for the database/sql package. +package pq + +import ( + "path/filepath" + "syscall" +) + +// Perform Windows user name lookup identically to libpq. +// +// The PostgreSQL code makes use of the legacy Win32 function +// GetUserName, and that function has not been imported into stock Go. +// GetUserNameEx is available though, the difference being that a +// wider range of names are available. To get the output to be the +// same as GetUserName, only the base (or last) component of the +// result is returned. +func userCurrent() (string, error) { + pw_name := make([]uint16, 128) + pwname_size := uint32(len(pw_name)) - 1 + err := syscall.GetUserNameEx(syscall.NameSamCompatible, &pw_name[0], &pwname_size) + if err != nil { + return "", ErrCouldNotDetectUsername + } + s := syscall.UTF16ToString(pw_name) + u := filepath.Base(s) + return u, nil +} diff --git a/Godeps/_workspace/src/github.com/qor/inflection/README.md b/Godeps/_workspace/src/github.com/qor/inflection/README.md new file mode 100644 index 0000000..2221b91 --- /dev/null +++ b/Godeps/_workspace/src/github.com/qor/inflection/README.md @@ -0,0 +1,39 @@ +Inflection +========= + +Inflection pluralizes and singularizes English nouns + +## Basic Usage + +```go +inflection.Plural("person") => "people" +inflection.Plural("Person") => "People" +inflection.Plural("PERSON") => "PEOPLE" +inflection.Plural("bus") => "buses" +inflection.Plural("BUS") => "BUSES" +inflection.Plural("Bus") => "Buses" + +inflection.Singularize("people") => "person" +inflection.Singularize("People") => "Person" +inflection.Singularize("PEOPLE") => "PERSON" +inflection.Singularize("buses") => "bus" +inflection.Singularize("BUSES") => "BUS" +inflection.Singularize("Buses") => "Bus" + +inflection.Plural("FancyPerson") => "FancyPeople" +inflection.Singularize("FancyPeople") => "FancyPerson" +``` + +## Register Rules + +Standard rules are from Rails's ActiveSupport (https://github.com/rails/rails/blob/master/activesupport/lib/active_support/inflections.rb) + +If you want to register more rules, follow: + +``` +inflection.AddUncountable("fish") +inflection.AddIrregular("person", "people") +inflection.AddPlural("(bu)s$", "${1}ses") # "bus" => "buses" / "BUS" => "BUSES" / "Bus" => "Buses" +inflection.AddSingular("(bus)(es)?$", "${1}") # "buses" => "bus" / "Buses" => "Bus" / "BUSES" => "BUS" +``` + diff --git a/Godeps/_workspace/src/github.com/qor/inflection/inflections.go b/Godeps/_workspace/src/github.com/qor/inflection/inflections.go new file mode 100644 index 0000000..a62b113 --- /dev/null +++ b/Godeps/_workspace/src/github.com/qor/inflection/inflections.go @@ -0,0 +1,195 @@ +/* +Package inflection pluralizes and singularizes English nouns. + + inflection.Plural("person") => "people" + inflection.Plural("Person") => "People" + inflection.Plural("PERSON") => "PEOPLE" + + inflection.Singularize("people") => "person" + inflection.Singularize("People") => "Person" + inflection.Singularize("PEOPLE") => "PERSON" + + inflection.Plural("FancyPerson") => "FancydPeople" + inflection.Singularize("FancyPeople") => "FancydPerson" + +Standard rules are from Rails's ActiveSupport (https://github.com/rails/rails/blob/master/activesupport/lib/active_support/inflections.rb) + +If you want to register more rules, follow: + + inflection.AddUncountable("fish") + inflection.AddIrregular("person", "people") + inflection.AddPlural("(bu)s$", "${1}ses") # "bus" => "buses" / "BUS" => "BUSES" / "Bus" => "Buses" + inflection.AddSingular("(bus)(es)?$", "${1}") # "buses" => "bus" / "Buses" => "Bus" / "BUSES" => "BUS" +*/ +package inflection + +import ( + "regexp" + "strings" +) + +var pluralInflections = [][]string{ + []string{"([a-z])$", "${1}s"}, + []string{"s$", "s"}, + []string{"^(ax|test)is$", "${1}es"}, + []string{"(octop|vir)us$", "${1}i"}, + []string{"(octop|vir)i$", "${1}i"}, + []string{"(alias|status)$", "${1}es"}, + []string{"(bu)s$", "${1}ses"}, + []string{"(buffal|tomat)o$", "${1}oes"}, + []string{"([ti])um$", "${1}a"}, + []string{"([ti])a$", "${1}a"}, + []string{"sis$", "ses"}, + []string{"(?:([^f])fe|([lr])f)$", "${1}${2}ves"}, + []string{"(hive)$", "${1}s"}, + []string{"([^aeiouy]|qu)y$", "${1}ies"}, + []string{"(x|ch|ss|sh)$", "${1}es"}, + []string{"(matr|vert|ind)(?:ix|ex)$", "${1}ices"}, + []string{"^(m|l)ouse$", "${1}ice"}, + []string{"^(m|l)ice$", "${1}ice"}, + []string{"^(ox)$", "${1}en"}, + []string{"^(oxen)$", "${1}"}, + []string{"(quiz)$", "${1}zes"}, +} + +var singularInflections = [][]string{ + []string{"s$", ""}, + []string{"(ss)$", "${1}"}, + []string{"(n)ews$", "${1}ews"}, + []string{"([ti])a$", "${1}um"}, + []string{"((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$", "${1}sis"}, + []string{"(^analy)(sis|ses)$", "${1}sis"}, + []string{"([^f])ves$", "${1}fe"}, + []string{"(hive)s$", "${1}"}, + []string{"(tive)s$", "${1}"}, + []string{"([lr])ves$", "${1}f"}, + []string{"([^aeiouy]|qu)ies$", "${1}y"}, + []string{"(s)eries$", "${1}eries"}, + []string{"(m)ovies$", "${1}ovie"}, + []string{"(x|ch|ss|sh)es$", "${1}"}, + []string{"^(m|l)ice$", "${1}ouse"}, + []string{"(bus)(es)?$", "${1}"}, + []string{"(o)es$", "${1}"}, + []string{"(shoe)s$", "${1}"}, + []string{"(cris|test)(is|es)$", "${1}is"}, + []string{"^(a)x[ie]s$", "${1}xis"}, + []string{"(octop|vir)(us|i)$", "${1}us"}, + []string{"(alias|status)(es)?$", "${1}"}, + []string{"^(ox)en", "${1}"}, + []string{"(vert|ind)ices$", "${1}ex"}, + []string{"(matr)ices$", "${1}ix"}, + []string{"(quiz)zes$", "${1}"}, + []string{"(database)s$", "${1}"}, +} + +var irregularInflections = [][]string{ + []string{"person", "people"}, + []string{"man", "men"}, + []string{"child", "children"}, + []string{"sex", "sexes"}, + []string{"move", "moves"}, + []string{"mombie", "mombies"}, +} + +var uncountableInflections = []string{"equipment", "information", "rice", "money", "species", "series", "fish", "sheep", "jeans", "police"} + +type inflection struct { + regexp *regexp.Regexp + replace string +} + +var compiledPluralMaps []inflection +var compiledSingularMaps []inflection + +func compile() { + compiledPluralMaps = []inflection{} + compiledSingularMaps = []inflection{} + for _, uncountable := range uncountableInflections { + inf := inflection{ + regexp: regexp.MustCompile("^(?i)(" + uncountable + ")$"), + replace: "${1}", + } + compiledPluralMaps = append(compiledPluralMaps, inf) + compiledSingularMaps = append(compiledSingularMaps, inf) + } + + for _, value := range irregularInflections { + infs := []inflection{ + inflection{regexp: regexp.MustCompile(strings.ToUpper(value[0]) + "$"), replace: strings.ToUpper(value[1])}, + inflection{regexp: regexp.MustCompile(strings.Title(value[0]) + "$"), replace: strings.Title(value[1])}, + inflection{regexp: regexp.MustCompile(value[0] + "$"), replace: value[1]}, + } + compiledPluralMaps = append(compiledPluralMaps, infs...) + } + + for _, value := range irregularInflections { + infs := []inflection{ + inflection{regexp: regexp.MustCompile(strings.ToUpper(value[1]) + "$"), replace: strings.ToUpper(value[0])}, + inflection{regexp: regexp.MustCompile(strings.Title(value[1]) + "$"), replace: strings.Title(value[0])}, + inflection{regexp: regexp.MustCompile(value[1] + "$"), replace: value[0]}, + } + compiledSingularMaps = append(compiledSingularMaps, infs...) + } + + for i := len(pluralInflections) - 1; i >= 0; i-- { + value := pluralInflections[i] + infs := []inflection{ + inflection{regexp: regexp.MustCompile(strings.ToUpper(value[0])), replace: strings.ToUpper(value[1])}, + inflection{regexp: regexp.MustCompile(value[0]), replace: value[1]}, + inflection{regexp: regexp.MustCompile("(?i)" + value[0]), replace: value[1]}, + } + compiledPluralMaps = append(compiledPluralMaps, infs...) + } + + for i := len(singularInflections) - 1; i >= 0; i-- { + value := singularInflections[i] + infs := []inflection{ + inflection{regexp: regexp.MustCompile(strings.ToUpper(value[0])), replace: strings.ToUpper(value[1])}, + inflection{regexp: regexp.MustCompile(value[0]), replace: value[1]}, + inflection{regexp: regexp.MustCompile("(?i)" + value[0]), replace: value[1]}, + } + compiledSingularMaps = append(compiledSingularMaps, infs...) + } +} + +func init() { + compile() +} + +func AddPlural(key, value string) { + pluralInflections = append(pluralInflections, []string{key, value}) + compile() +} + +func AddSingular(key, value string) { + singularInflections = append(singularInflections, []string{key, value}) + compile() +} + +func AddIrregular(key, value string) { + irregularInflections = append(irregularInflections, []string{key, value}) + compile() +} + +func AddUncountable(value string) { + uncountableInflections = append(uncountableInflections, value) + compile() +} + +func Plural(str string) string { + for _, inflection := range compiledPluralMaps { + if inflection.regexp.MatchString(str) { + return inflection.regexp.ReplaceAllString(str, inflection.replace) + } + } + return str +} + +func Singular(str string) string { + for _, inflection := range compiledSingularMaps { + if inflection.regexp.MatchString(str) { + return inflection.regexp.ReplaceAllString(str, inflection.replace) + } + } + return str +} diff --git a/Godeps/_workspace/src/github.com/qor/inflection/inflections_test.go b/Godeps/_workspace/src/github.com/qor/inflection/inflections_test.go new file mode 100644 index 0000000..448573d --- /dev/null +++ b/Godeps/_workspace/src/github.com/qor/inflection/inflections_test.go @@ -0,0 +1,97 @@ +package inflection_test + +import ( + "strings" + "testing" + + "github.com/qor/inflection" +) + +var inflections = map[string]string{ + "star": "stars", + "STAR": "STARS", + "Star": "Stars", + "bus": "buses", + "fish": "fish", + "mouse": "mice", + "query": "queries", + "ability": "abilities", + "agency": "agencies", + "movie": "movies", + "archive": "archives", + "index": "indices", + "wife": "wives", + "safe": "saves", + "half": "halves", + "move": "moves", + "salesperson": "salespeople", + "person": "people", + "spokesman": "spokesmen", + "man": "men", + "woman": "women", + "basis": "bases", + "diagnosis": "diagnoses", + "diagnosis_a": "diagnosis_as", + "datum": "data", + "medium": "media", + "stadium": "stadia", + "analysis": "analyses", + "node_child": "node_children", + "child": "children", + "experience": "experiences", + "day": "days", + "comment": "comments", + "foobar": "foobars", + "newsletter": "newsletters", + "old_news": "old_news", + "news": "news", + "series": "series", + "species": "species", + "quiz": "quizzes", + "perspective": "perspectives", + "ox": "oxen", + "photo": "photos", + "buffalo": "buffaloes", + "tomato": "tomatoes", + "dwarf": "dwarves", + "elf": "elves", + "information": "information", + "equipment": "equipment", + "criterion": "criteria", +} + +func init() { + inflection.AddIrregular("criterion", "criteria") +} + +func TestPlural(t *testing.T) { + for key, value := range inflections { + if v := inflection.Plural(strings.ToUpper(key)); v != strings.ToUpper(value) { + t.Errorf("%v's plural should be %v, but got %v", strings.ToUpper(key), strings.ToUpper(value), v) + } + + if v := inflection.Plural(strings.Title(key)); v != strings.Title(value) { + t.Errorf("%v's plural should be %v, but got %v", strings.Title(key), strings.Title(value), v) + } + + if v := inflection.Plural(key); v != value { + t.Errorf("%v's plural should be %v, but got %v", key, value, v) + } + } +} + +func TestSingular(t *testing.T) { + for key, value := range inflections { + if v := inflection.Singular(strings.ToUpper(value)); v != strings.ToUpper(key) { + t.Errorf("%v's singular should be %v, but got %v", strings.ToUpper(value), strings.ToUpper(key), v) + } + + if v := inflection.Singular(strings.Title(value)); v != strings.Title(key) { + t.Errorf("%v's singular should be %v, but got %v", strings.Title(value), strings.Title(key), v) + } + + if v := inflection.Singular(value); v != key { + t.Errorf("%v's singular should be %v, but got %v", value, key, v) + } + } +} diff --git a/api.go b/api.go index a9411fa..cc6282e 100644 --- a/api.go +++ b/api.go @@ -1,13 +1,71 @@ package main import ( + "github.com/ant0ine/go-json-rest/rest" + "github.com/jinzhu/gorm" + _ "github.com/lib/pq" + "log" "net/http" "os" + "time" ) func main() { - http.Handle("/", helloHandler()) - http.ListenAndServe(":"+os.Getenv("PORT"), nil) + i := Impl{} + i.InitDB() + i.InitSchema() + + api := rest.NewApi() + api.Use(rest.DefaultDevStack...) + router, err := rest.MakeRouter( + rest.Post("/events", i.PostEvent), + ) + if err != nil { + log.Fatal(err) + } + api.SetApp(router) + log.Fatal(http.ListenAndServe(":"+os.Getenv("PORT"), api.MakeHandler())) +} + +type Event struct { + Id int64 `json:"id"` + Measurement string `sql:"size:1024" json:"measurement"` + Fields string `sql:"size:1024" json:"fields"` + Tags string `sql:"size:1024" json:"tags"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt time.Time `json:"-"` +} + +type Impl struct { + DB gorm.DB +} + +func (i *Impl) InitDB() { + var err error + connection := os.Getenv("DATABASE_URL") + i.DB, err = gorm.Open("postgres", connection) + if err != nil { + log.Fatalf("Got error when connect database, the error is '%v'", err) + } + i.DB.LogMode(true) +} + +func (i *Impl) InitSchema() { + i.DB.AutoMigrate(&Event{}) +} + +func (i *Impl) PostEvent(w rest.ResponseWriter, r *rest.Request) { + event := Event{} + if err := r.DecodeJsonPayload(&event); err != nil { + rest.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if err := i.DB.Save(&event).Error; err != nil { + rest.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteJson(&event) } func helloHandler() http.Handler {