From 85d874b02080ac60202d038c1da687555589e3e7 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Tue, 29 Aug 2017 10:23:26 -0700 Subject: [PATCH] vendor: gorqlite --- vendor-log | 1 + vendor/git.xeserv.us/xena/gorqlite/api.go | 203 +++++++++ vendor/git.xeserv.us/xena/gorqlite/cluster.go | 223 ++++++++++ vendor/git.xeserv.us/xena/gorqlite/conn.go | 300 +++++++++++++ .../git.xeserv.us/xena/gorqlite/gorqlite.go | 189 +++++++++ .../xena/gorqlite/prepared_statement.go | 54 +++ vendor/git.xeserv.us/xena/gorqlite/query.go | 395 ++++++++++++++++++ vendor/git.xeserv.us/xena/gorqlite/write.go | 178 ++++++++ 8 files changed, 1543 insertions(+) create mode 100644 vendor/git.xeserv.us/xena/gorqlite/api.go create mode 100644 vendor/git.xeserv.us/xena/gorqlite/cluster.go create mode 100644 vendor/git.xeserv.us/xena/gorqlite/conn.go create mode 100644 vendor/git.xeserv.us/xena/gorqlite/gorqlite.go create mode 100644 vendor/git.xeserv.us/xena/gorqlite/prepared_statement.go create mode 100644 vendor/git.xeserv.us/xena/gorqlite/query.go create mode 100644 vendor/git.xeserv.us/xena/gorqlite/write.go diff --git a/vendor-log b/vendor-log index 71af920..9aa44ea 100644 --- a/vendor-log +++ b/vendor-log @@ -39,3 +39,4 @@ f5079bd7f6f74e23c4d65efa0f4ce14cbd6a3c0f golang.org/x/net/context f51c12702a4d776e4c1fa9b0fabab841babae631 golang.org/x/time/rate ae77be60afb1dcacde03767a8c37337fad28ac14 github.com/kardianos/osext 6a18b51d929caddbe795e1609195dee1d1cc729e github.com/justinian/dice +40a5e952d22c3ef520c6ab7bdb9b1a010ec9a524 git.xeserv.us/xena/gorqlite diff --git a/vendor/git.xeserv.us/xena/gorqlite/api.go b/vendor/git.xeserv.us/xena/gorqlite/api.go new file mode 100644 index 0000000..e91441d --- /dev/null +++ b/vendor/git.xeserv.us/xena/gorqlite/api.go @@ -0,0 +1,203 @@ +package gorqlite + +/* + this file has low level stuff: + + rqliteApiGet() + rqliteApiPost() + + There is some code duplication between those and they should + probably be combined into one function. + + nothing public here. + +*/ + +import "bytes" +import "encoding/json" +import "errors" +import "fmt" +import "io/ioutil" +import "net/http" +import "time" + +/* ***************************************************************** + + method: rqliteApiGet() - for api_STATUS + + - lowest level interface - does not do any JSON unmarshaling + - handles retries + - handles timeouts + + * *****************************************************************/ + +func (conn *Connection) rqliteApiGet(apiOp apiOperation) ([]byte, error) { + var responseBody []byte + trace("%s: rqliteApiGet() called", conn.ID) + + // only api_STATUS now - maybe someday BACKUP + if apiOp != api_STATUS { + return responseBody, errors.New("rqliteApiGet() called for invalid api operation") + } + + // just to be safe, check this + peersToTry := conn.cluster.makePeerList() + if len(peersToTry) < 1 { + return responseBody, errors.New("I don't have any cluster info") + } + trace("%s: I have a peer list %d peers long", conn.ID, len(peersToTry)) + + // failure log is used so that if all peers fail, we can say something + // about why each failed + failureLog := make([]string, 0) + +PeerLoop: + for peerNum, peerToTry := range peersToTry { + trace("%s: attemping to contact peer %d", conn.ID, peerNum) + // docs say default GET policy is up to 10 follows automatically + url := conn.assembleURL(api_STATUS, peerToTry) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + trace("%s: got error '%s' doing http.NewRequest", conn.ID, err.Error()) + failureLog = append(failureLog, fmt.Sprintf("%s failed due to %s", url, err.Error())) + continue PeerLoop + } + trace("%s: http.NewRequest() OK") + req.Header.Set("Content-Type", "application/json") + client := &http.Client{} + client.Timeout = time.Duration(conn.timeout) * time.Second + response, err := client.Do(req) + if err != nil { + trace("%s: got error '%s' doing client.Do", conn.ID, err.Error()) + failureLog = append(failureLog, fmt.Sprintf("%s failed due to %s", url, err.Error())) + continue PeerLoop + } + defer response.Body.Close() + trace("%s: client.Do() OK") + responseBody, err := ioutil.ReadAll(response.Body) + if err != nil { + trace("%s: got error '%s' doing ioutil.ReadAll", conn.ID, err.Error()) + failureLog = append(failureLog, fmt.Sprintf("%s failed due to %s", url, err.Error())) + continue PeerLoop + } + trace("%s: ioutil.ReadAll() OK") + if response.Status != "200 OK" { + trace("%s: got code %s", conn.ID, response.Status) + failureLog = append(failureLog, fmt.Sprintf("%s failed, got: %s", url, response.Status)) + continue PeerLoop + } + // if we got here, we succeeded + trace("%s: api call OK, returning", conn.ID) + return responseBody, nil + } + + // if we got here, all peers failed. Let's build a verbose error message + var stringBuffer bytes.Buffer + stringBuffer.WriteString("tried all peers unsuccessfully. here are the results:\n") + for n, v := range failureLog { + stringBuffer.WriteString(fmt.Sprintf(" peer #%d: %s\n", n, v)) + } + return responseBody, errors.New(stringBuffer.String()) +} + +/* ***************************************************************** + + method: rqliteApiPost() - for api_QUERY and api_WRITE + + - lowest level interface - does not do any JSON unmarshaling + - handles 301s, etc. + - handles retries + - handles timeouts + + it is called with an apiOperation type because the URL it will use varies + depending on the API operation type (api_QUERY vs. api_WRITE) + + * *****************************************************************/ + +func (conn *Connection) rqliteApiPost(apiOp apiOperation, sqlStatements []string) ([]byte, error) { + var responseBody []byte + + switch apiOp { + case api_QUERY: + trace("%s: rqliteApiGet() post called for a QUERY of %d statements", conn.ID, len(sqlStatements)) + case api_WRITE: + trace("%s: rqliteApiGet() post called for a QUERY of %d statements", conn.ID, len(sqlStatements)) + default: + return responseBody, errors.New("weird! called for an invalid apiOperation in rqliteApiPost()") + } + + // jsonify the statements. not really needed in the + // case of api_STATUS but doesn't hurt + + jStatements, err := json.Marshal(sqlStatements) + if err != nil { + return nil, err + } + + // just to be safe, check this + peersToTry := conn.cluster.makePeerList() + if len(peersToTry) < 1 { + return responseBody, errors.New("I don't have any cluster info") + } + + // failure log is used so that if all peers fail, we can say something + // about why each failed + failureLog := make([]string, 0) + +PeerLoop: + for peerNum, peer := range peersToTry { + trace("%s: trying peer #%d", conn.ID, peerNum) + + // we're doing a post, and the RFCs say that if you get a 301, it's not + // automatically followed, so we have to do that ourselves + + responseStatus := "Haven't Tried Yet" + var url string + for responseStatus == "Haven't Tried Yet" || responseStatus == "301 Moved Permanently" { + url = conn.assembleURL(apiOp, peer) + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jStatements)) + if err != nil { + trace("%s: got error '%s' doing http.NewRequest", conn.ID, err.Error()) + failureLog = append(failureLog, fmt.Sprintf("%s failed due to %s", url, err.Error())) + continue PeerLoop + } + req.Header.Set("Content-Type", "application/json") + client := &http.Client{} + response, err := client.Do(req) + if err != nil { + trace("%s: got error '%s' doing client.Do", conn.ID, err.Error()) + failureLog = append(failureLog, fmt.Sprintf("%s failed due to %s", url, err.Error())) + continue PeerLoop + } + defer response.Body.Close() + responseBody, err = ioutil.ReadAll(response.Body) + if err != nil { + trace("%s: got error '%s' doing ioutil.ReadAll", conn.ID, err.Error()) + failureLog = append(failureLog, fmt.Sprintf("%s failed due to %s", url, err.Error())) + continue PeerLoop + } + responseStatus = response.Status + if responseStatus == "301 Moved Permanently" { + v := response.Header["Location"] + failureLog = append(failureLog, fmt.Sprintf("%s redirected me to %s", url, v[0])) + url = v[0] + continue PeerLoop + } else if responseStatus == "200 OK" { + trace("%s: api call OK, returning", conn.ID) + return responseBody, nil + } else { + trace("%s: got error in responseStatus: %s", conn.ID, responseStatus) + failureLog = append(failureLog, fmt.Sprintf("%s failed, got: %s", url, response.Status)) + continue PeerLoop + } + } + } + + // if we got here, all peers failed. Let's build a verbose error message + var stringBuffer bytes.Buffer + stringBuffer.WriteString("tried all peers unsuccessfully. here are the results:\n") + for n, v := range failureLog { + stringBuffer.WriteString(fmt.Sprintf(" peer #%d: %s\n", n, v)) + } + return responseBody, errors.New(stringBuffer.String()) +} diff --git a/vendor/git.xeserv.us/xena/gorqlite/cluster.go b/vendor/git.xeserv.us/xena/gorqlite/cluster.go new file mode 100644 index 0000000..d5df7d5 --- /dev/null +++ b/vendor/git.xeserv.us/xena/gorqlite/cluster.go @@ -0,0 +1,223 @@ +package gorqlite + +/* + this file holds most of the cluster-related stuff: + + types: + peer + rqliteCluster + Connection methods: + assembleURL (from a peer) + updateClusterInfo (does the full cluster discovery via status) +*/ + +/* ***************************************************************** + + imports + + * *****************************************************************/ + +import "bytes" +import "encoding/json" +import "errors" +import "fmt" +import "strings" + +//import "os" +//import "reflect" + +/* ***************************************************************** + + type: peer + + this is an internal type to abstact peer info. + + note that hostname is sometimes used for "has this struct been + inialized" checks. + + * *****************************************************************/ + +type peer struct { + hostname string // hostname or "localhost" + port string // "4001" or port, only ever used as a string +} + +func (p *peer) String() string { + return fmt.Sprintf("%s:%s", p.hostname, p.port) +} + +/* ***************************************************************** + + type: rqliteCluster + + internal type that abstracts the full cluster state (leader, peers) + + * *****************************************************************/ + +type rqliteCluster struct { + leader peer + otherPeers []peer + conn *Connection +} + +/* ***************************************************************** + + method: rqliteCluster.makePeerList() + + in the api calls, we'll want to try the leader first, then the other + peers. to make looping easy, this function returns a list of peers + in the order the try them: leader, other peer, other peer, etc. + + * *****************************************************************/ + +func (rc *rqliteCluster) makePeerList() []peer { + trace("%s: makePeerList() called", rc.conn.ID) + var peerList []peer + peerList = append(peerList, rc.leader) + for _, p := range rc.otherPeers { + peerList = append(peerList, p) + } + + trace("%s: makePeerList() returning this list:", rc.conn.ID) + for n, v := range peerList { + trace("%s: makePeerList() peer %d -> %s", rc.conn.ID, n, v.hostname+":"+v.port) + } + + return peerList +} + +/* ***************************************************************** + + method: Connection.assembleURL() + + tell it what peer to talk to and what kind of API operation you're + making, and it will return the full URL, from start to finish. + e.g.: + + https://mary:secret2@server1.example.com:1234/db/query?transaction&level=strong + + note: this func needs to live at the Connection level because the + Connection holds the username, password, consistencyLevel, etc. + + * *****************************************************************/ + +func (conn *Connection) assembleURL(apiOp apiOperation, p peer) string { + var stringBuffer bytes.Buffer + + if conn.wantsHTTPS == true { + stringBuffer.WriteString("https") + } else { + stringBuffer.WriteString("http") + } + stringBuffer.WriteString("://") + if conn.username != "" && conn.password != "" { + stringBuffer.WriteString(conn.username) + stringBuffer.WriteString(":") + stringBuffer.WriteString(conn.password) + stringBuffer.WriteString("@") + } + stringBuffer.WriteString(p.hostname) + stringBuffer.WriteString(":") + stringBuffer.WriteString(p.port) + + switch apiOp { + case api_STATUS: + stringBuffer.WriteString("/status") + case api_QUERY: + stringBuffer.WriteString("/db/query") + case api_WRITE: + stringBuffer.WriteString("/db/execute") + } + + if apiOp == api_QUERY || apiOp == api_WRITE { + stringBuffer.WriteString("?timings&transaction&level=") + stringBuffer.WriteString(consistencyLevelNames[conn.consistencyLevel]) + } + + switch apiOp { + case api_QUERY: + trace("%s: assembled URL for an api_QUERY: %s", conn.ID, stringBuffer.String()) + case api_STATUS: + trace("%s: assembled URL for an api_STATUS: %s", conn.ID, stringBuffer.String()) + case api_WRITE: + trace("%s: assembled URL for an api_WRITE: %s", conn.ID, stringBuffer.String()) + } + + return stringBuffer.String() +} + +/* ***************************************************************** + + method: Connection.updateClusterInfo() + + upon invocation, updateClusterInfo() completely erases and refreshes + the Connection's cluster info, replacing its rqliteCluster object + with current info. + + the web heavy lifting (retrying, etc.) is done in rqliteApiGet() + + * *****************************************************************/ + +func (conn *Connection) updateClusterInfo() error { + trace("%s: updateClusterInfo() called", conn.ID) + + // start with a fresh new cluster + var rc rqliteCluster + rc.conn = conn + + responseBody, err := conn.rqliteApiGet(api_STATUS) + if err != nil { + return err + } + trace("%s: updateClusterInfo() back from api call OK", conn.ID) + + sections := make(map[string]interface{}) + err = json.Unmarshal(responseBody, §ions) + if err != nil { + return err + } + sMap := sections["store"].(map[string]interface{}) + leaderRaftAddr := sMap["leader"].(string) + trace("%s: leader from store section is %s", conn.ID, leaderRaftAddr) + + // leader in this case is the RAFT address + // we want the HTTP address, so we'll use this as + // a key as we sift through APIPeers + + meta := sMap["meta"].(map[string]interface{}) + apiPeers := meta["APIPeers"].(map[string]interface{}) + + for raftAddr, httpAddr := range apiPeers { + trace("%s: examining httpAddr %s", conn.ID, httpAddr) + + /* httpAddr are usually hostname:port */ + var p peer + parts := strings.Split(httpAddr.(string), ":") + p.hostname = parts[0] + p.port = parts[1] + + // so is this the leader? + if leaderRaftAddr == raftAddr { + trace("%s: found leader at %s", conn.ID, httpAddr) + rc.leader = p + } else { + rc.otherPeers = append(rc.otherPeers, p) + } + } + + if rc.leader.hostname == "" { + return errors.New("could not determine leader from API status call") + } + + // dump to trace + trace("%s: here is my cluster config:", conn.ID) + trace("%s: leader : %s", conn.ID, rc.leader.String()) + for n, v := range rc.otherPeers { + trace("%s: otherPeer #%d: %s", conn.ID, n, v.String()) + } + + // now make it official + conn.cluster = rc + + return nil +} diff --git a/vendor/git.xeserv.us/xena/gorqlite/conn.go b/vendor/git.xeserv.us/xena/gorqlite/conn.go new file mode 100644 index 0000000..2673064 --- /dev/null +++ b/vendor/git.xeserv.us/xena/gorqlite/conn.go @@ -0,0 +1,300 @@ +package gorqlite + +/* + this file contains some high-level Connection-oriented stuff +*/ + +/* ***************************************************************** + + imports + + * *****************************************************************/ + +import "errors" +import "fmt" +import "io" +import "net" +import nurl "net/url" +import "strings" + +var errClosed = errors.New("gorqlite: connection is closed") +var traceOut io.Writer + +// defaults to false. This is used in trace() to quickly +// return if tracing is off, so that we don't do a perhaps +// expensive Sprintf() call only to send it to Discard + +var wantsTrace bool + +/* ***************************************************************** + + type: Connection + + * *****************************************************************/ + +/* + The connection abstraction. Note that since rqlite is stateless, + there really is no "connection". However, this type holds + information such as the current leader, peers, connection + string to build URLs, etc. + + Connections are assigned a "connection ID" which is a pseudo-UUID + for connection identification in trace output only. This helps + sort out what's going on if you have multiple connections going + at once. It's generated using a non-standards-or-anything-else-compliant + function that uses crypto/rand to generate 16 random bytes. + + Note that the Connection objection holds info on all peers, gathered + at time of Open() from the node specified. +*/ + +type Connection struct { + cluster rqliteCluster + + /* + name type default + */ + + username string // username or "" + password string // username or "" + consistencyLevel consistencyLevel // WEAK + wantsHTTPS bool // false unless connection URL is https + + // variables below this line need to be initialized in Open() + + timeout int // 10 + hasBeenClosed bool // false + ID string // generated in init() +} + +/* ***************************************************************** + + method: Connection.Close() + + * *****************************************************************/ + +func (conn *Connection) Close() { + conn.hasBeenClosed = true + trace("%s: %s", conn.ID, "closing connection") +} + +/* ***************************************************************** + + method: Connection.ConsistencyLevel() + + * *****************************************************************/ + +func (conn *Connection) ConsistencyLevel() (string, error) { + if conn.hasBeenClosed { + return "", errClosed + } + return consistencyLevelNames[conn.consistencyLevel], nil +} + +/* ***************************************************************** + + method: Connection.Leader() + + * *****************************************************************/ + +func (conn *Connection) Leader() (string, error) { + if conn.hasBeenClosed { + return "", errClosed + } + trace("%s: Leader(), calling updateClusterInfo()", conn.ID) + err := conn.updateClusterInfo() + if err != nil { + trace("%s: Leader() got error from updateClusterInfo(): %s", conn.ID, err.Error()) + return "", err + } else { + trace("%s: Leader(), updateClusterInfo() OK", conn.ID) + } + return conn.cluster.leader.String(), nil +} + +/* ***************************************************************** + + method: Connection.Peers() + + * *****************************************************************/ + +func (conn *Connection) Peers() ([]string, error) { + if conn.hasBeenClosed { + var ans []string + return ans, errClosed + } + plist := make([]string, 0) + + trace("%s: Peers(), calling updateClusterInfo()", conn.ID) + err := conn.updateClusterInfo() + if err != nil { + trace("%s: Peers() got error from updateClusterInfo(): %s", conn.ID, err.Error()) + return plist, err + } else { + trace("%s: Peers(), updateClusterInfo() OK", conn.ID) + } + plist = append(plist, conn.cluster.leader.String()) + for _, p := range conn.cluster.otherPeers { + plist = append(plist, p.String()) + } + return plist, nil +} + +/* ***************************************************************** + + method: Connection.SetConsistencyLevel() + + * *****************************************************************/ + +func (conn *Connection) SetConsistencyLevel(levelDesired string) error { + if conn.hasBeenClosed { + return errClosed + } + _, ok := consistencyLevels[levelDesired] + if ok { + conn.consistencyLevel = consistencyLevels[levelDesired] + return nil + } + return errors.New(fmt.Sprintf("unknown consistency level: %s", levelDesired)) +} + +/* ***************************************************************** + + method: Connection.initConnection() + + * *****************************************************************/ + +/* + initConnection takes the initial connection URL specified by + the user, and parses it into a peer. This peer is assumed to + be the leader. The next thing Open() does is updateClusterInfo() + so the truth will be revealed soon enough. + + initConnection() does not talk to rqlite. It only parses the + connection URL and prepares the new connection for work. + + URL format: + + http[s]://${USER}:${PASSWORD}@${HOSTNAME}:${PORT}/db?[OPTIONS] + + Examples: + + https://mary:secret2@localhost:4001/db + https://mary:secret2@server1.example.com:4001/db?level=none + https://mary:secret2@server2.example.com:4001/db?level=weak + https://mary:secret2@localhost:2265/db?level=strong + + to use default connection to localhost:4001 with no auth: + http:// + https:// + + guaranteed map fields - will be set to "" if not specified + + field name default if not specified + + username "" + password "" + hostname "localhost" + port "4001" + consistencyLevel "weak" +*/ + +func (conn *Connection) initConnection(url string) error { + + // do some sanity checks. You know users. + + if len(url) < 7 { + return errors.New("url specified is impossibly short") + } + + if strings.HasPrefix(url, "http") == false { + return errors.New("url does not start with 'http'") + } + + u, err := nurl.Parse(url) + if err != nil { + return err + } + trace("%s: net.url.Parse() OK", conn.ID) + + if u.Scheme == "https" { + conn.wantsHTTPS = true + } + + // specs say Username() is always populated even if empty + + if u.User == nil { + conn.username = "" + conn.password = "" + } else { + // guaranteed, but could be empty which is ok + conn.username = u.User.Username() + + // not guaranteed, so test if set + pass, isset := u.User.Password() + if isset { + conn.password = pass + } else { + conn.password = "" + } + } + + if u.Host == "" { + conn.cluster.leader.hostname = "localhost" + } else { + conn.cluster.leader.hostname = u.Host + } + + if u.Host == "" { + conn.cluster.leader.hostname = "localhost" + conn.cluster.leader.port = "4001" + } else { + // SplitHostPort() should only return an error if there is no host port. + // I think. + h, p, err := net.SplitHostPort(u.Host) + if err != nil { + conn.cluster.leader.hostname = u.Host + } else { + conn.cluster.leader.hostname = h + conn.cluster.leader.port = p + } + } + + /* + + at the moment, the only allowed query is "level=" with + the desired consistency level + + */ + + // default + conn.consistencyLevel = cl_WEAK + + if u.RawQuery != "" { + if u.RawQuery == "level=weak" { + // that's ok but nothing to do + } else if u.RawQuery == "level=strong" { + conn.consistencyLevel = cl_STRONG + } else if u.RawQuery == "level=none" { // the fools! + conn.consistencyLevel = cl_NONE + } else { + return errors.New("don't know what to do with this query: " + u.RawQuery) + } + } + + trace("%s: parseDefaultPeer() is done:", conn.ID) + if conn.wantsHTTPS == true { + trace("%s: %s -> %s", conn.ID, "wants https?", "yes") + } else { + trace("%s: %s -> %s", conn.ID, "wants https?", "no") + } + trace("%s: %s -> %s", conn.ID, "username", conn.username) + trace("%s: %s -> %s", conn.ID, "password", conn.password) + trace("%s: %s -> %s", conn.ID, "hostname", conn.cluster.leader.hostname) + trace("%s: %s -> %s", conn.ID, "port", conn.cluster.leader.port) + trace("%s: %s -> %s", conn.ID, "consistencyLevel", consistencyLevelNames[conn.consistencyLevel]) + + conn.cluster.conn = conn + + return nil +} diff --git a/vendor/git.xeserv.us/xena/gorqlite/gorqlite.go b/vendor/git.xeserv.us/xena/gorqlite/gorqlite.go new file mode 100644 index 0000000..6ba7660 --- /dev/null +++ b/vendor/git.xeserv.us/xena/gorqlite/gorqlite.go @@ -0,0 +1,189 @@ +/* + gorqlite + A golang database/sql driver for rqlite, the distributed consistent sqlite. + + Copyright (c)2016 andrew fabbro (andrew@fabbro.org) + + See LICENSE.md for license. tl;dr: MIT. Conveniently, the same licese as rqlite. + + Project home page: https://github.com/raindo308/gorqlite + + Learn more about rqlite at: https://github.com/rqlite/rqlite + +*/ +package gorqlite + +/* + this file contains package-level stuff: + consts + init() + Open, TraceOn(), TraceOff() +*/ + +import "crypto/rand" +import "fmt" +import "io" +import "io/ioutil" +import "strings" + +/* ***************************************************************** + + const + + * *****************************************************************/ + +type consistencyLevel int + +const ( + cl_NONE consistencyLevel = iota + cl_WEAK + cl_STRONG +) + +// used in several places, actually +var consistencyLevelNames map[consistencyLevel]string +var consistencyLevels map[string]consistencyLevel + +type apiOperation int + +const ( + api_QUERY apiOperation = iota + api_STATUS + api_WRITE +) + +/* ***************************************************************** + + init() + + * *****************************************************************/ + +func init() { + traceOut = ioutil.Discard + + consistencyLevelNames = make(map[consistencyLevel]string) + consistencyLevelNames[cl_NONE] = "none" + consistencyLevelNames[cl_WEAK] = "weak" + consistencyLevelNames[cl_STRONG] = "strong" + + consistencyLevels = make(map[string]consistencyLevel) + consistencyLevels["none"] = cl_NONE + consistencyLevels["weak"] = cl_WEAK + consistencyLevels["strong"] = cl_STRONG + +} + +/* ***************************************************************** +Open() creates and returns a "connection" to rqlite. + +Since rqlite is stateless, there is no actual connection. Open() creates and initializes a gorqlite Connection type, which represents various config information. + +The URL should be in a form like this: + + http://localhost:4001 + + http:// default, no auth, localhost:4001 + https:// default, no auth, localhost:4001, using https + + http://localhost:1234 + http://mary:secret2@localhost:1234 + + https://mary:secret2@somewhere.example.com:1234 + https://mary:secret2@somewhere.example.com // will use 4001 + * *****************************************************************/ +func Open(connURL string) (Connection, error) { + var conn Connection + + // generate our uuid for trace + b := make([]byte, 16) + _, err := rand.Read(b) + if err != nil { + return conn, err + } + conn.ID = fmt.Sprintf("%X-%X-%X-%X-%X", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) + trace("%s: Open() called for url: %s", conn.ID, connURL) + + // set defaults + conn.timeout = 10 + conn.hasBeenClosed = false + + // parse the URL given + err = conn.initConnection(connURL) + if err != nil { + return conn, err + } + + // call updateClusterInfo() to populate the cluster + // also tests the user's default + + err = conn.updateClusterInfo() + + // and the err from updateClusterInfo() will be our err as well + return conn, err +} + +/* ***************************************************************** + + func: trace() + + adds a message to the trace output + + not a public function. we (inside) can add - outside they can + only see. + + Call trace as: Sprintf pattern , args... + + This is done so that the more expensive Sprintf() stuff is + done only if truly needed. When tracing is off, calls to + trace() just hit a bool check and return. If tracing is on, + then the Sprintfing is done at a leisurely pace because, well, + we're tracing. + + Premature optimization is the root of all evil, so this is + probably sinful behavior. + + Don't put a \n in your Sprintf pattern becuase trace() adds one + + * *****************************************************************/ + +func trace(pattern string, args ...interface{}) { + // don't do the probably expensive Sprintf() if not needed + if wantsTrace == false { + return + } + + // this could all be made into one long statement but we have + // compilers to do such things for us. let's sip a mint julep + // and spell this out in glorious exposition. + + // make sure there is one and only one newline + nlPattern := strings.TrimSpace(pattern) + "\n" + msg := fmt.Sprintf(nlPattern, args...) + traceOut.Write([]byte(msg)) +} + +/* + TraceOn() + + Turns on tracing output to the io.Writer of your choice. + + Trace output is very detailed and verbose, as you might expect. + + Normally, you should run with tracing off, as it makes absolutely + no concession to performance and is intended for debugging/dev use. +*/ +func TraceOn(w io.Writer) { + traceOut = w + wantsTrace = true +} + +/* + TraceOff() + + Turns off tracing output. Once you call TraceOff(), no further + info is sent to the io.Writer, unless it is TraceOn'd again. +*/ +func TraceOff() { + wantsTrace = false + traceOut = ioutil.Discard +} diff --git a/vendor/git.xeserv.us/xena/gorqlite/prepared_statement.go b/vendor/git.xeserv.us/xena/gorqlite/prepared_statement.go new file mode 100644 index 0000000..2ef43ca --- /dev/null +++ b/vendor/git.xeserv.us/xena/gorqlite/prepared_statement.go @@ -0,0 +1,54 @@ +package gorqlite + +import ( + "fmt" + "strings" +) + +// EscapeString sql-escapes a string. +func EscapeString(value string) string { + replace := [][2]string{ + {`\`, `\\`}, + {`\0`, `\\0`}, + {`\n`, `\\n`}, + {`\r`, `\\r`}, + {`"`, `\"`}, + {`'`, `\'`}, + } + + for _, val := range replace { + value = strings.Replace(value, val[0], val[1], -1) + } + + return value +} + +// PreparedStatement is a simple wrapper around fmt.Sprintf for prepared SQL +// statements. +type PreparedStatement struct { + body string +} + +// NewPreparedStatement takes a sprintf syntax SQL query for later binding of +// parameters. +func NewPreparedStatement(body string) PreparedStatement { + return PreparedStatement{body: body} +} + +// Bind takes arguments and SQL-escapes them, then calling fmt.Sprintf. +func (p PreparedStatement) Bind(args ...interface{}) string { + var spargs []interface{} + + for _, arg := range args { + switch arg.(type) { + case string: + spargs = append(spargs, `'`+EscapeString(arg.(string))+`'`) + case fmt.Stringer: + spargs = append(spargs, `'`+EscapeString(arg.(fmt.Stringer).String())+`'`) + default: + spargs = append(spargs, arg) + } + } + + return fmt.Sprintf(p.body, spargs...) +} diff --git a/vendor/git.xeserv.us/xena/gorqlite/query.go b/vendor/git.xeserv.us/xena/gorqlite/query.go new file mode 100644 index 0000000..d093e1a --- /dev/null +++ b/vendor/git.xeserv.us/xena/gorqlite/query.go @@ -0,0 +1,395 @@ +package gorqlite + +import "errors" +import "fmt" +import "encoding/json" + +/* ***************************************************************** + + method: Connection.Query() + + This is the JSON we get back: + +{ + "results": [ + { + "columns": [ + "id", + "name" + ], + "types": [ + "integer", + "text" + ], + "values": [ + [ + 1, + "fiona" + ], + [ + 2, + "sinead" + ] + ], + "time": 0.0150043 + } + ], + "time": 0.0220043 +} + + or + +{ + "results": [ + { + "columns": [ + "id", + "name" + ], + "types": [ + "number", + "text" + ], + "values": [ + [ + null, + "Hulk" + ] + ], + "time": 4.8958e-05 + }, + { + "columns": [ + "id", + "name" + ], + "types": [ + "number", + "text" + ], + "time": 1.8460000000000003e-05 + } + ], + "time": 0.000134776 +} + + or + +{ + "results": [ + { + "error": "near \"nonsense\": syntax error" + } + ], + "time": 2.478862 +} + + * *****************************************************************/ + +/* +QueryOne() is a convenience method that wraps Query() into a single-statement +method. +*/ +func (conn *Connection) QueryOne(sqlStatement string) (qr QueryResult, err error) { + if conn.hasBeenClosed { + qr.Err = errClosed + return qr, errClosed + } + sqlStatements := make([]string, 0) + sqlStatements = append(sqlStatements, sqlStatement) + qra, err := conn.Query(sqlStatements) + return qra[0], err +} + +/* +Query() is used to perform SELECT operations in the database. + +It takes an array of SQL statements and executes them in a single transaction, returning an array of QueryResult vars. +*/ +func (conn *Connection) Query(sqlStatements []string) (results []QueryResult, err error) { + results = make([]QueryResult, 0) + + if conn.hasBeenClosed { + var errResult QueryResult + errResult.Err = errClosed + results = append(results, errResult) + return results, errClosed + } + trace("%s: Query() for %d statements", conn.ID, len(sqlStatements)) + + // if we get an error POSTing, that's a showstopper + response, err := conn.rqliteApiPost(api_QUERY, sqlStatements) + if err != nil { + trace("%s: rqliteApiCall() ERROR: %s", conn.ID, err.Error()) + var errResult QueryResult + errResult.Err = err + results = append(results, errResult) + return results, err + } + trace("%s: rqliteApiCall() OK", conn.ID) + + // if we get an error Unmarshaling, that's a showstopper + var sections map[string]interface{} + err = json.Unmarshal(response, §ions) + if err != nil { + trace("%s: json.Unmarshal() ERROR: %s", conn.ID, err.Error()) + var errResult QueryResult + errResult.Err = err + results = append(results, errResult) + return results, err + } + + /* + at this point, we have a "results" section and + a "time" section. we can igore the latter. + */ + + resultsArray := sections["results"].([]interface{}) + trace("%s: I have %d result(s) to parse", conn.ID, len(resultsArray)) + + numStatementErrors := 0 + for n, r := range resultsArray { + trace("%s: parsing result %d", conn.ID, n) + var thisQR QueryResult + thisQR.conn = conn + + // r is a hash with columns, types, values, and time + thisResult := r.(map[string]interface{}) + + // did we get an error? + _, ok := thisResult["error"] + if ok { + trace("%s: have an error on this result: %s", conn.ID, thisResult["error"].(string)) + thisQR.Err = errors.New(thisResult["error"].(string)) + results = append(results, thisQR) + numStatementErrors++ + continue + } + + // time is a float64 + thisQR.Timing = thisResult["time"].(float64) + + // column & type are an array of strings + c := thisResult["columns"].([]interface{}) + t := thisResult["types"].([]interface{}) + for i := 0; i < len(c); i++ { + thisQR.columns = append(thisQR.columns, c[i].(string)) + thisQR.types = append(thisQR.types, t[i].(string)) + } + + // and values are an array of arrays + if thisResult["values"] != nil { + thisQR.values = thisResult["values"].([]interface{}) + } else { + trace("%s: fyi, no values this query", conn.ID) + } + + thisQR.rowNumber = -1 + + trace("%s: this result (#col,time) %d %f", conn.ID, len(thisQR.columns), thisQR.Timing) + results = append(results, thisQR) + } + + trace("%s: finished parsing, returning %d results", conn.ID, len(results)) + + if numStatementErrors > 0 { + return results, errors.New(fmt.Sprintf("there were %d statement errors", numStatementErrors)) + } else { + return results, nil + } +} + +/* ***************************************************************** + + type: QueryResult + + * *****************************************************************/ + +/* +A QueryResult type holds the results of a call to Query(). You could think of it as a rowset. + +So if you were to query: + + SELECT id, name FROM some_table; + +then a QueryResult would hold any errors from that query, a list of columns and types, and the actual row values. + +Query() returns an array of QueryResult vars, while QueryOne() returns a single variable. +*/ +type QueryResult struct { + conn *Connection + Err error + columns []string + types []string + Timing float64 + values []interface{} + rowNumber int64 +} + +// these are done as getters rather than as public +// variables to prevent monkey business by the user +// that would put us in an inconsistent state + +/* ***************************************************************** + + method: QueryResult.Columns() + + * *****************************************************************/ + +/* +Columns returns a list of the column names for this QueryResult. +*/ +func (qr *QueryResult) Columns() []string { + return qr.columns +} + +/* ***************************************************************** + + method: QueryResult.Map() + + * *****************************************************************/ + +/* +Map() returns the current row (as advanced by Next()) as a map[string]interface{} + +The key is a string corresponding to a column name. +The value is the corresponding column. + +Note that only json values are supported, so you will need to type the interface{} accordingly. +*/ +func (qr *QueryResult) Map() (map[string]interface{}, error) { + trace("%s: Map() called for row %d", qr.conn.ID, qr.rowNumber) + ans := make(map[string]interface{}) + + if qr.rowNumber == -1 { + return ans, errors.New("you need to Next() before you Map(), sorry, it's complicated") + } + + thisRowValues := qr.values[qr.rowNumber].([]interface{}) + for i := 0; i < len(qr.columns); i++ { + ans[qr.columns[i]] = thisRowValues[i] + } + + return ans, nil +} + +/* ***************************************************************** + + method: QueryResult.Next() + + * *****************************************************************/ + +/* +Next() positions the QueryResult result pointer so that Scan() or Map() is ready. + +You should call Next() first, but gorqlite will fix it if you call Map() or Scan() before +the initial Next(). + +A common idiom: + + rows := conn.Write(something) + for rows.Next() { + // your Scan/Map and processing here. + } +*/ +func (qr *QueryResult) Next() bool { + if qr.rowNumber >= int64(len(qr.values)-1) { + return false + } + + qr.rowNumber += 1 + return true +} + +/* ***************************************************************** + + method: QueryResult.NumRows() + + * *****************************************************************/ + +/* +NumRows() returns the number of rows returned by the query. +*/ +func (qr *QueryResult) NumRows() int64 { + return int64(len(qr.values)) +} + +/* ***************************************************************** + + method: QueryResult.RowNumber() + + * *****************************************************************/ + +/* +RowNumber() returns the current row number as Next() iterates through the result's rows. +*/ +func (qr *QueryResult) RowNumber() int64 { + return qr.rowNumber +} + +/* ***************************************************************** + + method: QueryResult.Scan() + + * *****************************************************************/ + +/* +Scan() takes a list of pointers and then updates them to reflect he current row's data. + +Note that only the following data types are used, and they +are a subset of the types JSON uses: + string, for JSON strings + float64, for JSON numbers + int64, as a convenient extension + nil for JSON null + +booleans, JSON arrays, and JSON objects are not supported, +since sqlite does not support them. +*/ +func (qr *QueryResult) Scan(dest ...interface{}) error { + trace("%s: Scan() called for %d vars", qr.conn.ID, len(dest)) + + if qr.rowNumber == -1 { + return errors.New("you need to Next() before you Scan(), sorry, it's complicated") + } + + if len(dest) != len(qr.columns) { + return errors.New(fmt.Sprintf("expected %d columns but got %d vars\n", len(qr.columns), len(dest))) + } + + thisRowValues := qr.values[qr.rowNumber].([]interface{}) + for n, d := range dest { + switch d.(type) { + case *int64: + f := int64(thisRowValues[n].(float64)) + *d.(*int64) = f + case *float64: + f := float64(thisRowValues[n].(float64)) + *d.(*float64) = f + case *string: + s := string(thisRowValues[n].(string)) + *d.(*string) = s + default: + return errors.New(fmt.Sprintf("unknown destination type to scan into in variable #%d", n)) + } + } + + return nil +} + +/* ***************************************************************** + + method: QueryResult.Types() + + * *****************************************************************/ + +/* +Types() returns an array of the column's types. + +Note that sqlite will repeat the type you tell it, but in many cases, it's ignored. So you can initialize a column as CHAR(3) but it's really TEXT. See https://www.sqlite.org/datatype3.html + +This info may additionally conflict with the reality that your data is being JSON encoded/decoded. +*/ +func (qr *QueryResult) Types() []string { + return qr.types +} diff --git a/vendor/git.xeserv.us/xena/gorqlite/write.go b/vendor/git.xeserv.us/xena/gorqlite/write.go new file mode 100644 index 0000000..0d7800b --- /dev/null +++ b/vendor/git.xeserv.us/xena/gorqlite/write.go @@ -0,0 +1,178 @@ +package gorqlite + +/* + this file has + Write() + WriteResult and its methods +*/ + +import "errors" +import "encoding/json" +import "fmt" + +/* ***************************************************************** + + method: Connection.Write() + + This is the JSON we get back: + +{ + "results": [ + { + "last_insert_id": 1, + "rows_affected": 1, + "time": 0.00759015 + }, + { + "last_insert_id": 2, + "rows_affected": 1, + "time": 0.00669015 + } + ], + "time": 0.869015 +} + + or + +{ + "results": [ + { + "error": "table foo already exists" + } + ], + "time": 0.18472685400000002 +} + + We don't care about the overall time. We just want the results, + so we'll take those and put each into a WriteResult + + Because the results themselves are smaller than the JSON + (which repeats strings like "last_insert_id" frequently), + we'll just parse everything at once. + + * *****************************************************************/ + +/* +WriteOne() is a convenience method that wraps Write() into a single-statement +method. +*/ + +func (conn *Connection) WriteOne(sqlStatement string) (wr WriteResult, err error) { + if conn.hasBeenClosed { + wr.Err = errClosed + return wr, errClosed + } + sqlStatements := make([]string, 0) + sqlStatements = append(sqlStatements, sqlStatement) + wra, err := conn.Write(sqlStatements) + return wra[0], err +} + +/* +Write() is used to perform DDL/DML in the database. ALTER, CREATE, DELETE, DROP, INSERT, UPDATE, etc. all go through Write(). + +Write() takes an array of SQL statements, and returns an equal-sized array of WriteResults, each corresponding to the SQL statement that produced it. + +All statements are executed as a single transaction. + +Write() returns an error if one is encountered during its operation. If it's something like a call to the rqlite API, then it'll return that error. If one statement out of several has an error, it will return a generic "there were %d statement errors" and you'll have to look at the individual statement's Err for more info. +*/ +func (conn *Connection) Write(sqlStatements []string) (results []WriteResult, err error) { + results = make([]WriteResult, 0) + + if conn.hasBeenClosed { + var errResult WriteResult + errResult.Err = errClosed + results = append(results, errResult) + return results, errClosed + } + + trace("%s: Write() for %d statements", conn.ID, len(sqlStatements)) + + response, err := conn.rqliteApiPost(api_WRITE, sqlStatements) + if err != nil { + trace("%s: rqliteApiCall() ERROR: %s", conn.ID, err.Error()) + var errResult WriteResult + errResult.Err = err + results = append(results, errResult) + return results, err + } + trace("%s: rqliteApiCall() OK", conn.ID) + + var sections map[string]interface{} + err = json.Unmarshal(response, §ions) + if err != nil { + trace("%s: json.Unmarshal() ERROR: %s", conn.ID, err.Error()) + var errResult WriteResult + errResult.Err = err + results = append(results, errResult) + return results, err + } + + /* + at this point, we have a "results" section and + a "time" section. we can igore the latter. + */ + + resultsArray := sections["results"].([]interface{}) + trace("%s: I have %d result(s) to parse", conn.ID, len(resultsArray)) + numStatementErrors := 0 + for n, k := range resultsArray { + trace("%s: starting on result %d", conn.ID, n) + thisResult := k.(map[string]interface{}) + + var thisWR WriteResult + thisWR.conn = conn + + // did we get an error? + _, ok := thisResult["error"] + if ok { + trace("%s: have an error on this result: %s", conn.ID, thisResult["error"].(string)) + thisWR.Err = errors.New(thisResult["error"].(string)) + results = append(results, thisWR) + numStatementErrors += 1 + continue + } + + _, ok = thisResult["last_insert_id"] + if ok { + thisWR.LastInsertID = int64(thisResult["last_insert_id"].(float64)) + } + + _, ok = thisResult["rows_affected"] // could be zero for a CREATE + if ok { + thisWR.RowsAffected = int64(thisResult["rows_affected"].(float64)) + } + thisWR.Timing = thisResult["time"].(float64) + + trace("%s: this result (LII,RA,T): %d %d %f", conn.ID, thisWR.LastInsertID, thisWR.RowsAffected, thisWR.Timing) + results = append(results, thisWR) + } + + trace("%s: finished parsing, returning %d results", conn.ID, len(results)) + + if numStatementErrors > 0 { + return results, errors.New(fmt.Sprintf("there were %d statement errors", numStatementErrors)) + } else { + return results, nil + } +} + +/* ***************************************************************** + + type: WriteResult + + * *****************************************************************/ + +/* +A WriteResult holds the result of a single statement sent to Write(). + +Write() returns an array of WriteResult vars, while WriteOne() returns a single WriteResult. +*/ +type WriteResult struct { + Err error // don't trust the rest if this isn't nil + Timing float64 + RowsAffected int64 // affected by the change + LastInsertID int64 // if relevant, otherwise zero value + conn *Connection +}