You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

views.go 5.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. package couchbase
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "io/ioutil"
  7. "math/rand"
  8. "net/http"
  9. "net/url"
  10. "time"
  11. )
  12. // ViewRow represents a single result from a view.
  13. //
  14. // Doc is present only if include_docs was set on the request.
  15. type ViewRow struct {
  16. ID string
  17. Key interface{}
  18. Value interface{}
  19. Doc *interface{}
  20. }
  21. // A ViewError is a node-specific error indicating a partial failure
  22. // within a view result.
  23. type ViewError struct {
  24. From string
  25. Reason string
  26. }
  27. func (ve ViewError) Error() string {
  28. return "Node: " + ve.From + ", reason: " + ve.Reason
  29. }
  30. // ViewResult holds the entire result set from a view request,
  31. // including the rows and the errors.
  32. type ViewResult struct {
  33. TotalRows int `json:"total_rows"`
  34. Rows []ViewRow
  35. Errors []ViewError
  36. }
  37. func (b *Bucket) randomBaseURL() (*url.URL, error) {
  38. nodes := b.HealthyNodes()
  39. if len(nodes) == 0 {
  40. return nil, errors.New("no available couch rest URLs")
  41. }
  42. nodeNo := rand.Intn(len(nodes))
  43. node := nodes[nodeNo]
  44. b.RLock()
  45. name := b.Name
  46. pool := b.pool
  47. b.RUnlock()
  48. u, err := ParseURL(node.CouchAPIBase)
  49. if err != nil {
  50. return nil, fmt.Errorf("config error: Bucket %q node #%d CouchAPIBase=%q: %v",
  51. name, nodeNo, node.CouchAPIBase, err)
  52. } else if pool != nil {
  53. u.User = pool.client.BaseURL.User
  54. }
  55. return u, err
  56. }
  57. const START_NODE_ID = -1
  58. func (b *Bucket) randomNextURL(lastNode int) (*url.URL, int, error) {
  59. nodes := b.HealthyNodes()
  60. if len(nodes) == 0 {
  61. return nil, -1, errors.New("no available couch rest URLs")
  62. }
  63. var nodeNo int
  64. if lastNode == START_NODE_ID || lastNode >= len(nodes) {
  65. // randomly select a node if the value of lastNode is invalid
  66. nodeNo = rand.Intn(len(nodes))
  67. } else {
  68. // wrap around the node list
  69. nodeNo = (lastNode + 1) % len(nodes)
  70. }
  71. b.RLock()
  72. name := b.Name
  73. pool := b.pool
  74. b.RUnlock()
  75. node := nodes[nodeNo]
  76. u, err := ParseURL(node.CouchAPIBase)
  77. if err != nil {
  78. return nil, -1, fmt.Errorf("config error: Bucket %q node #%d CouchAPIBase=%q: %v",
  79. name, nodeNo, node.CouchAPIBase, err)
  80. } else if pool != nil {
  81. u.User = pool.client.BaseURL.User
  82. }
  83. return u, nodeNo, err
  84. }
  85. // DocID is the document ID type for the startkey_docid parameter in
  86. // views.
  87. type DocID string
  88. func qParam(k, v string) string {
  89. format := `"%s"`
  90. switch k {
  91. case "startkey_docid", "endkey_docid", "stale":
  92. format = "%s"
  93. }
  94. return fmt.Sprintf(format, v)
  95. }
  96. // ViewURL constructs a URL for a view with the given ddoc, view name,
  97. // and parameters.
  98. func (b *Bucket) ViewURL(ddoc, name string,
  99. params map[string]interface{}) (string, error) {
  100. u, err := b.randomBaseURL()
  101. if err != nil {
  102. return "", err
  103. }
  104. values := url.Values{}
  105. for k, v := range params {
  106. switch t := v.(type) {
  107. case DocID:
  108. values[k] = []string{string(t)}
  109. case string:
  110. values[k] = []string{qParam(k, t)}
  111. case int:
  112. values[k] = []string{fmt.Sprintf(`%d`, t)}
  113. case bool:
  114. values[k] = []string{fmt.Sprintf(`%v`, t)}
  115. default:
  116. b, err := json.Marshal(v)
  117. if err != nil {
  118. return "", fmt.Errorf("unsupported value-type %T in Query, "+
  119. "json encoder said %v", t, err)
  120. }
  121. values[k] = []string{fmt.Sprintf(`%v`, string(b))}
  122. }
  123. }
  124. if ddoc == "" && name == "_all_docs" {
  125. u.Path = fmt.Sprintf("/%s/_all_docs", b.GetName())
  126. } else {
  127. u.Path = fmt.Sprintf("/%s/_design/%s/_view/%s", b.GetName(), ddoc, name)
  128. }
  129. u.RawQuery = values.Encode()
  130. return u.String(), nil
  131. }
  132. // ViewCallback is called for each view invocation.
  133. var ViewCallback func(ddoc, name string, start time.Time, err error)
  134. // ViewCustom performs a view request that can map row values to a
  135. // custom type.
  136. //
  137. // See the source to View for an example usage.
  138. func (b *Bucket) ViewCustom(ddoc, name string, params map[string]interface{},
  139. vres interface{}) (err error) {
  140. if SlowServerCallWarningThreshold > 0 {
  141. defer slowLog(time.Now(), "call to ViewCustom(%q, %q)", ddoc, name)
  142. }
  143. if ViewCallback != nil {
  144. defer func(t time.Time) { ViewCallback(ddoc, name, t, err) }(time.Now())
  145. }
  146. u, err := b.ViewURL(ddoc, name, params)
  147. if err != nil {
  148. return err
  149. }
  150. req, err := http.NewRequest("GET", u, nil)
  151. if err != nil {
  152. return err
  153. }
  154. ah := b.authHandler(false /* bucket not yet locked */)
  155. maybeAddAuth(req, ah)
  156. res, err := doHTTPRequest(req)
  157. if err != nil {
  158. return fmt.Errorf("error starting view req at %v: %v", u, err)
  159. }
  160. defer res.Body.Close()
  161. if res.StatusCode != 200 {
  162. bod := make([]byte, 512)
  163. l, _ := res.Body.Read(bod)
  164. return fmt.Errorf("error executing view req at %v: %v - %s",
  165. u, res.Status, bod[:l])
  166. }
  167. body, err := ioutil.ReadAll(res.Body)
  168. if err := json.Unmarshal(body, vres); err != nil {
  169. return nil
  170. }
  171. return nil
  172. }
  173. // View executes a view.
  174. //
  175. // The ddoc parameter is just the bare name of your design doc without
  176. // the "_design/" prefix.
  177. //
  178. // Parameters are string keys with values that correspond to couchbase
  179. // view parameters. Primitive should work fairly naturally (booleans,
  180. // ints, strings, etc...) and other values will attempt to be JSON
  181. // marshaled (useful for array indexing on on view keys, for example).
  182. //
  183. // Example:
  184. //
  185. // res, err := couchbase.View("myddoc", "myview", map[string]interface{}{
  186. // "group_level": 2,
  187. // "startkey_docid": []interface{}{"thing"},
  188. // "endkey_docid": []interface{}{"thing", map[string]string{}},
  189. // "stale": false,
  190. // })
  191. func (b *Bucket) View(ddoc, name string, params map[string]interface{}) (ViewResult, error) {
  192. vres := ViewResult{}
  193. if err := b.ViewCustom(ddoc, name, params, &vres); err != nil {
  194. //error in accessing views. Retry once after a bucket refresh
  195. b.Refresh()
  196. return vres, b.ViewCustom(ddoc, name, params, &vres)
  197. } else {
  198. return vres, nil
  199. }
  200. }