├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── database.go ├── database_test.go ├── design.go ├── design_test.go ├── doc.go ├── mapping.go ├── mapping_test.go ├── resource.go ├── server.go ├── server_test.go └── tools └── replicate.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | .idea/ 27 | *.iml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | services: 4 | - couchdb 5 | 6 | go: 7 | - 1.7.x 8 | - 1.8.x 9 | 10 | before_script: 11 | - go vet 12 | 13 | gobuild_args: -v -race 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2016, leesper 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CouchDB-Golang 2 | 3 | [![Build Status](https://travis-ci.org/leesper/couchdb-golang.svg?branch=master)](https://travis-ci.org/leesper/couchdb-golang) [![GoDoc](https://godoc.org/github.com/leesper/couchdb-golang?status.svg)](http://godoc.org/github.com/leesper/couchdb-golang) [![GitHub license](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://raw.githubusercontent.com/leesper/couchdb-golang/master/LICENSE) 4 | 5 | A Golang library for CouchDB 2.x, inspired by [CouchDB-Python](https://github.com/djc/couchdb-python). 6 | 7 | ## Features 8 | 9 | * Resource : a simple wrapper for HTTP requests and error handling 10 | * Server : CouchDB server instance 11 | * Database : CouchDB database instance 12 | * ViewResults : a representation of the results produced by design document views 13 | * ViewDefinition : a definition of view stored in a specific design document 14 | * Document : a representation of document object in database 15 | * tools/replicate : a command-line tool for replicating 16 | 17 | ```go 18 | func (d *Database) Query(fields []string, selector string, sorts []string, limit, skip, index interface{}) ([]map[string]interface{}, error) 19 | ``` 20 | 21 | You can query documents using a conditional selector statement in Golang. It will converts to the corresponding JSON query string. 22 | 23 | * **selector**: A filter string declaring which documents to return, formatted as a Golang statement. 24 | * **fields**: Specifying which fields to be returned, if passing nil the entire is returned, no automatic inclusion of \_id or other metadata fields. 25 | * **sorts**: How to order the documents returned, formatted as ["desc(fieldName1)", "desc(fieldName2)"] or ["fieldNameA", "fieldNameB"] of which "asc" is used by default, passing nil to disable ordering. 26 | * **limit**: Maximum number of results returned, passing nil to use default value(25). 27 | * **skip**: Skip the first 'n' results, where 'n' is the number specified, passing nil for no-skip. 28 | * **index**: Instruct a query to use a specific index, specified either as "" or ["", ""], passing nil to use primary index(\_all_docs) by default. 29 | 30 | For example: 31 | ```go 32 | docsQuery, err := movieDB.Query(nil, `year == 1989 && (director == "Ademir Kenovic" || director == "Dezs Garas")`, nil, nil, nil, nil) 33 | ``` 34 | equals to: 35 | ```go 36 | docsRaw, err := movieDB.QueryJSON(` 37 | { 38 | "selector": { 39 | "year": 1989, 40 | "$or": [ 41 | { "director": "Ademir Kenovic" }, 42 | { "director": "Dezs Garas" } 43 | ] 44 | } 45 | }`) 46 | ``` 47 | 48 | ### Inner functions for selector syntax 49 | 50 | * **nor(condexprs...)** matches if none of the conditions in condexprs match($nor). 51 | For example: nor(year == 1990, year == 1989, year == 1997) returns all documents whose year field not in 1989, 1990 and 1997. 52 | 53 | * **all(field, array)** matches an array value if it contains all the elements of the argument array($all). 54 | For example: all(genre, []string{"Comedy", "Short"} returns all documents whose genre field contains "Comedy" and "Short". 55 | 56 | * **any(field, condexpr)** matches an array field with at least one element meets the specified condition($elemMatch). 57 | For example: any(genre, genre == "Short" || genre == "Horror") returns all documents whose genre field contains "Short" or "Horror" or both. 58 | 59 | * **exists(field, boolean)** checks whether the field exists or not, regardless of its value($exists). 60 | For example: exists(director, false) returns all documents who does not have a director field. 61 | 62 | * **typeof(field, type)** checks the document field's type, valid types are "null", "boolean", "number", "string", "array", "object"($type). 63 | For example: typeof(genre, "array") returns all documents whose genre field is of array type. 64 | 65 | * **in(field, array)** the field must exist in the array provided($in). 66 | For example: in(director, []string{"Mike Portnoy", "Vitali Kanevsky"}) returns all documents whose director field is "Mike Portnoy" or "Vitali Kanevsky". 67 | 68 | * **nin(field, array)** the document field must not exist in the array provided($nin). 69 | For example: nin(year, []int{1990, 1992, 1998}) returns all documents whose year field is not in 1990, 1992 or 1998. 70 | 71 | * **size(field, int)** matches the length of an array field in a document($size). 72 | For example: size(genre, 2) returns all documents whose genre field is of length 2. 73 | 74 | * **mod(field, divisor, remainder)** matches documents where field % divisor == remainder($mod). 75 | For example: mod(year, 2, 1) returns all documents whose year field is an odd number. 76 | 77 | * **regex(field, regexstr)** a regular expression pattern to match against the document field. 78 | For example: regex(title, "^A") returns all documents whose title is begin with an "A". 79 | 80 | ### Inner functions for sort syntax 81 | 82 | **asc(field)** sorts the field in ascending order, this is the default option while desc(field) sorts the field in descending order. 83 | 84 | ## Requirements 85 | 86 | * Golang 1.7.x and above 87 | 88 | ## Installation 89 | 90 | `go get -u -v github.com/leesper/couchdb-golang` 91 | 92 | ## Authors and acknowledgment 93 | 94 | * [Philipp Winter](https://github.com/philippwinter) 95 | * [Serkan Sipahi](https://github.com/SerkanSipahi) 96 | * [paduraru](https://github.com/paduraru) 97 | * [Andrei Pavel](https://github.com/andreipavelQ) 98 | * [jcantonio](https://github.com/jcantonio) 99 | 100 | ## Contributing 101 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. Please make sure to update unit tests as appropriate. 102 | -------------------------------------------------------------------------------- /database.go: -------------------------------------------------------------------------------- 1 | package couchdb 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "go/ast" 10 | "go/parser" 11 | "go/token" 12 | "math" 13 | "net/http" 14 | "net/url" 15 | "os" 16 | "reflect" 17 | "strconv" 18 | "strings" 19 | ) 20 | 21 | const ( 22 | // DefaultBaseURL is the default address of CouchDB server. 23 | DefaultBaseURL = "http://localhost:5984" 24 | ) 25 | 26 | var ( 27 | // ErrBatchValue for invalid batch parameter of IterView 28 | ErrBatchValue = errors.New("batch must be 1 or more") 29 | // ErrLimitValue for invalid limit parameter of IterView 30 | ErrLimitValue = errors.New("limit must be 1 or more") 31 | ) 32 | 33 | // getDefaultCouchDBURL returns the default CouchDB server url. 34 | func getDefaultCouchDBURL() string { 35 | var couchdbURLEnviron string 36 | for _, couchdbURLEnviron = range os.Environ() { 37 | if strings.HasPrefix(couchdbURLEnviron, "COUCHDB_URL") { 38 | break 39 | } 40 | } 41 | if len(couchdbURLEnviron) == 0 { 42 | couchdbURLEnviron = DefaultBaseURL 43 | } else { 44 | couchdbURLEnviron = strings.Split(couchdbURLEnviron, "=")[1] 45 | } 46 | return couchdbURLEnviron 47 | } 48 | 49 | // Database represents a CouchDB database instance. 50 | type Database struct { 51 | resource *Resource 52 | } 53 | 54 | // NewDatabase returns a CouchDB database instance. 55 | func NewDatabase(urlStr string) (*Database, error) { 56 | var dbURLStr string 57 | if !strings.HasPrefix(urlStr, "http") { 58 | base, err := url.Parse(getDefaultCouchDBURL()) 59 | if err != nil { 60 | return nil, err 61 | } 62 | dbURL, err := base.Parse(urlStr) 63 | if err != nil { 64 | return nil, err 65 | } 66 | dbURLStr = dbURL.String() 67 | } else { 68 | dbURLStr = urlStr 69 | } 70 | 71 | res, err := NewResource(dbURLStr, nil) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | return newDatabase(res) 77 | } 78 | 79 | // NewDatabaseWithResource returns a CouchDB database instance with resource obj. 80 | func NewDatabaseWithResource(res *Resource) (*Database, error) { 81 | return newDatabase(res) 82 | } 83 | 84 | func newDatabase(res *Resource) (*Database, error) { 85 | return &Database{ 86 | resource: res, 87 | }, nil 88 | } 89 | 90 | // Available returns error if the database is not good to go. 91 | func (d *Database) Available() error { 92 | _, _, err := d.resource.Head("", nil, nil) 93 | return err 94 | } 95 | 96 | // Save creates a new document or update an existing document. 97 | // If doc has no _id the server will generate a random UUID and a new document will be created. 98 | // Otherwise the doc's _id will be used to identify the document to create or update. 99 | // Trying to update an existing document with an incorrect _rev will cause failure. 100 | // *NOTE* It is recommended to avoid saving doc without _id and instead generate document ID on client side. 101 | // To avoid such problems you can generate a UUID on the client side. 102 | // GenerateUUID provides a simple, platform-independent implementation. 103 | // You can also use other third-party packages instead. 104 | // doc: the document to create or update. 105 | func (d *Database) Save(doc map[string]interface{}, options url.Values) (string, string, error) { 106 | var id, rev string 107 | 108 | var httpFunc func(string, http.Header, map[string]interface{}, url.Values) (http.Header, []byte, error) 109 | if v, ok := doc["_id"]; ok { 110 | httpFunc = docResource(d.resource, v.(string)).PutJSON 111 | } else { 112 | httpFunc = d.resource.PostJSON 113 | } 114 | 115 | _, data, err := httpFunc("", nil, doc, options) 116 | if err != nil { 117 | return id, rev, err 118 | } 119 | 120 | var jsonMap map[string]interface{} 121 | jsonMap, err = parseData(data) 122 | if err != nil { 123 | return id, rev, err 124 | } 125 | 126 | if v, ok := jsonMap["id"]; ok { 127 | id = v.(string) 128 | doc["_id"] = id 129 | } 130 | 131 | if v, ok := jsonMap["rev"]; ok { 132 | rev = v.(string) 133 | doc["_rev"] = rev 134 | } 135 | 136 | return id, rev, nil 137 | } 138 | 139 | // Get returns the document with the specified ID. 140 | func (d *Database) Get(docid string, options url.Values) (map[string]interface{}, error) { 141 | docRes := docResource(d.resource, docid) 142 | _, data, err := docRes.GetJSON("", nil, options) 143 | if err != nil { 144 | return nil, err 145 | } 146 | var doc map[string]interface{} 147 | doc, err = parseData(data) 148 | if err != nil { 149 | return nil, err 150 | } 151 | return doc, nil 152 | } 153 | 154 | // Delete deletes the document with the specified ID. 155 | func (d *Database) Delete(docid string) error { 156 | docRes := docResource(d.resource, docid) 157 | header, _, err := docRes.Head("", nil, nil) 158 | if err != nil { 159 | return err 160 | } 161 | rev := strings.Trim(header.Get("ETag"), `"`) 162 | return deleteDoc(docRes, rev) 163 | } 164 | 165 | // DeleteDoc deletes the specified document 166 | func (d *Database) DeleteDoc(doc map[string]interface{}) error { 167 | id, ok := doc["_id"] 168 | if !ok || id == nil { 169 | return errors.New("document ID not existed") 170 | } 171 | 172 | rev, ok := doc["_rev"] 173 | if !ok || rev == nil { 174 | return errors.New("document rev not existed") 175 | } 176 | 177 | docRes := docResource(d.resource, id.(string)) 178 | return deleteDoc(docRes, rev.(string)) 179 | } 180 | 181 | func deleteDoc(docRes *Resource, rev string) error { 182 | _, _, err := docRes.DeleteJSON("", nil, url.Values{"rev": []string{rev}}) 183 | return err 184 | } 185 | 186 | // Set creates or updates a document with the specified ID. 187 | func (d *Database) Set(docid string, doc map[string]interface{}) error { 188 | docRes := docResource(d.resource, docid) 189 | _, data, err := docRes.PutJSON("", nil, doc, nil) 190 | if err != nil { 191 | return err 192 | } 193 | 194 | result, err := parseData(data) 195 | if err != nil { 196 | return err 197 | } 198 | 199 | doc["_id"] = result["id"].(string) 200 | doc["_rev"] = result["rev"].(string) 201 | return nil 202 | } 203 | 204 | // Contains returns true if the database contains a document with the specified ID. 205 | func (d *Database) Contains(docid string) error { 206 | docRes := docResource(d.resource, docid) 207 | _, _, err := docRes.Head("", nil, nil) 208 | return err 209 | } 210 | 211 | // UpdateResult represents result of an update. 212 | type UpdateResult struct { 213 | ID, Rev string 214 | Err error 215 | } 216 | 217 | // Update performs a bulk update or creation of the given documents in a single HTTP request. 218 | // It returns a 3-tuple (id, rev, error) 219 | func (d *Database) Update(docs []map[string]interface{}, options map[string]interface{}) ([]UpdateResult, error) { 220 | results := make([]UpdateResult, len(docs)) 221 | body := map[string]interface{}{} 222 | if options != nil { 223 | for k, v := range options { 224 | body[k] = v 225 | } 226 | } 227 | body["docs"] = docs 228 | 229 | _, data, err := d.resource.PostJSON("_bulk_docs", nil, body, nil) 230 | if err != nil { 231 | return nil, err 232 | } 233 | var jsonArr []map[string]interface{} 234 | err = json.Unmarshal(data, &jsonArr) 235 | if err != nil { 236 | return nil, err 237 | } 238 | 239 | for i, v := range jsonArr { 240 | var retErr error 241 | var result UpdateResult 242 | if val, ok := v["error"]; ok { 243 | errMsg := val.(string) 244 | switch errMsg { 245 | case "conflict": 246 | retErr = ErrConflict 247 | case "forbidden": 248 | retErr = ErrForbidden 249 | default: 250 | retErr = ErrInternalServerError 251 | } 252 | result = UpdateResult{ 253 | ID: v["id"].(string), 254 | Rev: "", 255 | Err: retErr, 256 | } 257 | } else { 258 | id, rev := v["id"].(string), v["rev"].(string) 259 | result = UpdateResult{ 260 | ID: id, 261 | Rev: rev, 262 | Err: retErr, 263 | } 264 | doc := docs[i] 265 | doc["_id"] = id 266 | doc["_rev"] = rev 267 | } 268 | results[i] = result 269 | } 270 | return results, nil 271 | } 272 | 273 | // DocIDs returns the IDs of all documents in database. 274 | func (d *Database) DocIDs() ([]string, error) { 275 | docRes := docResource(d.resource, "_all_docs") 276 | _, data, err := docRes.GetJSON("", nil, nil) 277 | if err != nil { 278 | return nil, err 279 | } 280 | var jsonMap map[string]*json.RawMessage 281 | err = json.Unmarshal(data, &jsonMap) 282 | if err != nil { 283 | return nil, err 284 | } 285 | var jsonArr []*json.RawMessage 286 | json.Unmarshal(*jsonMap["rows"], &jsonArr) 287 | ids := make([]string, len(jsonArr)) 288 | for i, v := range jsonArr { 289 | var row map[string]interface{} 290 | err = json.Unmarshal(*v, &row) 291 | if err != nil { 292 | return ids, err 293 | } 294 | ids[i] = row["id"].(string) 295 | } 296 | return ids, nil 297 | } 298 | 299 | // Name returns the name of database. 300 | func (d *Database) Name() (string, error) { 301 | var name string 302 | info, err := d.Info("") 303 | if err != nil { 304 | return name, err 305 | } 306 | return info["db_name"].(string), nil 307 | } 308 | 309 | // Info returns the information about the database or design document 310 | func (d *Database) Info(ddoc string) (map[string]interface{}, error) { 311 | var data []byte 312 | var err error 313 | if ddoc == "" { 314 | _, data, err = d.resource.GetJSON("", nil, url.Values{}) 315 | if err != nil { 316 | return nil, err 317 | } 318 | } else { 319 | _, data, err = d.resource.GetJSON(fmt.Sprintf("_design/%s/_info", ddoc), nil, nil) 320 | if err != nil { 321 | return nil, err 322 | } 323 | } 324 | 325 | var info map[string]interface{} 326 | err = json.Unmarshal(data, &info) 327 | if err != nil { 328 | return nil, err 329 | } 330 | 331 | return info, nil 332 | } 333 | 334 | func (d *Database) String() string { 335 | return fmt.Sprintf("Database %s", d.resource.base) 336 | } 337 | 338 | // Commit flushes any recent changes to the specified database to disk. 339 | // If the server is configured to delay commits or previous requests use the special 340 | // "X-Couch-Full-Commit: false" header to disable immediate commits, this method 341 | // can be used to ensure that non-commited changes are commited to physical storage. 342 | func (d *Database) Commit() error { 343 | _, _, err := d.resource.PostJSON("_ensure_full_commit", nil, nil, nil) 344 | return err 345 | } 346 | 347 | // Compact compacts the database by compressing the disk database file. 348 | func (d *Database) Compact() error { 349 | _, _, err := d.resource.PostJSON("_compact", nil, nil, nil) 350 | return err 351 | } 352 | 353 | // Revisions returns all available revisions of the given document in reverse 354 | // order, e.g. latest first. 355 | func (d *Database) Revisions(docid string, options url.Values) ([]map[string]interface{}, error) { 356 | docRes := docResource(d.resource, docid) 357 | _, data, err := docRes.GetJSON("", nil, url.Values{"revs": []string{"true"}}) 358 | if err != nil { 359 | return nil, err 360 | } 361 | var jsonMap map[string]*json.RawMessage 362 | err = json.Unmarshal(data, &jsonMap) 363 | if err != nil { 364 | return nil, err 365 | } 366 | var revsMap map[string]interface{} 367 | err = json.Unmarshal(*jsonMap["_revisions"], &revsMap) 368 | if err != nil { 369 | return nil, err 370 | } 371 | startRev := int(revsMap["start"].(float64)) 372 | val := reflect.ValueOf(revsMap["ids"]) 373 | if options == nil { 374 | options = url.Values{} 375 | } 376 | docs := make([]map[string]interface{}, val.Len()) 377 | for i := 0; i < val.Len(); i++ { 378 | rev := fmt.Sprintf("%d-%s", startRev-i, val.Index(i).Interface().(string)) 379 | options.Set("rev", rev) 380 | doc, err := d.Get(docid, options) 381 | if err != nil { 382 | return nil, err 383 | } 384 | docs[i] = doc 385 | } 386 | return docs, nil 387 | } 388 | 389 | // GetAttachment returns the file attachment associated with the document. 390 | // The raw data is returned as a []byte. 391 | func (d *Database) GetAttachment(doc map[string]interface{}, name string) ([]byte, error) { 392 | docid, ok := doc["_id"] 393 | if !ok { 394 | return nil, errors.New("doc _id not existed") 395 | } 396 | return d.getAttachment(docid.(string), name) 397 | } 398 | 399 | // GetAttachmentID returns the file attachment associated with the document ID. 400 | // The raw data is returned as []byte. 401 | func (d *Database) GetAttachmentID(docid, name string) ([]byte, error) { 402 | return d.getAttachment(docid, name) 403 | } 404 | 405 | func (d *Database) getAttachment(docid, name string) ([]byte, error) { 406 | docRes := docResource(docResource(d.resource, docid), name) 407 | _, data, err := docRes.Get("", nil, nil) 408 | return data, err 409 | } 410 | 411 | // PutAttachment uploads the supplied []byte as an attachment to the specified document. 412 | // doc: the document that the attachment belongs to. Must have _id and _rev inside. 413 | // content: the data to be attached to doc. 414 | // name: name of attachment. 415 | // mimeType: MIME type of content. 416 | func (d *Database) PutAttachment(doc map[string]interface{}, content []byte, name, mimeType string) error { 417 | if id, ok := doc["_id"]; !ok || id.(string) == "" { 418 | return errors.New("doc _id not existed") 419 | } 420 | if rev, ok := doc["_rev"]; !ok || rev.(string) == "" { 421 | return errors.New("doc _rev not extisted") 422 | } 423 | 424 | id, rev := doc["_id"].(string), doc["_rev"].(string) 425 | 426 | docRes := docResource(docResource(d.resource, id), name) 427 | header := http.Header{} 428 | header.Set("Content-Type", mimeType) 429 | params := url.Values{} 430 | params.Set("rev", rev) 431 | 432 | _, data, err := docRes.Put("", header, content, params) 433 | if err != nil { 434 | return err 435 | } 436 | 437 | result, err := parseData(data) 438 | if err != nil { 439 | return err 440 | } 441 | 442 | doc["_rev"] = result["rev"].(string) 443 | return nil 444 | } 445 | 446 | // DeleteAttachment deletes the specified attachment 447 | func (d *Database) DeleteAttachment(doc map[string]interface{}, name string) error { 448 | if id, ok := doc["_id"]; !ok || id.(string) == "" { 449 | return errors.New("doc _id not existed") 450 | } 451 | if rev, ok := doc["_rev"]; !ok || rev.(string) == "" { 452 | return errors.New("doc _rev not extisted") 453 | } 454 | 455 | id, rev := doc["_id"].(string), doc["_rev"].(string) 456 | 457 | params := url.Values{} 458 | params.Set("rev", rev) 459 | docRes := docResource(docResource(d.resource, id), name) 460 | _, data, err := docRes.DeleteJSON("", nil, params) 461 | if err != nil { 462 | return err 463 | } 464 | 465 | result, err := parseData(data) 466 | if err != nil { 467 | return err 468 | } 469 | doc["_rev"] = result["rev"] 470 | 471 | return nil 472 | } 473 | 474 | // Copy copies an existing document to a new or existing document. 475 | func (d *Database) Copy(srcID, destID, destRev string) (string, error) { 476 | docRes := docResource(d.resource, srcID) 477 | var destination string 478 | if destRev != "" { 479 | destination = fmt.Sprintf("%s?rev=%s", destID, destRev) 480 | } else { 481 | destination = destID 482 | } 483 | header := http.Header{ 484 | "Destination": []string{destination}, 485 | } 486 | _, data, err := request("COPY", docRes.base, header, nil, nil) 487 | var rev string 488 | if err != nil { 489 | return rev, err 490 | } 491 | result, err := parseData(data) 492 | if err != nil { 493 | return rev, err 494 | } 495 | rev = result["rev"].(string) 496 | return rev, nil 497 | } 498 | 499 | // Changes returns a sorted list of changes feed made to documents in the database. 500 | func (d *Database) Changes(options url.Values) (map[string]interface{}, error) { 501 | _, data, err := d.resource.GetJSON("_changes", nil, options) 502 | if err != nil { 503 | return nil, err 504 | } 505 | result, err := parseData(data) 506 | return result, err 507 | } 508 | 509 | // Purge performs complete removing of the given documents. 510 | func (d *Database) Purge(docs []map[string]interface{}) (map[string]interface{}, error) { 511 | revs := map[string][]string{} 512 | for _, doc := range docs { 513 | id, rev := doc["_id"].(string), doc["_rev"].(string) 514 | if _, ok := revs[id]; !ok { 515 | revs[id] = []string{} 516 | } 517 | revs[id] = append(revs[id], rev) 518 | } 519 | 520 | body := map[string]interface{}{} 521 | for k, v := range revs { 522 | body[k] = v 523 | } 524 | _, data, err := d.resource.PostJSON("_purge", nil, body, nil) 525 | if err != nil { 526 | return nil, err 527 | } 528 | 529 | return parseData(data) 530 | } 531 | 532 | func parseData(data []byte) (map[string]interface{}, error) { 533 | result := map[string]interface{}{} 534 | err := json.Unmarshal(data, &result) 535 | if err != nil { 536 | return result, err 537 | } 538 | if _, ok := result["error"]; ok { 539 | reason := result["reason"].(string) 540 | return result, errors.New(reason) 541 | } 542 | return result, nil 543 | } 544 | 545 | func parseRaw(data []byte) (map[string]*json.RawMessage, error) { 546 | result := map[string]*json.RawMessage{} 547 | err := json.Unmarshal(data, &result) 548 | if err != nil { 549 | return result, err 550 | } 551 | if _, ok := result["error"]; ok { 552 | var reason string 553 | json.Unmarshal(*result["reason"], &reason) 554 | return result, errors.New(reason) 555 | } 556 | return result, nil 557 | } 558 | 559 | // GenerateUUID returns a random 128-bit UUID 560 | func GenerateUUID() string { 561 | b := make([]byte, 16) 562 | _, err := rand.Read(b) 563 | if err != nil { 564 | return "" 565 | } 566 | 567 | uuid := fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) 568 | return uuid 569 | } 570 | 571 | // SetSecurity sets the security object for the given database. 572 | func (d *Database) SetSecurity(securityDoc map[string]interface{}) error { 573 | _, _, err := d.resource.PutJSON("_security", nil, securityDoc, nil) 574 | return err 575 | } 576 | 577 | // GetSecurity returns the current security object from the given database. 578 | func (d *Database) GetSecurity() (map[string]interface{}, error) { 579 | _, data, err := d.resource.GetJSON("_security", nil, nil) 580 | if err != nil { 581 | return nil, err 582 | } 583 | return parseData(data) 584 | } 585 | 586 | // Len returns the number of documents stored in it. 587 | func (d *Database) Len() (int, error) { 588 | info, err := d.Info("") 589 | if err != nil { 590 | return 0, err 591 | } 592 | return int(info["doc_count"].(float64)), nil 593 | } 594 | 595 | // GetRevsLimit gets the current revs_limit(revision limit) setting. 596 | func (d *Database) GetRevsLimit() (int, error) { 597 | _, data, err := d.resource.Get("_revs_limit", nil, nil) 598 | if err != nil { 599 | return 0, err 600 | } 601 | limit, err := strconv.Atoi(strings.Trim(string(data), "\n")) 602 | if err != nil { 603 | return limit, err 604 | } 605 | return limit, nil 606 | } 607 | 608 | // SetRevsLimit sets the maximum number of document revisions that will be 609 | // tracked by CouchDB. 610 | func (d *Database) SetRevsLimit(limit int) error { 611 | _, _, err := d.resource.Put("_revs_limit", nil, []byte(strconv.Itoa(limit)), nil) 612 | return err 613 | } 614 | 615 | // docResource returns a Resource instance for docID 616 | func docResource(res *Resource, docID string) *Resource { 617 | if len(docID) == 0 { 618 | return res 619 | } 620 | 621 | docRes := res 622 | if docID[:1] == "_" { 623 | paths := strings.SplitN(docID, "/", 2) 624 | for _, p := range paths { 625 | docRes, _ = docRes.NewResourceWithURL(p) 626 | } 627 | return docRes 628 | } 629 | 630 | docRes, _ = res.NewResourceWithURL(url.QueryEscape(docID)) 631 | return docRes 632 | } 633 | 634 | // Cleanup removes all view index files no longer required by CouchDB. 635 | func (d *Database) Cleanup() error { 636 | _, _, err := d.resource.PostJSON("_view_cleanup", nil, nil, nil) 637 | return err 638 | } 639 | 640 | // Query returns documents using a conditional selector statement in Golang. 641 | // 642 | // selector: A filter string declaring which documents to return, formatted as a Golang statement. 643 | // 644 | // fields: Specifying which fields to be returned, if passing nil the entire 645 | // is returned, no automatic inclusion of _id or other metadata fields. 646 | // 647 | // sorts: How to order the documents returned, formatted as ["desc(fieldName1)", "desc(fieldName2)"] 648 | // or ["fieldNameA", "fieldNameB"] of which "asc" is used by default, passing nil to disable ordering. 649 | // 650 | // limit: Maximum number of results returned, passing nil to use default value(25). 651 | // 652 | // skip: Skip the first 'n' results, where 'n' is the number specified, passing nil for no-skip. 653 | // 654 | // index: Instruct a query to use a specific index, specified either as "" or 655 | // ["", ""], passing nil to use primary index(_all_docs) by default. 656 | // 657 | // Inner functions for selector syntax 658 | // 659 | // nor(condexprs...) matches if none of the conditions in condexprs match($nor). 660 | // 661 | // For example: nor(year == 1990, year == 1989, year == 1997) returns all documents 662 | // whose year field not in 1989, 1990 and 1997. 663 | // 664 | // all(field, array) matches an array value if it contains all the elements of the argument array($all). 665 | // 666 | // For example: all(genre, []string{"Comedy", "Short"} returns all documents whose 667 | // genre field contains "Comedy" and "Short". 668 | // 669 | // any(field, condexpr) matches an array field with at least one element meets the specified condition($elemMatch). 670 | // 671 | // For example: any(genre, genre == "Short" || genre == "Horror") returns all documents whose 672 | // genre field contains "Short" or "Horror" or both. 673 | // 674 | // exists(field, boolean) checks whether the field exists or not, regardless of its value($exists). 675 | // 676 | // For example: exists(director, false) returns all documents who does not have a director field. 677 | // 678 | // typeof(field, type) checks the document field's type, valid types are 679 | // "null", "boolean", "number", "string", "array", "object"($type). 680 | // 681 | // For example: typeof(genre, "array") returns all documents whose genre field is of array type. 682 | // 683 | // in(field, array) the field must exist in the array provided($in). 684 | // 685 | // For example: in(director, []string{"Mike Portnoy", "Vitali Kanevsky"}) returns all documents 686 | // whose director field is "Mike Portnoy" or "Vitali Kanevsky". 687 | // 688 | // nin(field, array) the document field must not exist in the array provided($nin). 689 | // 690 | // For example: nin(year, []int{1990, 1992, 1998}) returns all documents whose year field is not 691 | // in 1990, 1992 or 1998. 692 | // 693 | // size(field, int) matches the length of an array field in a document($size). 694 | // 695 | // For example: size(genre, 2) returns all documents whose genre field is of length 2. 696 | // 697 | // mod(field, divisor, remainder) matches documents where field % divisor == remainder($mod). 698 | // 699 | // For example: mod(year, 2, 1) returns all documents whose year field is an odd number. 700 | // 701 | // regex(field, regexstr) a regular expression pattern to match against the document field. 702 | // 703 | // For example: regex(title, "^A") returns all documents whose title is begin with an "A". 704 | // 705 | // Inner functions for sort syntax 706 | // 707 | // asc(field) sorts the field in ascending order, this is the default option while 708 | // desc(field) sorts the field in descending order. 709 | func (d *Database) Query(fields []string, selector string, sorts []string, limit, skip, index interface{}) ([]map[string]interface{}, error) { 710 | selectorJSON, err := parseSelectorSyntax(selector) 711 | if err != nil { 712 | return nil, err 713 | } 714 | find := map[string]interface{}{ 715 | "selector": selectorJSON, 716 | } 717 | 718 | if limitVal, ok := limit.(int); ok { 719 | find["limit"] = limitVal 720 | } 721 | 722 | if skipVal, ok := skip.(int); ok { 723 | find["skip"] = skipVal 724 | } 725 | 726 | if sorts != nil { 727 | sortsJSON, err := parseSortSyntax(sorts) 728 | if err != nil { 729 | return nil, err 730 | } 731 | find["sort"] = sortsJSON 732 | } 733 | 734 | if fields != nil { 735 | find["fields"] = fields 736 | } 737 | 738 | if index != nil { 739 | find["use_index"] = index 740 | } 741 | 742 | return d.queryJSON(find) 743 | } 744 | 745 | // QueryJSON returns documents using a declarative JSON querying syntax. 746 | func (d *Database) QueryJSON(query string) ([]map[string]interface{}, error) { 747 | queryMap := map[string]interface{}{} 748 | err := json.Unmarshal([]byte(query), &queryMap) 749 | if err != nil { 750 | return nil, err 751 | } 752 | return d.queryJSON(queryMap) 753 | } 754 | 755 | func (d *Database) queryJSON(queryMap map[string]interface{}) ([]map[string]interface{}, error) { 756 | _, data, err := d.resource.PostJSON("_find", nil, queryMap, nil) 757 | if err != nil { 758 | return nil, err 759 | } 760 | 761 | result, err := parseRaw(data) 762 | if err != nil { 763 | return nil, err 764 | } 765 | 766 | docs := []map[string]interface{}{} 767 | err = json.Unmarshal(*result["docs"], &docs) 768 | if err != nil { 769 | return nil, err 770 | } 771 | return docs, nil 772 | } 773 | 774 | // parseSelectorSyntax returns a map representing the selector JSON struct. 775 | func parseSelectorSyntax(selector string) (interface{}, error) { 776 | // protect selector against query selector injection attacks 777 | if strings.Contains(selector, "$") { 778 | return nil, fmt.Errorf("no $s are allowed in selector: %s", selector) 779 | } 780 | 781 | // parse selector into abstract syntax tree (ast) 782 | expr, err := parser.ParseExpr(selector) 783 | if err != nil { 784 | return nil, err 785 | } 786 | 787 | // recursively processing ast into json object 788 | selectObj, err := parseAST(expr) 789 | if err != nil { 790 | return nil, err 791 | } 792 | 793 | return selectObj, nil 794 | } 795 | 796 | // parseSortSyntax returns a slice of sort JSON struct. 797 | func parseSortSyntax(sorts []string) (interface{}, error) { 798 | if sorts == nil { 799 | return nil, nil 800 | } 801 | 802 | sortObjs := []interface{}{} 803 | for _, sort := range sorts { 804 | sortExpr, err := parser.ParseExpr(sort) 805 | if err != nil { 806 | return nil, err 807 | } 808 | 809 | sortObj, err := parseAST(sortExpr) 810 | if err != nil { 811 | return nil, err 812 | } 813 | sortObjs = append(sortObjs, sortObj) 814 | } 815 | 816 | return sortObjs, nil 817 | } 818 | 819 | // parseAST converts and returns a JSON struct according to 820 | // CouchDB mango query syntax for the abstract syntax tree represented by expr. 821 | func parseAST(expr ast.Expr) (interface{}, error) { 822 | switch expr := expr.(type) { 823 | case *ast.BinaryExpr: 824 | // fmt.Println("BinaryExpr", expr) 825 | return parseBinary(expr.Op, expr.X, expr.Y) 826 | case *ast.UnaryExpr: 827 | // fmt.Println("UnaryExpr", expr) 828 | return parseUnary(expr.Op, expr.X) 829 | case *ast.CallExpr: 830 | // fmt.Println("CallExpr", expr, expr.Fun, expr.Args) 831 | return parseFuncCall(expr.Fun, expr.Args) 832 | case *ast.Ident: 833 | // fmt.Println("Ident", expr) 834 | switch expr.Name { 835 | case "nil": // for nil value such as _id > nil 836 | return nil, nil 837 | case "true": // for boolean value true 838 | return true, nil 839 | case "false": 840 | return false, nil // for boolean value false 841 | default: 842 | return expr.Name, nil 843 | } 844 | case *ast.BasicLit: 845 | // fmt.Println("BasicLit", expr) 846 | switch expr.Kind { 847 | case token.INT: 848 | intVal, err := strconv.Atoi(expr.Value) 849 | if err != nil { 850 | return nil, err 851 | } 852 | return intVal, nil 853 | case token.FLOAT: 854 | floatVal, err := strconv.ParseFloat(expr.Value, 64) 855 | if err != nil { 856 | return nil, err 857 | } 858 | return floatVal, nil 859 | case token.STRING: 860 | return strings.Trim(expr.Value, `"`), nil 861 | default: 862 | return nil, fmt.Errorf("token type %s not supported", expr.Kind.String()) 863 | } 864 | case *ast.SelectorExpr: 865 | // fmt.Println("SelectorExpr", expr.X, expr.Sel) 866 | xExpr, err := parseAST(expr.X) 867 | if err != nil { 868 | return nil, err 869 | } 870 | return fmt.Sprintf("%s.%s", xExpr, expr.Sel.Name), nil 871 | case *ast.ParenExpr: 872 | pExpr, err := parseAST(expr.X) 873 | if err != nil { 874 | return nil, err 875 | } 876 | return pExpr, nil 877 | case *ast.CompositeLit: 878 | if _, ok := expr.Type.(*ast.ArrayType); !ok { 879 | return nil, fmt.Errorf("not an ArrayType for a composite literal %v", expr.Type) 880 | } 881 | elements := make([]interface{}, len(expr.Elts)) 882 | for idx, elt := range expr.Elts { 883 | e, err := parseAST(elt) 884 | if err != nil { 885 | return nil, err 886 | } 887 | elements[idx] = e 888 | } 889 | return elements, nil 890 | default: 891 | return nil, fmt.Errorf("expressions other than unary, binary and function call are not allowed %v", expr) 892 | } 893 | } 894 | 895 | // parseBinary parses and returns a JSON struct according to 896 | // CouchDB mango query syntax for the supported binary operators. 897 | func parseBinary(operator token.Token, leftOperand, rightOperand ast.Expr) (interface{}, error) { 898 | left, err := parseAST(leftOperand) 899 | if err != nil { 900 | return nil, err 901 | } 902 | right, err := parseAST(rightOperand) 903 | if err != nil { 904 | return nil, err 905 | } 906 | 907 | // <, <=, ==, !=, >=, >, &&, || 908 | switch operator { 909 | case token.LSS: 910 | return map[string]interface{}{ 911 | left.(string): map[string]interface{}{"$lt": right}, 912 | }, nil 913 | case token.LEQ: 914 | return map[string]interface{}{ 915 | left.(string): map[string]interface{}{"$lte": right}, 916 | }, nil 917 | case token.EQL: 918 | return map[string]interface{}{ 919 | left.(string): map[string]interface{}{"$eq": right}, 920 | }, nil 921 | case token.NEQ: 922 | return map[string]interface{}{ 923 | left.(string): map[string]interface{}{"$ne": right}, 924 | }, nil 925 | case token.GEQ: 926 | return map[string]interface{}{ 927 | left.(string): map[string]interface{}{"$gte": right}, 928 | }, nil 929 | case token.GTR: 930 | return map[string]interface{}{ 931 | left.(string): map[string]interface{}{"$gt": right}, 932 | }, nil 933 | case token.LAND: 934 | return map[string]interface{}{ 935 | "$and": []interface{}{left, right}, 936 | }, nil 937 | case token.LOR: 938 | return map[string]interface{}{ 939 | "$or": []interface{}{left, right}, 940 | }, nil 941 | } 942 | return nil, fmt.Errorf("binary operator %s not supported", operator) 943 | } 944 | 945 | // parseUnary parses and returns a JSON struct according to 946 | // CouchDB mango query syntax for supported unary operators. 947 | func parseUnary(operator token.Token, operandExpr ast.Expr) (interface{}, error) { 948 | operand, err := parseAST(operandExpr) 949 | if err != nil { 950 | return nil, err 951 | } 952 | 953 | switch operator { 954 | case token.NOT: 955 | return map[string]interface{}{ 956 | "$not": operand, 957 | }, nil 958 | } 959 | return nil, fmt.Errorf("unary operator %s not supported", operator) 960 | } 961 | 962 | // parseFuncCall parses and returns a JSON struct according to 963 | // CouchDB mango query syntax for supported meta functions. 964 | func parseFuncCall(funcExpr ast.Expr, args []ast.Expr) (interface{}, error) { 965 | funcIdent := funcExpr.(*ast.Ident) 966 | functionName := funcIdent.Name 967 | switch functionName { 968 | case "nor": 969 | if len(args) < 1 { 970 | return nil, fmt.Errorf("function nor(exprs...) need at least 1 arguments, not %d", len(args)) 971 | } 972 | 973 | selectors := make([]interface{}, len(args)) 974 | for idx, arg := range args { 975 | selector, err := parseAST(arg) 976 | if err != nil { 977 | return nil, err 978 | } 979 | selectors[idx] = selector 980 | } 981 | 982 | return map[string]interface{}{ 983 | "$nor": selectors, 984 | }, nil 985 | case "all": 986 | if len(args) != 2 { 987 | return nil, fmt.Errorf("function all(field, array) need 2 arguments, not %d", len(args)) 988 | } 989 | 990 | fieldExpr, err := parseAST(args[0]) 991 | if err != nil { 992 | return nil, err 993 | } 994 | if _, ok := fieldExpr.(string); !ok { 995 | return nil, fmt.Errorf("invalid field expression type %s", fieldExpr) 996 | } 997 | 998 | arrayExpr, err := parseAST(args[1]) 999 | if err != nil { 1000 | return nil, err 1001 | } 1002 | 1003 | return map[string]interface{}{ 1004 | fieldExpr.(string): map[string]interface{}{"$all": arrayExpr}, 1005 | }, nil 1006 | case "any": 1007 | if len(args) != 2 { 1008 | return nil, fmt.Errorf("function any(field, condition) need 2 arguments, not %d", len(args)) 1009 | } 1010 | 1011 | fieldExpr, err := parseAST(args[0]) 1012 | if err != nil { 1013 | return nil, err 1014 | } 1015 | if _, ok := fieldExpr.(string); !ok { 1016 | return nil, fmt.Errorf("invalid field expression type %s", fieldExpr) 1017 | } 1018 | 1019 | anyExpr, err := parseAST(args[1]) 1020 | if err != nil { 1021 | return nil, err 1022 | } 1023 | anyExpr, err = removeFieldKey(fieldExpr.(string), anyExpr) 1024 | if err != nil { 1025 | return nil, err 1026 | } 1027 | 1028 | return map[string]interface{}{ 1029 | fieldExpr.(string): map[string]interface{}{"$elemMatch": anyExpr}, 1030 | }, nil 1031 | case "exists": 1032 | if len(args) != 2 { 1033 | return nil, fmt.Errorf("function exists(field, boolean) need 2 arguments, not %d", len(args)) 1034 | } 1035 | 1036 | fieldExpr, err := parseAST(args[0]) 1037 | if err != nil { 1038 | return nil, err 1039 | } 1040 | if _, ok := fieldExpr.(string); !ok { 1041 | return nil, fmt.Errorf("invalid field expression type %s", fieldExpr) 1042 | } 1043 | 1044 | boolExpr, err := parseAST(args[1]) 1045 | if err != nil { 1046 | return nil, err 1047 | } 1048 | 1049 | return map[string]interface{}{ 1050 | fieldExpr.(string): map[string]interface{}{"$exists": boolExpr}, 1051 | }, nil 1052 | case "typeof": 1053 | if len(args) != 2 { 1054 | return nil, fmt.Errorf("function typeof(field, type) need 2 arguments, not %d", len(args)) 1055 | } 1056 | 1057 | fieldExpr, err := parseAST(args[0]) 1058 | if err != nil { 1059 | return nil, err 1060 | } 1061 | if _, ok := fieldExpr.(string); !ok { 1062 | return nil, fmt.Errorf("invalid field expression type %s", fieldExpr) 1063 | } 1064 | 1065 | typeStr, err := parseAST(args[1]) 1066 | if err != nil { 1067 | return nil, err 1068 | } 1069 | 1070 | return map[string]interface{}{ 1071 | fieldExpr.(string): map[string]interface{}{"$type": typeStr}, 1072 | }, nil 1073 | case "in": 1074 | if len(args) != 2 { 1075 | return nil, fmt.Errorf("function in(field, array) need 2 arguments, not %d", len(args)) 1076 | } 1077 | 1078 | fieldExpr, err := parseAST(args[0]) 1079 | if err != nil { 1080 | return nil, err 1081 | } 1082 | if _, ok := fieldExpr.(string); !ok { 1083 | return nil, fmt.Errorf("invalid field expression type %s", fieldExpr) 1084 | } 1085 | 1086 | arrExpr, err := parseAST(args[1]) 1087 | if err != nil { 1088 | return nil, err 1089 | } 1090 | 1091 | return map[string]interface{}{ 1092 | fieldExpr.(string): map[string]interface{}{"$in": arrExpr}, 1093 | }, nil 1094 | case "nin": 1095 | if len(args) != 2 { 1096 | return nil, fmt.Errorf("function nin(field, array) need 2 arguments, not %d", len(args)) 1097 | } 1098 | 1099 | fieldExpr, err := parseAST(args[0]) 1100 | if err != nil { 1101 | return nil, err 1102 | } 1103 | if _, ok := fieldExpr.(string); !ok { 1104 | return nil, fmt.Errorf("invalid field expression type %s", fieldExpr) 1105 | } 1106 | 1107 | arrExpr, err := parseAST(args[1]) 1108 | if err != nil { 1109 | return nil, err 1110 | } 1111 | 1112 | return map[string]interface{}{ 1113 | fieldExpr.(string): map[string]interface{}{"$nin": arrExpr}, 1114 | }, nil 1115 | case "size": 1116 | if len(args) != 2 { 1117 | return nil, fmt.Errorf("function size(field, int) need 2 arguments, not %d", len(args)) 1118 | } 1119 | 1120 | fieldExpr, err := parseAST(args[0]) 1121 | if err != nil { 1122 | return nil, err 1123 | } 1124 | if _, ok := fieldExpr.(string); !ok { 1125 | return nil, fmt.Errorf("invalid field expression type %s", fieldExpr) 1126 | } 1127 | 1128 | intExpr, err := parseAST(args[1]) 1129 | if err != nil { 1130 | return nil, err 1131 | } 1132 | 1133 | return map[string]interface{}{ 1134 | fieldExpr.(string): map[string]interface{}{"$size": intExpr}, 1135 | }, nil 1136 | case "mod": 1137 | if len(args) != 3 { 1138 | return nil, fmt.Errorf("function mod(field, divisor, remainder) need 3 arguments, not %d", len(args)) 1139 | } 1140 | 1141 | fieldExpr, err := parseAST(args[0]) 1142 | if err != nil { 1143 | return nil, err 1144 | } 1145 | if _, ok := fieldExpr.(string); !ok { 1146 | return nil, fmt.Errorf("invalid field expression type %s", fieldExpr) 1147 | } 1148 | 1149 | divisorExpr, err := parseAST(args[1]) 1150 | if err != nil { 1151 | return nil, err 1152 | } 1153 | divisor, ok := divisorExpr.(int) 1154 | if !ok { 1155 | return nil, fmt.Errorf("invalid divisor %s", divisorExpr) 1156 | } 1157 | 1158 | remainderExpr, err := parseAST(args[2]) 1159 | if err != nil { 1160 | return nil, err 1161 | } 1162 | remainder, ok := remainderExpr.(int) 1163 | if !ok { 1164 | return nil, fmt.Errorf("invalid remainder %s", remainderExpr) 1165 | } 1166 | 1167 | expr, err := parser.ParseExpr(fmt.Sprintf("%#v", []int{divisor, remainder})) 1168 | if err != nil { 1169 | return nil, err 1170 | } 1171 | arrExpr, err := parseAST(expr) 1172 | if err != nil { 1173 | return nil, err 1174 | } 1175 | 1176 | return map[string]interface{}{ 1177 | fieldExpr.(string): map[string]interface{}{"$mod": arrExpr}, 1178 | }, nil 1179 | case "regex": 1180 | if len(args) != 2 { 1181 | return nil, fmt.Errorf("function regex(field, regexstr) need 2 arguments, not %d", len(args)) 1182 | } 1183 | 1184 | fieldExpr, err := parseAST(args[0]) 1185 | if err != nil { 1186 | return nil, err 1187 | } 1188 | if _, ok := fieldExpr.(string); !ok { 1189 | return nil, fmt.Errorf("invalid field expression type %s", fieldExpr) 1190 | } 1191 | 1192 | regexExpr, err := parseAST(args[1]) 1193 | if err != nil { 1194 | return nil, err 1195 | } 1196 | 1197 | return map[string]interface{}{ 1198 | fieldExpr.(string): map[string]interface{}{"$regex": regexExpr}, 1199 | }, nil 1200 | case "asc": // for sort syntax 1201 | if len(args) != 1 { 1202 | return nil, fmt.Errorf("function asc(field) need 1 argument, not %d", len(args)) 1203 | } 1204 | 1205 | fieldExpr, err := parseAST(args[0]) 1206 | if err != nil { 1207 | return nil, err 1208 | } 1209 | if _, ok := fieldExpr.(string); !ok { 1210 | return nil, fmt.Errorf("invalid field expression type %s", fieldExpr) 1211 | } 1212 | 1213 | return map[string]interface{}{ 1214 | fieldExpr.(string): "asc", 1215 | }, nil 1216 | case "desc": // for sort syntax 1217 | if len(args) != 1 { 1218 | return nil, fmt.Errorf("function desc(field) need 1 argument, not %d", len(args)) 1219 | } 1220 | 1221 | fieldExpr, err := parseAST(args[0]) 1222 | if err != nil { 1223 | return nil, err 1224 | } 1225 | if _, ok := fieldExpr.(string); !ok { 1226 | return nil, fmt.Errorf("invalid field expression type %s", fieldExpr) 1227 | } 1228 | 1229 | return map[string]interface{}{ 1230 | fieldExpr.(string): "desc", 1231 | }, nil 1232 | } 1233 | return nil, fmt.Errorf("function %s() not supported", functionName) 1234 | } 1235 | 1236 | // removeFieldKey removes the key which equals to fieldName, 1237 | // moves its value one level up in the map. 1238 | func removeFieldKey(fieldName string, exprMap interface{}) (interface{}, error) { 1239 | mapValue := reflect.ValueOf(exprMap) 1240 | if mapValue.Kind() != reflect.Map { 1241 | return nil, errors.New("not a map type") 1242 | } 1243 | mapKeys := mapValue.MapKeys() 1244 | for _, mapKey := range mapKeys { 1245 | // exprMap is a interface type contains map[string]interface{} 1246 | // so MapIndex returns a value whose Kind is Interface, so we 1247 | // have to call its Interface() methods then pass to ValueOf() 1248 | // to get the underline map type. 1249 | value := reflect.ValueOf(mapValue.MapIndex(mapKey).Interface()) 1250 | if value.Kind() == reflect.Slice { 1251 | for idx := 0; idx < value.Len(); idx++ { 1252 | elemVal := value.Index(idx) 1253 | processed, err := removeFieldKey(fieldName, elemVal.Interface()) 1254 | if err != nil { 1255 | return nil, err 1256 | } 1257 | elemVal.Set(reflect.ValueOf(processed)) 1258 | } 1259 | mapValue.SetMapIndex(mapKey, value) 1260 | } else if value.Kind() == reflect.Map { 1261 | if mapKey.Interface().(string) == fieldName { // found 1262 | if value.Len() != 1 { 1263 | return nil, fmt.Errorf("field map length %d, not 1", value.Len()) 1264 | } 1265 | // setting to empty value deletes the key 1266 | mapValue.SetMapIndex(mapKey, reflect.Value{}) 1267 | keys := value.MapKeys() 1268 | // moves the value one level up 1269 | for _, key := range keys { 1270 | val := value.MapIndex(key) 1271 | mapValue.SetMapIndex(key, val) 1272 | } 1273 | } else { 1274 | processed, err := removeFieldKey(fieldName, value.Interface()) 1275 | if err != nil { 1276 | return nil, err 1277 | } 1278 | mapValue.SetMapIndex(mapKey, reflect.ValueOf(processed)) 1279 | } 1280 | } 1281 | } 1282 | return mapValue.Interface(), nil 1283 | } 1284 | 1285 | // beautifulJSONString returns a beautified string representing the JSON struct. 1286 | func beautifulJSONString(jsonable interface{}) (string, error) { 1287 | b, err := json.Marshal(jsonable) 1288 | if err != nil { 1289 | return "", err 1290 | } 1291 | var out bytes.Buffer 1292 | err = json.Indent(&out, b, "", "\t") 1293 | if err != nil { 1294 | return "", err 1295 | } 1296 | return out.String(), nil 1297 | } 1298 | 1299 | // PutIndex creates a new index in database. 1300 | // 1301 | // indexFields: a JSON array of field names following the sort syntax. 1302 | // 1303 | // ddoc: optional, name of the design document in which the index will be created. 1304 | // By default each index will be created in its own design document. Indexes can be 1305 | // grouped into design documents for efficiency. However a change to one index 1306 | // in a design document will invalidate all other indexes in the same document. 1307 | // 1308 | // name: optional, name of the index. A name generated automatically if not provided. 1309 | func (d *Database) PutIndex(indexFields []string, ddoc, name string) (string, string, error) { 1310 | var design, index string 1311 | if len(indexFields) == 0 { 1312 | return design, index, errors.New("index fields cannot be empty") 1313 | } 1314 | 1315 | indexObjs, err := parseSortSyntax(indexFields) 1316 | if err != nil { 1317 | return design, index, err 1318 | } 1319 | 1320 | indexJSON := map[string]interface{}{} 1321 | indexJSON["index"] = map[string]interface{}{ 1322 | "fields": indexObjs, 1323 | } 1324 | 1325 | if len(ddoc) > 0 { 1326 | indexJSON["ddoc"] = ddoc 1327 | } 1328 | 1329 | if len(name) > 0 { 1330 | indexJSON["name"] = name 1331 | } 1332 | 1333 | _, data, err := d.resource.PostJSON("_index", nil, indexJSON, nil) 1334 | if err != nil { 1335 | return design, index, err 1336 | } 1337 | 1338 | result, err := parseData(data) 1339 | if err != nil { 1340 | return design, index, err 1341 | } 1342 | design = result["id"].(string) 1343 | index = result["name"].(string) 1344 | 1345 | return design, index, nil 1346 | } 1347 | 1348 | // GetIndex gets all indexes created in database. 1349 | func (d *Database) GetIndex() (map[string]*json.RawMessage, error) { 1350 | _, data, err := d.resource.GetJSON("_index", nil, nil) 1351 | if err != nil { 1352 | return nil, err 1353 | } 1354 | return parseRaw(data) 1355 | } 1356 | 1357 | // DeleteIndex deletes index in database. 1358 | func (d *Database) DeleteIndex(ddoc, name string) error { 1359 | indexRes := docResource(d.resource, fmt.Sprintf("_index/%s/json/%s", ddoc, name)) 1360 | _, _, err := indexRes.DeleteJSON("", nil, nil) 1361 | return err 1362 | } 1363 | 1364 | // designPath resturns a make-up design path based on designDoc and designType 1365 | // for example designPath("design/foo", "_view") returns "_design/design/_view/foo" 1366 | func designPath(designDoc, designType string) string { 1367 | if strings.HasPrefix(designDoc, "_") { 1368 | return designDoc 1369 | } 1370 | parts := strings.SplitN(designDoc, "/", 2) 1371 | if len(parts) == 1 { 1372 | return parts[0] 1373 | } 1374 | return strings.Join([]string{"_design", parts[0], designType, parts[1]}, "/") 1375 | } 1376 | 1377 | // View executes a predefined design document view and returns the results. 1378 | // 1379 | // name: the name of the view, for user-defined views use the format "design_docid/viewname", 1380 | // that is, the document ID of the design document and the name of the view, separated by a /. 1381 | // 1382 | // wrapper: an optional function for processing the result rows after retrieved. 1383 | // 1384 | // options: optional query parameters. 1385 | func (d *Database) View(name string, wrapper func(Row) Row, options map[string]interface{}) (*ViewResults, error) { 1386 | designDocPath := designPath(name, "_view") 1387 | return newViewResults(d.resource, designDocPath, options, wrapper), nil 1388 | } 1389 | 1390 | // IterView returns a channel fetching rows in batches which iterates a row at a time(pagination). 1391 | // 1392 | // name: the name of the view, for user-defined views use the format "design_docid/viewname", 1393 | // that is, the document ID of the design document and the name of the view, separated by a /. 1394 | // 1395 | // wrapper: an optional function for processing the result rows after retrieved. 1396 | // 1397 | // options: optional query parameters. 1398 | func (d *Database) IterView(name string, batch int, wrapper func(Row) Row, options map[string]interface{}) (<-chan Row, error) { 1399 | if batch <= 0 { 1400 | return nil, ErrBatchValue 1401 | } 1402 | 1403 | if options == nil { 1404 | options = map[string]interface{}{} 1405 | } 1406 | 1407 | _, ok := options["limit"] 1408 | var limit int 1409 | if ok { 1410 | if options["limit"].(int) <= 0 { 1411 | return nil, ErrLimitValue 1412 | } 1413 | limit = options["limit"].(int) 1414 | } 1415 | 1416 | // Row generator 1417 | rchan := make(chan Row) 1418 | var err error 1419 | go func() { 1420 | defer close(rchan) 1421 | for { 1422 | loopLimit := batch 1423 | if ok { 1424 | loopLimit = min(batch, limit) 1425 | } 1426 | // get rows in batch with one extra for start of next batch 1427 | options["limit"] = loopLimit + 1 1428 | var results *ViewResults 1429 | results, err = d.View(name, wrapper, options) 1430 | if err != nil { 1431 | break 1432 | } 1433 | var rows []Row 1434 | rows, err = results.Rows() 1435 | if err != nil { 1436 | break 1437 | } 1438 | 1439 | // send all rows to channel except the last extra one 1440 | for _, row := range rows[:min(len(rows), loopLimit)] { 1441 | rchan <- row 1442 | } 1443 | 1444 | if ok { 1445 | limit -= min(len(rows), batch) 1446 | } 1447 | 1448 | if len(rows) <= batch || (ok && limit == 0) { 1449 | break 1450 | } 1451 | options["startkey"] = rows[len(rows)-1].Key 1452 | options["startkey_docid"] = rows[len(rows)-1].ID 1453 | options["skip"] = 0 1454 | } 1455 | }() 1456 | return rchan, nil 1457 | } 1458 | 1459 | func min(a, b int) int { 1460 | return int(math.Min(float64(a), float64(b))) 1461 | } 1462 | 1463 | // Show calls a server-side 'show' function. 1464 | // 1465 | // name: the name of the show function in the format "designdoc/showname" 1466 | // 1467 | // docID: optional document ID to pass to the show function 1468 | // 1469 | // params: optional query parameters 1470 | func (d *Database) Show(name, docID string, params url.Values) (http.Header, []byte, error) { 1471 | designDocPath := designPath(name, "_show") 1472 | if docID != "" { 1473 | designDocPath = fmt.Sprintf("%s/%s", designDocPath, docID) 1474 | } 1475 | return d.resource.Get(designDocPath, nil, params) 1476 | } 1477 | 1478 | // List formats a view using a server-side 'list' function. 1479 | // 1480 | // name: the name of the list function in the format "designdoc/listname" 1481 | // 1482 | // view: the name of the view in the format "designdoc/viewname" 1483 | // 1484 | // options: optional query parameters 1485 | func (d *Database) List(name, view string, options map[string]interface{}) (http.Header, []byte, error) { 1486 | designDocPath := designPath(name, "_list") 1487 | res := docResource(d.resource, fmt.Sprintf("%s/%s", designDocPath, strings.Split(view, "/")[1])) 1488 | return viewLikeResourceRequest(res, options) 1489 | } 1490 | 1491 | // UpdateDoc calls server-side update handler. 1492 | // 1493 | // name: the name of the update handler function in the format "designdoc/updatename". 1494 | // 1495 | // docID: optional document ID to pass to the show function 1496 | // 1497 | // params: optional query parameters 1498 | func (d *Database) UpdateDoc(name, docID string, params url.Values) (http.Header, []byte, error) { 1499 | designDocPath := designPath(name, "_update") 1500 | if docID == "" { 1501 | return d.resource.Post(designDocPath, nil, nil, params) 1502 | } 1503 | 1504 | designDocPath = fmt.Sprintf("%s/%s", designDocPath, docID) 1505 | return d.resource.Put(designDocPath, nil, nil, params) 1506 | } 1507 | -------------------------------------------------------------------------------- /database_test.go: -------------------------------------------------------------------------------- 1 | package couchdb 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "math" 9 | "mime" 10 | "net/url" 11 | "os" 12 | "path/filepath" 13 | "reflect" 14 | "strings" 15 | "testing" 16 | ) 17 | 18 | func TestNewDefaultDB(t *testing.T) { 19 | dbDefault, err := NewDatabase("golang-default") 20 | if err != nil { 21 | t.Errorf("new default database error %v", err) 22 | } 23 | if err = dbDefault.Available(); err == nil { 24 | t.Error(`db available`) 25 | } 26 | } 27 | 28 | func TestNewDB(t *testing.T) { 29 | newDB := "golang-newdb" 30 | server.Create(newDB) 31 | defer server.Delete(newDB) 32 | dbNew, err := NewDatabase(fmt.Sprintf("%s/%s", DefaultBaseURL, newDB)) 33 | if err != nil { 34 | t.Error(`new database error`, err) 35 | } 36 | if err = dbNew.Available(); err != nil { 37 | t.Error(`db not available, error`, err) 38 | } 39 | } 40 | 41 | func TestSaveNew(t *testing.T) { 42 | doc := map[string]interface{}{"doc": "bar"} 43 | id, rev, err := testsDB.Save(doc, nil) 44 | if err != nil { 45 | t.Error(`db save error`, err) 46 | } 47 | if id != doc["_id"].(string) { 48 | t.Errorf("invalid id: %q", id) 49 | } 50 | if rev != doc["_rev"].(string) { 51 | t.Errorf("invalid rev: %q", rev) 52 | } 53 | } 54 | 55 | func TestSaveNewWithID(t *testing.T) { 56 | doc := map[string]interface{}{"_id": "foo"} 57 | id, rev, err := testsDB.Save(doc, nil) 58 | if err != nil { 59 | t.Error(`db save error`, err) 60 | } 61 | if doc["_id"].(string) != "foo" { 62 | t.Errorf("doc[_id] = %s, not foo", doc["_id"]) 63 | } 64 | if id != "foo" { 65 | t.Errorf("id = %s, not foo", id) 66 | } 67 | if rev != doc["_rev"].(string) { 68 | t.Errorf("invalid rev: %q", rev) 69 | } 70 | } 71 | 72 | func TestSaveExisting(t *testing.T) { 73 | doc := map[string]interface{}{} 74 | idOld, revOld, err := testsDB.Save(doc, nil) 75 | if err != nil { 76 | t.Error(`db save error`, err) 77 | } 78 | doc["foo"] = true 79 | idNew, revNew, err := testsDB.Save(doc, nil) 80 | if err != nil { 81 | t.Error(`db save foo error`, err) 82 | } 83 | if idOld != idNew { 84 | t.Errorf("ids are not equal old %s new %s", idOld, idNew) 85 | } 86 | if doc["_rev"].(string) != revNew { 87 | t.Errorf("invalid rev %s want %s", doc["_rev"].(string), revNew) 88 | } 89 | if revOld == revNew { 90 | t.Errorf("new rev is equal to old rev %s", revOld) 91 | } 92 | } 93 | 94 | func TestSaveNewBatch(t *testing.T) { 95 | doc := map[string]interface{}{"_id": "foo"} 96 | _, rev, err := testsDB.Save(doc, url.Values{"batch": []string{"ok"}}) 97 | if err != nil { 98 | t.Error(`db save batch error`, err) 99 | } 100 | if len(rev) > 0 { 101 | t.Error(`rev not empty`, rev) 102 | } 103 | if r, ok := doc["_rev"]; ok { 104 | t.Error(`doc has _rev field`, r.(string)) 105 | } 106 | } 107 | 108 | func TestSaveExistingBatch(t *testing.T) { 109 | doc := map[string]interface{}{"_id": "bar"} 110 | idOld, revOld, err := testsDB.Save(doc, nil) 111 | if err != nil { 112 | t.Error(`db save error`, err) 113 | } 114 | 115 | idNew, revNew, err := testsDB.Save(doc, url.Values{"batch": []string{"ok"}}) 116 | if err != nil { 117 | t.Error(`db save batch error`, err) 118 | } 119 | 120 | if idOld != idNew { 121 | t.Errorf("old id %s not equal to new id %s", idOld, idNew) 122 | } 123 | 124 | if len(revNew) > 0 { 125 | t.Error(`rev not empty`, revNew) 126 | } 127 | 128 | if doc["_rev"].(string) != revOld { 129 | t.Errorf("doc[_rev] %s not equal to old rev %s", doc["_rev"].(string), revOld) 130 | } 131 | } 132 | 133 | func TestDatabaseExists(t *testing.T) { 134 | if err := testsDB.Available(); err != nil { 135 | t.Error(`golang-tests not available, error`, err) 136 | } 137 | dbMissing, _ := NewDatabase("golang-missing") 138 | if err := dbMissing.Available(); err == nil { 139 | t.Error(`golang-missing available`) 140 | } 141 | } 142 | 143 | func TestDatabaseName(t *testing.T) { 144 | name, err := testsDB.Name() 145 | if err != nil { 146 | t.Error(`db name error`, err) 147 | } 148 | if name != "golang-tests" { 149 | t.Errorf("db name %s, want golang-tests", name) 150 | } 151 | } 152 | 153 | func TestDatabaseString(t *testing.T) { 154 | if testsDB.String() != "Database http://localhost:5984/golang-tests" { 155 | t.Error(`db string invalid`, testsDB) 156 | } 157 | } 158 | 159 | func TestCommit(t *testing.T) { 160 | if err := testsDB.Commit(); err != nil { 161 | t.Error(`db commit error`, err) 162 | } 163 | } 164 | 165 | func TestCreateLargeDoc(t *testing.T) { 166 | var buf bytes.Buffer 167 | // 10MB 168 | for i := 0; i < 110*1024; i++ { 169 | buf.WriteString("0123456789") 170 | } 171 | doc := map[string]interface{}{"data": buf.String()} 172 | if err := testsDB.Set("large", doc); err != nil { 173 | t.Error(`db set error`, err) 174 | } 175 | doc, err := testsDB.Get("large", nil) 176 | if err != nil { 177 | t.Error(`db get error`, err) 178 | } 179 | if doc["_id"].(string) != "large" { 180 | t.Errorf("doc[_id] = %s, want large", doc["_id"].(string)) 181 | } 182 | err = testsDB.DeleteDoc(doc) 183 | if err != nil { 184 | t.Error(`db delete doc error`, err) 185 | } 186 | } 187 | 188 | func TestDocIDQuoting(t *testing.T) { 189 | doc := map[string]interface{}{"foo": "bar"} 190 | err := testsDB.Set("foo/bar", doc) 191 | if err != nil { 192 | t.Error(`db set error`, err) 193 | } 194 | doc, err = testsDB.Get("foo/bar", nil) 195 | if err != nil { 196 | t.Error(`db get error`, err) 197 | } 198 | if doc["foo"].(string) != "bar" { 199 | t.Errorf("doc[foo] = %s want bar", doc["foo"].(string)) 200 | } 201 | err = testsDB.Delete("foo/bar") 202 | if err != nil { 203 | t.Error(`db delete error`, err) 204 | } 205 | _, err = testsDB.Get("foo/bar", nil) 206 | if err == nil { 207 | t.Error(`db get foo/bar ok`) 208 | } 209 | } 210 | 211 | func TestDisallowNaN(t *testing.T) { 212 | doc := map[string]interface{}{"number": math.NaN()} 213 | err := testsDB.Set("foo", doc) 214 | if err == nil { 215 | t.Error(`db set NaN ok`) 216 | } 217 | } 218 | 219 | func TestDisallowNilID(t *testing.T) { 220 | err := testsDB.DeleteDoc(map[string]interface{}{"_id": nil, "_rev": nil}) 221 | if err == nil { 222 | t.Error(`db delete doc with id nil ok`) 223 | } 224 | err = testsDB.DeleteDoc(map[string]interface{}{"_id": "foo", "_rev": nil}) 225 | if err == nil { 226 | t.Error(`db delete doc with rev nil ok`) 227 | } 228 | } 229 | 230 | func TestDocRevs(t *testing.T) { 231 | uuid := GenerateUUID() 232 | doc := map[string]interface{}{"bar": 42} 233 | err := testsDB.Set(uuid, doc) 234 | if err != nil { 235 | t.Error(`db set doc error`, err) 236 | } 237 | oldRev := doc["_rev"].(string) 238 | doc["bar"] = 43 239 | err = testsDB.Set(uuid, doc) 240 | if err != nil { 241 | t.Error(`db set doc error`, err) 242 | } 243 | newRev := doc["_rev"].(string) 244 | 245 | newDoc, err := testsDB.Get(uuid, nil) 246 | if err != nil { 247 | t.Error("db get error", err) 248 | } 249 | if newRev != newDoc["_rev"].(string) { 250 | t.Errorf("new doc rev %s want %s", newDoc["_rev"].(string), newRev) 251 | } 252 | newDoc, err = testsDB.Get(uuid, url.Values{"rev": []string{newRev}}) 253 | if err != nil { 254 | t.Error("db get error", err) 255 | } 256 | if newRev != newDoc["_rev"].(string) { 257 | t.Errorf("new doc rev %s want %s", newDoc["_rev"].(string), newRev) 258 | } 259 | oldDoc, err := testsDB.Get(uuid, url.Values{"rev": []string{oldRev}}) 260 | if err != nil { 261 | t.Error("db get error", err) 262 | } 263 | if oldRev != oldDoc["_rev"].(string) { 264 | t.Errorf("old doc rev %s want %s", oldDoc["_rev"].(string), oldRev) 265 | } 266 | 267 | revs, err := testsDB.Revisions(uuid, nil) 268 | if err != nil { 269 | t.Error(`db revisions error`, err) 270 | } 271 | if revs[0]["_rev"].(string) != newRev { 272 | t.Errorf("revs first %s want %s", revs[0]["_rev"].(string), newRev) 273 | } 274 | if revs[1]["_rev"].(string) != oldRev { 275 | t.Errorf("revs second %s not equal to %s", revs[1]["_rev"].(string), oldRev) 276 | } 277 | _, err = testsDB.Revisions("crap", nil) 278 | if err == nil { 279 | t.Error(`db revisions crap ok`) 280 | } 281 | 282 | err = testsDB.Compact() 283 | if err != nil { 284 | t.Error("db compact error", err) 285 | } 286 | 287 | info, err := testsDB.Info("") 288 | if err != nil { 289 | t.Error(`db info error`, err) 290 | } 291 | for info["compact_running"].(bool) { 292 | info, err = testsDB.Info("") 293 | if err != nil { 294 | t.Error(`db info error`, err) 295 | } 296 | } 297 | 298 | _, err = testsDB.Get(uuid, url.Values{"rev": []string{oldRev}}) 299 | if err == nil { 300 | t.Errorf("db get compacted doc ok, rev = %s", oldRev) 301 | } 302 | } 303 | 304 | func TestAttachmentCRUD(t *testing.T) { 305 | uuid := GenerateUUID() 306 | doc := map[string]interface{}{"bar": 42} 307 | testsDB.Set(uuid, doc) 308 | oldRev := doc["_rev"].(string) 309 | 310 | testsDB.PutAttachment(doc, []byte("Foo bar"), "foo.txt", "text/plain") 311 | if oldRev == doc["_rev"].(string) { 312 | t.Error(`doc[_rev] == oldRev`) 313 | } 314 | 315 | doc, err := testsDB.Get(uuid, nil) 316 | if err != nil { 317 | t.Error(`db get error`, err) 318 | } 319 | attachments := reflect.ValueOf(doc["_attachments"]) 320 | foo := reflect.ValueOf(attachments.MapIndex(reflect.ValueOf("foo.txt")).Interface()) 321 | length := int(foo.MapIndex(reflect.ValueOf("length")).Interface().(float64)) 322 | if length != len("Foo bar") { 323 | t.Errorf("length %d want %d", length, len("Foo bar")) 324 | } 325 | contentType := foo.MapIndex(reflect.ValueOf("content_type")).Interface().(string) 326 | if contentType != "text/plain" { 327 | t.Errorf("content type %s want text/plain", contentType) 328 | } 329 | 330 | data, err := testsDB.GetAttachment(doc, "foo.txt") 331 | if err != nil { 332 | t.Error(`get attachment error`, err) 333 | } 334 | if string(data) != "Foo bar" { 335 | t.Errorf("db get attachment %s want Foo bar", string(data)) 336 | } 337 | 338 | data, err = testsDB.GetAttachmentID(uuid, "foo.txt") 339 | if err != nil { 340 | t.Error(`get attachment id error`, err) 341 | } 342 | if string(data) != "Foo bar" { 343 | t.Errorf("db get attachment id %s want Foo bar", string(data)) 344 | } 345 | 346 | oldRev = doc["_rev"].(string) 347 | err = testsDB.DeleteAttachment(doc, "foo.txt") 348 | if err != nil { 349 | t.Error(`db delete attachment error`, err) 350 | } 351 | if oldRev == doc["_rev"].(string) { 352 | t.Error(`doc[_rev] == oldRev`) 353 | } 354 | 355 | doc, err = testsDB.Get(uuid, nil) 356 | if err != nil { 357 | t.Error(`db get error`, err) 358 | } 359 | if _, ok := doc["_attachments"]; ok { 360 | t.Error(`doc attachments still existed`) 361 | } 362 | } 363 | 364 | func TestAttachmentWithFiles(t *testing.T) { 365 | uuid := GenerateUUID() 366 | doc := map[string]interface{}{"bar": 42} 367 | err := testsDB.Set(uuid, doc) 368 | if err != nil { 369 | t.Error(`db set doc error`, err) 370 | } 371 | oldRev := doc["_rev"].(string) 372 | fileObj := []byte("Foo bar baz") 373 | 374 | err = testsDB.PutAttachment(doc, fileObj, "foo.txt", mime.TypeByExtension(".txt")) 375 | if err != nil { 376 | t.Error("db put attachment error", err) 377 | } 378 | if oldRev == doc["_rev"].(string) { 379 | t.Error(`doc[_rev] == oldRev`) 380 | } 381 | 382 | doc, err = testsDB.Get(uuid, nil) 383 | if err != nil { 384 | t.Error(`db get error`, err) 385 | } 386 | attachments := reflect.ValueOf(doc["_attachments"]) 387 | foo := reflect.ValueOf(attachments.MapIndex(reflect.ValueOf("foo.txt")).Interface()) 388 | length := int(foo.MapIndex(reflect.ValueOf("length")).Interface().(float64)) 389 | if length != len("Foo bar baz") { 390 | t.Errorf("length %d want %d", length, len("Foo bar")) 391 | } 392 | contentType := foo.MapIndex(reflect.ValueOf("content_type")).Interface().(string) 393 | if contentType != "text/plain; charset=utf-8" { 394 | t.Errorf("content type %s want text/plain; charset=utf-8", contentType) 395 | } 396 | 397 | data, err := testsDB.GetAttachment(doc, "foo.txt") 398 | if err != nil { 399 | t.Error(`get attachment error`, err) 400 | } 401 | if string(data) != "Foo bar baz" { 402 | t.Errorf("db get attachment %s want Foo bar", string(data)) 403 | } 404 | 405 | data, err = testsDB.GetAttachmentID(uuid, "foo.txt") 406 | if err != nil { 407 | t.Error(`get attachment id error`, err) 408 | } 409 | if string(data) != "Foo bar baz" { 410 | t.Errorf("db get attachment id %s want Foo bar", string(data)) 411 | } 412 | 413 | oldRev = doc["_rev"].(string) 414 | err = testsDB.DeleteAttachment(doc, "foo.txt") 415 | if err != nil { 416 | t.Error(`db delete attachment error`, err) 417 | } 418 | if oldRev == doc["_rev"].(string) { 419 | t.Error(`doc[_rev] == oldRev`) 420 | } 421 | 422 | doc, err = testsDB.Get(uuid, nil) 423 | if err != nil { 424 | t.Error(`db get error`, err) 425 | } 426 | if _, ok := doc["_attachments"]; ok { 427 | t.Error(`doc attachments still existed`) 428 | } 429 | } 430 | 431 | func TestAttachmentCRUDFromFS(t *testing.T) { 432 | uuid := GenerateUUID() 433 | content := "Foo bar baz" 434 | tmpFileName := filepath.Join(os.TempDir(), "foo.txt") 435 | tmpFile, err := os.Create(tmpFileName) 436 | if err != nil { 437 | t.Error(`create file error`, err) 438 | } 439 | _, err = tmpFile.Write([]byte(content)) 440 | if err != nil { 441 | t.Error(`write file error`, err) 442 | } 443 | tmpFile.Close() 444 | 445 | tmpFile, err = os.Open(tmpFileName) 446 | if err != nil { 447 | t.Error(`open file error`, err) 448 | } 449 | defer tmpFile.Close() 450 | 451 | data, err := ioutil.ReadAll(tmpFile) 452 | if err != nil { 453 | t.Error(`read tmp file error`, err) 454 | } 455 | 456 | doc := map[string]interface{}{"bar": 42} 457 | err = testsDB.Set(uuid, doc) 458 | if err != nil { 459 | t.Error(`db set doc error`, err) 460 | } 461 | oldRev := doc["_rev"].(string) 462 | err = testsDB.PutAttachment(doc, data, "foo.txt", mime.TypeByExtension(filepath.Ext(tmpFileName))) 463 | if err != nil { 464 | t.Error(`put attachment error`, err) 465 | } 466 | if oldRev == doc["_rev"].(string) { 467 | t.Error(`doc[_rev] == oldRev`) 468 | } 469 | 470 | doc, err = testsDB.Get(uuid, nil) 471 | if err != nil { 472 | t.Error(`db get error`, err) 473 | } 474 | attachment := reflect.ValueOf(doc["_attachments"]) 475 | foo := reflect.ValueOf(attachment.MapIndex(reflect.ValueOf("foo.txt")).Interface()) 476 | length := int(foo.MapIndex(reflect.ValueOf("length")).Interface().(float64)) 477 | if len(content) != length { 478 | t.Errorf("length %d want %d", length, len(content)) 479 | } 480 | contentType := foo.MapIndex(reflect.ValueOf("content_type")).Interface().(string) 481 | if contentType != "text/plain; charset=utf-8" { 482 | t.Errorf("content type %s want text/plain; charset=utf-8", contentType) 483 | } 484 | 485 | data, err = testsDB.GetAttachment(doc, "foo.txt") 486 | if err != nil { 487 | t.Error(`get attachment error`, err) 488 | } 489 | if string(data) != content { 490 | t.Error(`get attachment should be `, content) 491 | } 492 | 493 | data, err = testsDB.GetAttachmentID(uuid, "foo.txt") 494 | if err != nil { 495 | t.Error(`get attachment id error`, err) 496 | } 497 | if string(data) != content { 498 | t.Error(`get attachment id should be `, content) 499 | } 500 | 501 | if err = testsDB.DeleteAttachment(doc, "foo.txt"); err != nil { 502 | t.Error(`delete attachment file error`, err) 503 | } 504 | if oldRev == doc["_rev"].(string) { 505 | t.Error(`doc[_rev] == oldRev`) 506 | } 507 | 508 | doc, err = testsDB.Get(uuid, nil) 509 | if err != nil { 510 | t.Error(`db get error`, err) 511 | } 512 | if _, ok := doc["_attachments"]; ok { 513 | t.Error(`doc attachments still existed`) 514 | } 515 | } 516 | 517 | func TestEmptyAttachment(t *testing.T) { 518 | uuid := GenerateUUID() 519 | doc := map[string]interface{}{} 520 | err := testsDB.Set(uuid, doc) 521 | if err != nil { 522 | t.Error(`db set doc error`, err) 523 | } 524 | oldRev := doc["_rev"].(string) 525 | 526 | err = testsDB.PutAttachment(doc, []byte(""), "empty.txt", mime.TypeByExtension(".txt")) 527 | if err != nil { 528 | t.Error(`put attachment error`, err) 529 | } 530 | if oldRev == doc["_rev"].(string) { 531 | t.Error(`doc[_rev] == oldRev`) 532 | } 533 | 534 | doc, err = testsDB.Get(uuid, nil) 535 | if err != nil { 536 | t.Error(`db get error`, err) 537 | } 538 | 539 | attachment := reflect.ValueOf(doc["_attachments"]) 540 | empty := reflect.ValueOf(attachment.MapIndex(reflect.ValueOf("empty.txt")).Interface()) 541 | length := int(empty.MapIndex(reflect.ValueOf("length")).Interface().(float64)) 542 | if length != 0 { 543 | t.Errorf("length %d want %d", length, 0) 544 | } 545 | } 546 | 547 | func TestDefaultAttachment(t *testing.T) { 548 | uuid := GenerateUUID() 549 | doc := map[string]interface{}{} 550 | err := testsDB.Set(uuid, doc) 551 | if err != nil { 552 | t.Error(`db set doc error`, err) 553 | } 554 | _, err = testsDB.GetAttachment(doc, "missing.txt") 555 | if err == nil { 556 | t.Error(`db get attachment ok`) 557 | } 558 | } 559 | 560 | func TestAttachmentNoFilename(t *testing.T) { 561 | uuid := GenerateUUID() 562 | doc := map[string]interface{}{} 563 | err := testsDB.Set(uuid, doc) 564 | if err != nil { 565 | t.Error(`db set doc error`, err) 566 | } 567 | err = testsDB.PutAttachment(doc, []byte(""), "", "") 568 | if err == nil { 569 | t.Error(`db put attachment with no file name ok`) 570 | } 571 | } 572 | 573 | func TestJSONAttachment(t *testing.T) { 574 | doc := map[string]interface{}{} 575 | err := testsDB.Set(GenerateUUID(), doc) 576 | if err != nil { 577 | t.Error(`db set doc error`, err) 578 | } 579 | err = testsDB.PutAttachment(doc, []byte("{}"), "test.json", "application/json") 580 | if err != nil { 581 | t.Error(`db put attachment json error`, err) 582 | } 583 | data, err := testsDB.GetAttachment(doc, "test.json") 584 | if err != nil { 585 | t.Error(`db get attachment json error`, err) 586 | } 587 | if string(data) != "{}" { 588 | t.Errorf("data = %s want {}", string(data)) 589 | } 590 | } 591 | 592 | func TestBulkUpdateConflict(t *testing.T) { 593 | docs := []map[string]interface{}{ 594 | { 595 | "type": "Person", 596 | "name": "John Doe", 597 | }, 598 | { 599 | "type": "Person", 600 | "name": "Mary Jane", 601 | }, 602 | { 603 | "type": "Person", 604 | "name": "Gotham City", 605 | }, 606 | } 607 | 608 | testsDB.Update(docs, nil) 609 | 610 | // update the first doc to provoke a conflict in the next bulk update 611 | doc := map[string]interface{}{} 612 | for k, v := range docs[0] { 613 | doc[k] = v 614 | } 615 | testsDB.Set(doc["_id"].(string), doc) 616 | 617 | results, err := testsDB.Update(docs, nil) 618 | if err != nil { 619 | t.Error(`db update error`, err) 620 | } 621 | if results[0].Err != ErrConflict { 622 | t.Errorf("db update conflict err %v want ErrConflict", results[0].Err) 623 | } 624 | } 625 | 626 | func TestCopyDocConflict(t *testing.T) { 627 | testsDB.Set("foo1", map[string]interface{}{"status": "idle"}) 628 | testsDB.Set("bar1", map[string]interface{}{"status": "testing"}) 629 | _, err := testsDB.Copy("foo1", "bar1", "") 630 | if err != ErrConflict { 631 | t.Errorf(`db copy returns %v, want ErrConflict`, err) 632 | } 633 | } 634 | 635 | func TestCopyDocOverwrite(t *testing.T) { 636 | foo2 := map[string]interface{}{"status": "testing"} 637 | bar2 := map[string]interface{}{"status": "idle"} 638 | testsDB.Set("foo2", foo2) 639 | testsDB.Set("bar2", bar2) 640 | result, err := testsDB.Copy("foo2", "bar2", bar2["_rev"].(string)) 641 | if err != nil { 642 | t.Error(`db copy error`, err) 643 | } 644 | doc, _ := testsDB.Get("bar2", nil) 645 | if result != doc["_rev"].(string) { 646 | t.Errorf("db copy returns %s want %s", result, doc["_rev"].(string)) 647 | } 648 | if doc["status"].(string) != "testing" { 649 | t.Errorf("db copy status = %s, want testing", doc["status"].(string)) 650 | } 651 | } 652 | 653 | func TestChanges(t *testing.T) { 654 | options := url.Values{ 655 | "style": []string{"all_docs"}, 656 | } 657 | _, err := testsDB.Changes(options) 658 | if err != nil { 659 | t.Error(`db change error`, err) 660 | } 661 | } 662 | 663 | func TestPurge(t *testing.T) { 664 | version, err := server.Version() 665 | if err != nil { 666 | t.Error("server version error", err) 667 | } 668 | // TODO: purge not implemented in CouchDB 2.0.0 669 | if !strings.HasPrefix(version, "2") { 670 | doc := map[string]interface{}{"a": "b"} 671 | err := testsDB.Set("purge", doc) 672 | if err != nil { 673 | t.Error(`db set error`, err) 674 | } 675 | result, err := testsDB.Purge([]map[string]interface{}{doc}) 676 | if err != nil { 677 | t.Error(`db purge error`, err) 678 | } 679 | 680 | purgeSeq := int(result["purge_seq"].(float64)) 681 | if purgeSeq != 1 { 682 | t.Errorf("db purge seq=%d want 1", purgeSeq) 683 | } 684 | } 685 | } 686 | 687 | func TestSecurity(t *testing.T) { 688 | secDoc, err := testsDB.GetSecurity() 689 | if err != nil { 690 | t.Error(`get security should return true`) 691 | } 692 | if len(secDoc) > 0 { 693 | t.Error(`secDoc should be empty`) 694 | } 695 | if testsDB.SetSecurity(map[string]interface{}{ 696 | "names": []string{"test"}, 697 | "roles": []string{}, 698 | }) != nil { 699 | t.Error(`set security should return true`) 700 | } 701 | } 702 | 703 | func TestDBContains(t *testing.T) { 704 | doc := map[string]interface{}{ 705 | "type": "Person", 706 | "name": "Jason Statham", 707 | } 708 | id, _, err := testsDB.Save(doc, nil) 709 | if err != nil { 710 | t.Error(`db save error`, err) 711 | } 712 | if err = testsDB.Contains(id); err != nil { 713 | t.Error(`db contains error`, err) 714 | } 715 | } 716 | 717 | func TestDBSetGetDelete(t *testing.T) { 718 | doc := map[string]interface{}{ 719 | "type": "Person", 720 | "name": "Jason Statham", 721 | } 722 | err := testsDB.Set("Mechanic", doc) 723 | if err != nil { 724 | t.Error(`db set error`, err) 725 | } 726 | _, err = testsDB.Get("Mechanic", nil) 727 | if err != nil { 728 | t.Error(`db get error`, err) 729 | } 730 | err = testsDB.Delete("Mechanic") 731 | if err != nil { 732 | t.Error(`db delete error`, err) 733 | } 734 | } 735 | 736 | func TestDBDocIDsAndLen(t *testing.T) { 737 | doc := map[string]interface{}{ 738 | "type": "Person", 739 | "name": "Jason Statham", 740 | } 741 | 742 | err := testsDB.Set("Mechanic", doc) 743 | if err != nil { 744 | t.Error(`db set error`, err) 745 | } 746 | 747 | ids, err := testsDB.DocIDs() 748 | if err != nil { 749 | t.Error(`db doc ids error`, err) 750 | } 751 | 752 | length, err := testsDB.Len() 753 | if err != nil { 754 | t.Error(`db len error`, err) 755 | } 756 | if length != len(ids) { 757 | t.Errorf("Len() returns %d want %d", length, len(ids)) 758 | } 759 | } 760 | 761 | func TestGetSetRevsLimit(t *testing.T) { 762 | err := testsDB.SetRevsLimit(10) 763 | if err != nil { 764 | t.Error(`db set revs limit error`, err) 765 | } 766 | limit, err := testsDB.GetRevsLimit() 767 | if err != nil { 768 | t.Error(`db get revs limit error`, err) 769 | } 770 | if limit != 10 { 771 | t.Error(`limit should be 10`) 772 | } 773 | } 774 | 775 | func TestCleanup(t *testing.T) { 776 | err := testsDB.Cleanup() 777 | if err != nil { 778 | t.Error(`db clean up error`, err) 779 | } 780 | } 781 | 782 | func TestParseSelectorSyntax(t *testing.T) { 783 | _, err := parseSelectorSyntax(`title == "Spacecataz" && year == 2004 && director == "Dave Willis"`) 784 | if err != nil { 785 | t.Error("parse selector syntax error", err) 786 | } 787 | 788 | _, err = parseSelectorSyntax(`year >= 1990 && (director == "George Lucas" || director == "Steven Spielberg")`) 789 | if err != nil { 790 | t.Error("parse selector syntax error", err) 791 | } 792 | 793 | _, err = parseSelectorSyntax(`year >= 1900 && year <= 2000 && nor(year == 1990, year == 1989, year == 1997)`) 794 | if err != nil { 795 | t.Error("parse selector syntax error", err) 796 | } 797 | 798 | _, err = parseSelectorSyntax(`_id > nil && all(genre, []string{"Comedy", "Short"})`) 799 | if err != nil { 800 | t.Error("parse selector syntax error", err) 801 | } 802 | 803 | _, err = parseSelectorSyntax(`_id > nil && any(genre, genre == "Short" || genre == "Horror" || score >= 8)`) 804 | if err != nil { 805 | t.Error("parse selector syntax error", err) 806 | } 807 | 808 | _, err = parseSelectorSyntax(`exists(director, true, "wrongParam")`) 809 | if err == nil { 810 | t.Error("parse exists function ok, should be 2 parameters") 811 | } 812 | 813 | _, err = parseSelectorSyntax(`typeof(genre, "array", "wrongParam")`) 814 | if err == nil { 815 | t.Error("parse typeof function ok, should be 2 parameters") 816 | } 817 | 818 | _, err = parseSelectorSyntax(`in(director, []string{"Mike Portnoy", "Vitali Kanevsky"}, "wrongParam")`) 819 | if err == nil { 820 | t.Error("parse in function ok, should be 2 parameters") 821 | } 822 | 823 | _, err = parseSelectorSyntax(`nin(year, []int{1990, 1992, 1998}, "wrongParam")`) 824 | if err == nil { 825 | t.Error("parse nin function ok, should be 2 parameters") 826 | } 827 | 828 | _, err = parseSelectorSyntax(`size(genre, 2, "wrongParam")`) 829 | if err == nil { 830 | t.Error("parse size function ok, should be 2 parameters") 831 | } 832 | 833 | _, err = parseSelectorSyntax(`mod(year, 2, 1, "wrongParam")`) 834 | if err == nil { 835 | t.Error("parse mod function ok, should be 3 parameters") 836 | } 837 | 838 | _, err = parseSelectorSyntax(`regex(title, "^A", "wrongParam")`) 839 | if err == nil { 840 | t.Error("parse regex function ok, should be 2 parameters") 841 | } 842 | } 843 | 844 | func TestParseSortSyntax(t *testing.T) { 845 | _, err := parseSortSyntax([]string{"fieldNameA", "fieldNameB"}) 846 | if err != nil { 847 | t.Error("parse sort syntax error", err) 848 | } 849 | 850 | _, err = parseSortSyntax([]string{"fieldNameA.subFieldA", "fieldNameB.subFieldB"}) 851 | if err != nil { 852 | t.Error("parse sort syntax error", err) 853 | } 854 | 855 | _, err = parseSortSyntax([]string{"desc(fieldName1)", "asc(fieldName2)"}) 856 | if err != nil { 857 | t.Error("parse sort syntax error", err) 858 | } 859 | 860 | _, err = parseSortSyntax([]string{"desc(fieldName1.subField1)", "asc(fieldName2.subField2)"}) 861 | if err != nil { 862 | t.Error("parse sort syntax error", err) 863 | } 864 | } 865 | 866 | func TestQueryYearAndID(t *testing.T) { 867 | version, err := server.Version() 868 | if err != nil { 869 | t.Error("server version error", err) 870 | } 871 | // CouchDB 2.0 feature 872 | if strings.HasPrefix(version, "2") { 873 | docsQuery, err := movieDB.Query(nil, `_id > nil && in(year, []int{2007, 2004})`, nil, nil, nil, nil) 874 | if err != nil { 875 | t.Error("db query error", err) 876 | } 877 | 878 | var rawJSON = ` 879 | { 880 | "selector": { 881 | "$and": [ 882 | { 883 | "_id": { "$gt": null } 884 | }, 885 | { 886 | "year": { 887 | "$in": [2007, 2004] 888 | } 889 | } 890 | ] 891 | } 892 | }` 893 | 894 | docsRaw, err := movieDB.QueryJSON(rawJSON) 895 | if err != nil { 896 | t.Error("db query json error", err) 897 | } 898 | 899 | if !reflect.DeepEqual(docsQuery, docsRaw) { 900 | t.Error("db query year and id not equal") 901 | } 902 | } 903 | } 904 | 905 | func TestQueryYearOrDirector(t *testing.T) { 906 | version, err := server.Version() 907 | if err != nil { 908 | t.Error("server version error", err) 909 | } 910 | // CouchDB 2.0 feature 911 | if strings.HasPrefix(version, "2") { 912 | docsQuery, err := movieDB.Query(nil, `year == 1989 && (director == "Ademir Kenovic" || director == "Dezs Garas")`, nil, nil, nil, nil) 913 | if err != nil { 914 | t.Error("db query error", err) 915 | } 916 | 917 | var rawJSON = ` 918 | { 919 | "selector": { 920 | "year": 1989, 921 | "$or": [ 922 | { "director": "Ademir Kenovic" }, 923 | { "director": "Dezs Garas" } 924 | ] 925 | } 926 | }` 927 | 928 | docsRaw, err := movieDB.QueryJSON(rawJSON) 929 | if err != nil { 930 | t.Error("db query json error", err) 931 | } 932 | 933 | if !reflect.DeepEqual(docsQuery, docsRaw) { 934 | t.Error("db query year or director not equal") 935 | } 936 | } 937 | } 938 | 939 | func TestQueryYearGteLteNot(t *testing.T) { 940 | version, err := server.Version() 941 | if err != nil { 942 | t.Error("server version error", err) 943 | } 944 | // CouchDB 2.0 feature 945 | if strings.HasPrefix(version, "2") { 946 | docsQuery, err := movieDB.Query(nil, `year >= 1989 && year <= 2006 && year != 2004`, nil, nil, nil, nil) 947 | if err != nil { 948 | t.Error("db query error", err) 949 | } 950 | 951 | var rawJSON = ` 952 | { 953 | "selector": { 954 | "year": { 955 | "$gte": 1989 956 | }, 957 | "year": { 958 | "$lte": 2006 959 | }, 960 | "$not": { 961 | "year": 2004 962 | } 963 | } 964 | }` 965 | 966 | docsRaw, err := movieDB.QueryJSON(rawJSON) 967 | if err != nil { 968 | t.Error("db query json error", err) 969 | } 970 | 971 | if !reflect.DeepEqual(docsQuery, docsRaw) { 972 | t.Error("db query year gte lte not not equal") 973 | } 974 | } 975 | } 976 | 977 | func TestQueryIMDBRatingNor(t *testing.T) { 978 | version, err := server.Version() 979 | if err != nil { 980 | t.Error("server version error", err) 981 | } 982 | // CouchDB 2.0 feature 983 | if strings.HasPrefix(version, "2") { 984 | docsQuery, err := movieDB.Query(nil, `imdb.rating >= 6 && imdb.rating <= 9 && nor(imdb.rating == 8.1, imdb.rating == 8.2)`, nil, nil, nil, nil) 985 | if err != nil { 986 | t.Error("db query error", err) 987 | } 988 | 989 | var rawJSON = ` 990 | { 991 | "selector": { 992 | "imdb.rating": { 993 | "$gte": 6 994 | }, 995 | "imdb.rating": { 996 | "$lte": 9 997 | }, 998 | "$nor": [ 999 | { "imdb.rating": 8.1 }, 1000 | { "imdb.rating": 8.2 } 1001 | ] 1002 | } 1003 | }` 1004 | 1005 | docsRaw, err := movieDB.QueryJSON(rawJSON) 1006 | if err != nil { 1007 | t.Error("db query json error", err) 1008 | } 1009 | 1010 | if !reflect.DeepEqual(docsQuery, docsRaw) { 1011 | t.Error("db query imdb rating nor not equal") 1012 | } 1013 | } 1014 | } 1015 | 1016 | func TestQueryGenreAll(t *testing.T) { 1017 | version, err := server.Version() 1018 | if err != nil { 1019 | t.Error("server version error", err) 1020 | } 1021 | // CouchDB 2.0 feature 1022 | if strings.HasPrefix(version, "2") { 1023 | docsQuery, err := movieDB.Query(nil, `_id > nil && all(genre, []string{"Comedy", "Short"})`, nil, nil, nil, nil) 1024 | if err != nil { 1025 | t.Error("db query error", err) 1026 | } 1027 | 1028 | var rawJSON = ` 1029 | { 1030 | "selector": { 1031 | "_id": { 1032 | "$gt": null 1033 | }, 1034 | "genre": { 1035 | "$all": ["Comedy","Short"] 1036 | } 1037 | } 1038 | }` 1039 | 1040 | docsRaw, err := movieDB.QueryJSON(rawJSON) 1041 | if err != nil { 1042 | t.Error("db query json error", err) 1043 | } 1044 | 1045 | if !reflect.DeepEqual(docsQuery, docsRaw) { 1046 | t.Error("db query genre all not equal") 1047 | } 1048 | } 1049 | } 1050 | 1051 | func TestQueryGenreElemMatch(t *testing.T) { 1052 | version, err := server.Version() 1053 | if err != nil { 1054 | t.Error("server version error", err) 1055 | } 1056 | // CouchDB 2.0 feature 1057 | if strings.HasPrefix(version, "2") { 1058 | docsQuery, err := movieDB.Query(nil, `_id > nil && any(genre, genre == "Horror" || genre == "Comedy")`, nil, nil, nil, nil) 1059 | if err != nil { 1060 | t.Error("db query error", err) 1061 | } 1062 | 1063 | var rawJSON = ` 1064 | { 1065 | "selector": { 1066 | "$and": [ 1067 | { 1068 | "_id": { 1069 | "$gt": null 1070 | } 1071 | }, 1072 | { 1073 | "genre": { 1074 | "$elemMatch": { 1075 | "$or": [ 1076 | { 1077 | "$eq": "Horror" 1078 | }, 1079 | { 1080 | "$eq": "Comedy" 1081 | } 1082 | ] 1083 | } 1084 | } 1085 | } 1086 | ] 1087 | } 1088 | }` 1089 | 1090 | docsRaw, err := movieDB.QueryJSON(rawJSON) 1091 | if err != nil { 1092 | t.Error("db query json error", err) 1093 | } 1094 | 1095 | if !reflect.DeepEqual(docsQuery, docsRaw) { 1096 | t.Error("db query genre elem match not equal") 1097 | } 1098 | } 1099 | } 1100 | 1101 | func TestQueryRegex(t *testing.T) { 1102 | version, err := server.Version() 1103 | if err != nil { 1104 | t.Error("server version error", err) 1105 | } 1106 | // CouchDB 2.0 feature 1107 | if strings.HasPrefix(version, "2") { 1108 | docsQuery, err := movieDB.Query(nil, `regex(director, "^D")`, nil, nil, nil, nil) 1109 | if err != nil { 1110 | t.Error("db query error", err) 1111 | } 1112 | 1113 | var rawJSON = ` 1114 | { 1115 | "selector": { 1116 | "director": {"$regex": "^D"} 1117 | } 1118 | }` 1119 | docsRaw, err := movieDB.QueryJSON(rawJSON) 1120 | if err != nil { 1121 | t.Error("db query json error", err) 1122 | } 1123 | 1124 | if !reflect.DeepEqual(docsQuery, docsRaw) { 1125 | t.Error("db query regex not equal") 1126 | } 1127 | } 1128 | } 1129 | 1130 | func TestQueryYearIDNin(t *testing.T) { 1131 | version, err := server.Version() 1132 | if err != nil { 1133 | t.Error("server version error", err) 1134 | } 1135 | // CouchDB 2.0 feature 1136 | if strings.HasPrefix(version, "2") { 1137 | docsQuery, err := movieDB.Query(nil, `_id > nil && nin(year, []int{1989, 1990})`, nil, nil, nil, nil) 1138 | if err != nil { 1139 | t.Error("db query error", err) 1140 | } 1141 | 1142 | var rawJSON = ` 1143 | { 1144 | "selector": { 1145 | "$and": [ 1146 | { 1147 | "_id": { "$gt": null } 1148 | }, 1149 | { 1150 | "year": { 1151 | "$nin": [1989, 1990] 1152 | } 1153 | } 1154 | ] 1155 | } 1156 | }` 1157 | docsRaw, err := movieDB.QueryJSON(rawJSON) 1158 | if err != nil { 1159 | t.Error("db query json error", err) 1160 | } 1161 | 1162 | if !reflect.DeepEqual(docsQuery, docsRaw) { 1163 | t.Error("db query year id nin not equal") 1164 | } 1165 | } 1166 | } 1167 | 1168 | func TestQueryTypeAndExists(t *testing.T) { 1169 | version, err := server.Version() 1170 | if err != nil { 1171 | t.Error("server version error", err) 1172 | } 1173 | // CouchDB 2.0 feature 1174 | if strings.HasPrefix(version, "2") { 1175 | docsQuery, err := movieDB.Query(nil, `typeof(poster, "string") && exists(runtime, true)`, nil, nil, nil, nil) 1176 | if err != nil { 1177 | t.Error("db query error", err) 1178 | } 1179 | 1180 | var rawJSON = ` 1181 | { 1182 | "selector": { 1183 | "$and": [ 1184 | { 1185 | "poster": { 1186 | "$type": "string" 1187 | } 1188 | }, 1189 | { 1190 | "runtime": { 1191 | "$exists": true 1192 | } 1193 | } 1194 | ] 1195 | } 1196 | }` 1197 | docsRaw, err := movieDB.QueryJSON(rawJSON) 1198 | if err != nil { 1199 | t.Error("db query json error", err) 1200 | } 1201 | 1202 | if !reflect.DeepEqual(docsQuery, docsRaw) { 1203 | t.Error("db query type and exists not equal") 1204 | } 1205 | } 1206 | } 1207 | 1208 | func TestQuerySizeAndMod(t *testing.T) { 1209 | version, err := server.Version() 1210 | if err != nil { 1211 | t.Error("server version error", err) 1212 | } 1213 | // CouchDB 2.0 feature 1214 | if strings.HasPrefix(version, "2") { 1215 | docsQuery, err := movieDB.Query(nil, `size(writer, 2) && mod(year, 2, 0)`, nil, nil, nil, nil) 1216 | if err != nil { 1217 | t.Error("db query error", err) 1218 | } 1219 | 1220 | var rawJSON = ` 1221 | { 1222 | "selector": { 1223 | "$and": [ 1224 | { 1225 | "writer": { 1226 | "$size": 2 1227 | } 1228 | }, 1229 | { 1230 | "year": { 1231 | "$mod": [2, 0] 1232 | } 1233 | } 1234 | ] 1235 | } 1236 | }` 1237 | docsRaw, err := movieDB.QueryJSON(rawJSON) 1238 | if err != nil { 1239 | t.Error("db query json error", err) 1240 | } 1241 | 1242 | if !reflect.DeepEqual(docsQuery, docsRaw) { 1243 | t.Error("db query size and mod not equal") 1244 | } 1245 | } 1246 | } 1247 | 1248 | func TestRatingOrYear(t *testing.T) { 1249 | version, err := server.Version() 1250 | if err != nil { 1251 | t.Error("server version error", err) 1252 | } 1253 | // CouchDB 2.0 feature 1254 | if strings.HasPrefix(version, "2") { 1255 | docsQuery, err := movieDB.Query(nil, "rating != nil || year < 2000", nil, nil, nil, nil) 1256 | if err != nil { 1257 | t.Error("db query error", err) 1258 | } 1259 | 1260 | var rawJSON = ` 1261 | { 1262 | "selector":{ 1263 | "$or": [ 1264 | { 1265 | "rating": { 1266 | "$ne": null 1267 | } 1268 | }, 1269 | { 1270 | "year": { 1271 | "$lt": 2000 1272 | } 1273 | } 1274 | ] 1275 | } 1276 | }` 1277 | docsRaw, err := movieDB.QueryJSON(rawJSON) 1278 | if err != nil { 1279 | t.Error("db query json error", err) 1280 | } 1281 | 1282 | if !reflect.DeepEqual(docsRaw, docsQuery) { 1283 | t.Error("db query rating or year not equal") 1284 | } 1285 | } 1286 | } 1287 | 1288 | func TestQuerySortLimitSkip(t *testing.T) { 1289 | version, err := server.Version() 1290 | if err != nil { 1291 | t.Error("server version error", err) 1292 | } 1293 | // CouchDB 2.0 feature 1294 | if strings.HasPrefix(version, "2") { 1295 | fields := []string{"_id", "_rev", "year", "director"} 1296 | selector := `year > 1989` 1297 | sorts := []string{"desc(_id)"} 1298 | docsQuery, err := movieDB.Query(fields, selector, sorts, 5, 2, nil) 1299 | if err != nil { 1300 | t.Error("db query error", err) 1301 | } 1302 | 1303 | var rawJSON = ` 1304 | { 1305 | "selector": { 1306 | "year": {"$gt": 1989} 1307 | }, 1308 | "fields": ["_id", "_rev", "year", "director"], 1309 | "limit": 5, 1310 | "skip": 2, 1311 | "sort": [{"_id": "desc"}] 1312 | }` 1313 | docsRaw, err := movieDB.QueryJSON(rawJSON) 1314 | if err != nil { 1315 | t.Error("db query json error", err) 1316 | } 1317 | 1318 | if !reflect.DeepEqual(docsQuery, docsRaw) { 1319 | t.Error("db query sort limit skip not equal") 1320 | } 1321 | } 1322 | } 1323 | 1324 | func TestIndexCRUD(t *testing.T) { 1325 | version, err := server.Version() 1326 | if err != nil { 1327 | t.Error("server version error", err) 1328 | } 1329 | // CouchDB 2.0 feature 1330 | if strings.HasPrefix(version, "2") { 1331 | designName, indexName, err := movieDB.PutIndex([]string{"asc(year)"}, "", "year-index") 1332 | if err != nil { 1333 | t.Error("db put index error", err) 1334 | } 1335 | 1336 | indexResult, err := movieDB.GetIndex() 1337 | if err != nil { 1338 | t.Error("db get index error", err) 1339 | } 1340 | 1341 | var totalRows float64 1342 | err = json.Unmarshal(*indexResult["total_rows"], &totalRows) 1343 | if err != nil { 1344 | t.Error("json unmarshal total rows error", err) 1345 | } 1346 | if int(totalRows) < 2 { 1347 | t.Error("index total rows should be >= 2") 1348 | } 1349 | 1350 | var idxes = []*json.RawMessage{} 1351 | err = json.Unmarshal(*indexResult["indexes"], &idxes) 1352 | if err != nil { 1353 | t.Error("json unmarshal indexes error", err) 1354 | } 1355 | idxMap := map[string]*json.RawMessage{} 1356 | found := false 1357 | idxName := "" 1358 | idxType := "" 1359 | idxDef := map[string]*json.RawMessage{} 1360 | defFields := []*json.RawMessage{} 1361 | fieldMap := map[string]string{} 1362 | for _, idx := range idxes { 1363 | json.Unmarshal(*idx, &idxMap) 1364 | json.Unmarshal(*idxMap["name"], &idxName) 1365 | if idxName != "year-index" { 1366 | continue 1367 | } 1368 | found = true 1369 | json.Unmarshal(*idxMap["type"], &idxType) 1370 | if idxType != "json" { 1371 | t.Error("index type not json") 1372 | } 1373 | json.Unmarshal(*idxMap["def"], &idxDef) 1374 | json.Unmarshal(*idxDef["fields"], &defFields) 1375 | if len(defFields) != 1 { 1376 | t.Error("index def fields != 1") 1377 | } 1378 | json.Unmarshal(*defFields[0], &fieldMap) 1379 | if fieldMap["year"] != "asc" { 1380 | t.Errorf("index year order %s want asc", fieldMap["year"]) 1381 | } 1382 | } 1383 | if !found { 1384 | t.Error("index year not found") 1385 | } 1386 | 1387 | err = movieDB.DeleteIndex(designName, indexName) 1388 | if err != nil { 1389 | t.Error("db delete index error", err) 1390 | } 1391 | } 1392 | } 1393 | 1394 | func TestQueryDoubleSort(t *testing.T) { 1395 | version, err := server.Version() 1396 | if err != nil { 1397 | t.Error("server version error", err) 1398 | } 1399 | // CouchDB 2.0 feature 1400 | if strings.HasPrefix(version, "2") { 1401 | designName, indexName, err := movieDB.PutIndex([]string{"imdb.rating", "imdb.votes"}, "", "imdb-index") 1402 | if err != nil { 1403 | t.Error("db put index error", err) 1404 | } 1405 | 1406 | fields := []string{"_id", "_rev", "year", "title"} 1407 | selector := `year > 1989 && imdb.rating > 6 && imdb.votes > 100` 1408 | sorts := []string{"imdb.rating", "imdb.votes"} 1409 | docsQuery, err := movieDB.Query(fields, selector, sorts, nil, nil, nil) 1410 | if err != nil { 1411 | t.Error("db query error", err) 1412 | } 1413 | 1414 | var rawJSON = ` 1415 | { 1416 | "selector": { 1417 | "year": {"$gt": 1989}, 1418 | "imdb.rating": {"$gt": 6}, 1419 | "imdb.votes": {"$gt": 100} 1420 | }, 1421 | "fields": ["_id", "_rev", "year", "title"], 1422 | "sort": [{"imdb.rating": "asc"}, {"imdb.votes": "asc"}] 1423 | }` 1424 | docsRaw, err := movieDB.QueryJSON(rawJSON) 1425 | if err != nil { 1426 | t.Error("db query json error", err) 1427 | } 1428 | 1429 | if !reflect.DeepEqual(docsQuery, docsRaw) { 1430 | t.Error("db query double sort not equal") 1431 | } 1432 | 1433 | movieDB.DeleteIndex(designName, indexName) 1434 | } 1435 | } 1436 | 1437 | func TestQueryUseIndex(t *testing.T) { 1438 | version, err := server.Version() 1439 | if err != nil { 1440 | t.Error("server version error", err) 1441 | } 1442 | // CouchDB 2.0 feature 1443 | if strings.HasPrefix(version, "2") { 1444 | designName, indexName, err := movieDB.PutIndex([]string{"year"}, "", "year-index") 1445 | if err != nil { 1446 | t.Error("db put index error", err) 1447 | } 1448 | if indexName != "year-index" { 1449 | t.Errorf("db put index return %s want year-index", indexName) 1450 | } 1451 | 1452 | fields := []string{"_id", "_rev", "year", "title"} 1453 | selector := `year > 1989` 1454 | sorts := []string{"asc(year)"} 1455 | docsQuery, err := movieDB.Query(fields, selector, sorts, 5, 0, []string{designName, indexName}) 1456 | if err != nil { 1457 | t.Error("db query error", err) 1458 | } 1459 | 1460 | var rawJSON = ` 1461 | { 1462 | "selector": { 1463 | "year": {"$gt": 1989} 1464 | }, 1465 | "fields": ["_id", "_rev", "year", "title"], 1466 | "sort": [{"year": "asc"}], 1467 | "limit": 5, 1468 | "skip": 0, 1469 | "use_index": [%q, %q] 1470 | }` 1471 | rawJSON = fmt.Sprintf(rawJSON, designName, indexName) 1472 | docsRaw, err := movieDB.QueryJSON(rawJSON) 1473 | if err != nil { 1474 | t.Error("db query json error", err) 1475 | } 1476 | if !reflect.DeepEqual(docsRaw, docsQuery) { 1477 | t.Error("db query usd index not equal") 1478 | } 1479 | } 1480 | } 1481 | -------------------------------------------------------------------------------- /design.go: -------------------------------------------------------------------------------- 1 | package couchdb 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "reflect" 10 | "sort" 11 | "strings" 12 | ) 13 | 14 | // Row represents a row returned by database views. 15 | type Row struct { 16 | ID string 17 | Key interface{} 18 | Val interface{} 19 | Doc interface{} 20 | Err error 21 | } 22 | 23 | // String returns a string representation for Row 24 | func (r Row) String() string { 25 | id := fmt.Sprintf("%s=%s", "id", r.ID) 26 | key := fmt.Sprintf("%s=%v", "key", r.Key) 27 | doc := fmt.Sprintf("%s=%v", "doc", r.Doc) 28 | estr := fmt.Sprintf("%s=%v", "err", r.Err) 29 | val := fmt.Sprintf("%s=%v", "val", r.Val) 30 | return fmt.Sprintf("<%s %s>", "Row", strings.Join([]string{id, key, doc, estr, val}, ", ")) 31 | } 32 | 33 | // ViewResults represents the results produced by design document views. 34 | type ViewResults struct { 35 | resource *Resource 36 | designDoc string 37 | options map[string]interface{} 38 | wrapper func(Row) Row 39 | 40 | offset int 41 | totalRows int 42 | updateSeq int 43 | rows []Row 44 | err error 45 | } 46 | 47 | // newViewResults returns a newly-allocated *ViewResults 48 | func newViewResults(r *Resource, ddoc string, opt map[string]interface{}, wr func(Row) Row) *ViewResults { 49 | return &ViewResults{ 50 | resource: r, 51 | designDoc: ddoc, 52 | options: opt, 53 | wrapper: wr, 54 | offset: -1, 55 | totalRows: -1, 56 | updateSeq: -1, 57 | } 58 | } 59 | 60 | // Offset returns offset of ViewResults 61 | func (vr *ViewResults) Offset() (int, error) { 62 | if vr.rows == nil { 63 | vr.rows, vr.err = vr.fetch() 64 | } 65 | return vr.offset, vr.err 66 | } 67 | 68 | // TotalRows returns total rows of ViewResults 69 | func (vr *ViewResults) TotalRows() (int, error) { 70 | if vr.rows == nil { 71 | vr.rows, vr.err = vr.fetch() 72 | } 73 | return vr.totalRows, vr.err 74 | } 75 | 76 | // UpdateSeq returns update sequence of ViewResults 77 | func (vr *ViewResults) UpdateSeq() (int, error) { 78 | if vr.rows == nil { 79 | vr.rows, vr.err = vr.fetch() 80 | } 81 | return vr.updateSeq, vr.err 82 | } 83 | 84 | // Rows returns a slice of rows mapped (and reduced) by the view. 85 | func (vr *ViewResults) Rows() ([]Row, error) { 86 | if vr.rows == nil { 87 | vr.rows, vr.err = vr.fetch() 88 | } 89 | return vr.rows, vr.err 90 | } 91 | 92 | func viewLikeResourceRequest(res *Resource, opts map[string]interface{}) (http.Header, []byte, error) { 93 | params := url.Values{} 94 | body := map[string]interface{}{} 95 | for key, val := range opts { 96 | switch key { 97 | case "keys": // json-array, put in body and send POST request 98 | body[key] = val 99 | case "key", "startkey", "start_key", "endkey", "end_key": 100 | data, err := json.Marshal(val) 101 | if err != nil { 102 | return nil, nil, err 103 | } 104 | params.Add(key, string(data)) 105 | case "startkey_string", "endkey_string": 106 | params.Add(strings.Split(key, "_")[0], val.(string)) 107 | case "conflicts", "descending", "group", "include_docs", "attachments", "att_encoding_info", "inclusive_end", "reduce", "sorted", "update_seq": 108 | if val.(bool) { 109 | params.Add(key, "true") 110 | } else { 111 | params.Add(key, "false") 112 | } 113 | case "endkey_docid", "end_key_doc_id", "stale", "startkey_docid", "start_key_doc_id", "format": // format for _list request 114 | params.Add(key, val.(string)) 115 | case "group_level", "limit", "skip": 116 | params.Add(key, fmt.Sprintf("%d", val)) 117 | default: 118 | switch val := val.(type) { 119 | case bool: 120 | if val { 121 | params.Add(key, "true") 122 | } else { 123 | params.Add(key, "false") 124 | } 125 | case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: 126 | params.Add(key, fmt.Sprintf("%d", val)) 127 | case float32, float64: 128 | params.Add(key, fmt.Sprintf("%f", val)) 129 | default: 130 | return nil, nil, fmt.Errorf("value %v not supported", val) 131 | } 132 | } 133 | } 134 | 135 | if len(body) > 0 { 136 | return res.PostJSON("", nil, body, params) 137 | } 138 | 139 | return res.GetJSON("", nil, params) 140 | } 141 | 142 | func (vr *ViewResults) fetch() ([]Row, error) { 143 | res := docResource(vr.resource, vr.designDoc) 144 | _, data, err := viewLikeResourceRequest(res, vr.options) 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | var jsonMap map[string]*json.RawMessage 150 | err = json.Unmarshal(data, &jsonMap) 151 | if err != nil { 152 | return nil, err 153 | } 154 | 155 | if offsetRaw, ok := jsonMap["offset"]; ok { 156 | var offset float64 157 | json.Unmarshal(*offsetRaw, &offset) 158 | vr.offset = int(offset) 159 | } 160 | 161 | if totalRowsRaw, ok := jsonMap["total_rows"]; ok { 162 | var totalRows float64 163 | json.Unmarshal(*totalRowsRaw, &totalRows) 164 | vr.totalRows = int(totalRows) 165 | } 166 | 167 | if updateSeqRaw, ok := jsonMap["update_seq"]; ok { 168 | var updateSeq float64 169 | json.Unmarshal(*updateSeqRaw, &updateSeq) 170 | vr.updateSeq = int(updateSeq) 171 | } 172 | 173 | var rowsRaw []*json.RawMessage 174 | json.Unmarshal(*jsonMap["rows"], &rowsRaw) 175 | 176 | rows := make([]Row, len(rowsRaw)) 177 | var rowMap map[string]interface{} 178 | for idx, raw := range rowsRaw { 179 | json.Unmarshal(*raw, &rowMap) 180 | row := Row{} 181 | if id, ok := rowMap["id"]; ok { 182 | row.ID = id.(string) 183 | } 184 | 185 | if key, ok := rowMap["key"]; ok { 186 | row.Key = key 187 | } 188 | 189 | if val, ok := rowMap["value"]; ok { 190 | row.Val = val 191 | } 192 | 193 | if errmsg, ok := rowMap["error"]; ok { 194 | row.Err = errors.New(errmsg.(string)) 195 | } 196 | 197 | if doc, ok := rowMap["doc"]; ok { 198 | row.Doc = doc 199 | } 200 | 201 | if vr.wrapper != nil { 202 | row = vr.wrapper(row) 203 | } 204 | rows[idx] = row 205 | } 206 | return rows, nil 207 | } 208 | 209 | // ViewDefinition is a definition of view stored in a specific design document. 210 | type ViewDefinition struct { 211 | design, name, mapFun, reduceFun, language string 212 | wrapper func(Row) Row 213 | options map[string]interface{} 214 | } 215 | 216 | // NewViewDefinition returns a newly-created *ViewDefinition. 217 | // design: the name of the design document. 218 | // 219 | // name: the name of the view. 220 | // 221 | // mapFun: the map function code. 222 | // 223 | // reduceFun: the reduce function code(optional). 224 | // 225 | // language: the name of the programming language used, default is javascript. 226 | // 227 | // wrapper: an optional function for processing the result rows after retrieved. 228 | // 229 | // options: view specific options. 230 | func NewViewDefinition(design, name, mapFun, reduceFun, language string, wrapper func(Row) Row, options map[string]interface{}) (*ViewDefinition, error) { 231 | if language == "" { 232 | language = "javascript" 233 | } 234 | 235 | if mapFun == "" { 236 | return nil, errors.New("map function empty") 237 | } 238 | 239 | return &ViewDefinition{ 240 | design: design, 241 | name: name, 242 | mapFun: strings.TrimLeft(mapFun, "\n"), 243 | reduceFun: strings.TrimLeft(reduceFun, "\n"), 244 | language: language, 245 | wrapper: wrapper, 246 | options: options, 247 | }, nil 248 | } 249 | 250 | // View executes the view definition in the given database. 251 | func (vd *ViewDefinition) View(db *Database, options map[string]interface{}) (*ViewResults, error) { 252 | opts := deepCopy(options) 253 | for k, v := range vd.options { 254 | opts[k] = v 255 | } 256 | return db.View(fmt.Sprintf("%s/%s", vd.design, vd.name), nil, opts) 257 | } 258 | 259 | // GetDoc retrieves the design document corresponding to this view definition from 260 | // the given database. 261 | func (vd *ViewDefinition) GetDoc(db *Database) (map[string]interface{}, error) { 262 | if db == nil { 263 | return nil, errors.New("database nil") 264 | } 265 | return db.Get(fmt.Sprintf("_design/%s", vd.design), nil) 266 | } 267 | 268 | // Sync ensures that the view stored in the database matches the view defined by this instance. 269 | func (vd *ViewDefinition) Sync(db *Database) ([]UpdateResult, error) { 270 | if db == nil { 271 | return nil, errors.New("database nil") 272 | } 273 | return SyncMany(db, []*ViewDefinition{vd}, false, nil) 274 | } 275 | 276 | // SyncMany ensures that the views stored in the database match the views defined 277 | // by the corresponding view definitions. This function might update more than 278 | // one design document. This is done using CouchDB's bulk update to ensure atomicity of the opeation. 279 | // db: the corresponding database. 280 | // 281 | // viewDefns: a sequence of *ViewDefinition instances. 282 | // 283 | // removeMissing: whether to remove views found in a design document that are not 284 | // found in the list of ViewDefinition instances, default false. 285 | // 286 | // callback: a callback function invoked when a design document gets updated; 287 | // it is called before the doc has actually been saved back to the database. 288 | func SyncMany(db *Database, viewDefns []*ViewDefinition, removeMissing bool, callback func(map[string]interface{})) ([]UpdateResult, error) { 289 | if db == nil { 290 | return nil, errors.New("database nil") 291 | } 292 | 293 | docs := []map[string]interface{}{} 294 | designs := map[string]bool{} 295 | defMap := map[string][]*ViewDefinition{} 296 | 297 | for _, dfn := range viewDefns { 298 | designs[dfn.design] = true 299 | if _, ok := defMap[dfn.design]; !ok { 300 | defMap[dfn.design] = []*ViewDefinition{} 301 | } 302 | defMap[dfn.design] = append(defMap[dfn.design], dfn) 303 | } 304 | 305 | orders := []string{} 306 | for k := range designs { 307 | orders = append(orders, k) 308 | } 309 | sort.Strings(orders) 310 | 311 | for _, design := range orders { 312 | docID := fmt.Sprintf("_design/%s", design) 313 | doc, err := db.Get(docID, nil) 314 | if err != nil { 315 | doc = map[string]interface{}{"_id": docID} 316 | } 317 | origDoc := deepCopy(doc) 318 | languages := map[string]bool{} 319 | 320 | missing := map[string]bool{} 321 | vs, ok := doc["views"] 322 | if ok { 323 | for k := range vs.(map[string]interface{}) { 324 | missing[k] = true 325 | } 326 | } 327 | 328 | for _, dfn := range defMap[design] { 329 | funcs := map[string]interface{}{"map": dfn.mapFun} 330 | if len(dfn.reduceFun) > 0 { 331 | funcs["reduce"] = dfn.reduceFun 332 | } 333 | if dfn.options != nil { 334 | funcs["options"] = dfn.options 335 | } 336 | _, ok = doc["views"] 337 | if ok { 338 | doc["views"].(map[string]interface{})[dfn.name] = funcs 339 | } else { 340 | doc["views"] = map[string]interface{}{dfn.name: funcs} 341 | } 342 | languages[dfn.language] = true 343 | if missing[dfn.name] { 344 | delete(missing, dfn.name) 345 | } 346 | } 347 | 348 | if removeMissing { 349 | for k := range missing { 350 | delete(doc["views"].(map[string]interface{}), k) 351 | } 352 | } else if _, ok := doc["language"]; ok { 353 | languages[doc["language"].(string)] = true 354 | } 355 | 356 | langs := []string{} 357 | for lang := range languages { 358 | langs = append(langs, lang) 359 | } 360 | 361 | if len(langs) > 1 { 362 | return nil, fmt.Errorf("found different language views in one design document %v", langs) 363 | } 364 | doc["language"] = langs[0] 365 | 366 | if !reflect.DeepEqual(doc, origDoc) { 367 | if callback != nil { 368 | callback(doc) 369 | } 370 | docs = append(docs, doc) 371 | } 372 | } 373 | 374 | return db.Update(docs, nil) 375 | } 376 | 377 | func deepCopy(src map[string]interface{}) map[string]interface{} { 378 | dst := map[string]interface{}{} 379 | for k, v := range src { 380 | dst[k] = v 381 | } 382 | return dst 383 | } 384 | -------------------------------------------------------------------------------- /design_test.go: -------------------------------------------------------------------------------- 1 | package couchdb 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestRowObject(t *testing.T) { 13 | results, err := designDB.View("_all_docs", nil, map[string]interface{}{"keys": []string{"blah"}}) 14 | if err != nil { 15 | t.Error("db view error", err) 16 | } 17 | 18 | rows, err := results.Rows() 19 | if err != nil { 20 | t.Error("rows error", err) 21 | } 22 | 23 | row := rows[0] 24 | if row.ID != "" { 25 | t.Error("row ID not empty", row.ID) 26 | } 27 | 28 | if row.Key.(string) != "blah" { 29 | t.Errorf("row key %s want blah", row.Key.(string)) 30 | } 31 | 32 | if row.Val != nil { 33 | t.Error("row value not nil", row.Val) 34 | } 35 | 36 | if row.Err.Error() != "not_found" { 37 | t.Errorf("row error %s want not_found", row.Err) 38 | } 39 | 40 | _, _, err = designDB.Save(map[string]interface{}{"_id": "xyz", "foo": "bar"}, nil) 41 | if err != nil { 42 | t.Error("db save error", err) 43 | } 44 | 45 | results, err = designDB.View("_all_docs", nil, map[string]interface{}{"keys": []string{"xyz"}}) 46 | if err != nil { 47 | t.Error("db view error", err) 48 | } 49 | 50 | rows, err = results.Rows() 51 | if err != nil { 52 | t.Error("rows error", err) 53 | } 54 | 55 | row = rows[0] 56 | if row.ID != "xyz" { 57 | t.Errorf("row ID %s want xyz", row.ID) 58 | } 59 | 60 | if row.Key.(string) != "xyz" { 61 | t.Errorf("row key %s want xyz", row.Key) 62 | } 63 | 64 | value := row.Val.(map[string]interface{}) 65 | _, ok := value["rev"] 66 | if !(ok && len(value) == 1) { 67 | t.Error("row value not contains rev only") 68 | } 69 | 70 | if row.Err != nil { 71 | t.Error("row error not nil", row.Err) 72 | } 73 | 74 | designDB.Delete(row.ID) 75 | } 76 | 77 | func TestViewMultiGet(t *testing.T) { 78 | for i := 1; i < 6; i++ { 79 | designDB.Save(map[string]interface{}{"i": i}, nil) 80 | } 81 | 82 | designDB.Set("_design/test", map[string]interface{}{ 83 | "language": "javascript", 84 | "views": map[string]interface{}{ 85 | "multi_key": map[string]string{ 86 | "map": "function(doc) { emit(doc.i, null); }", 87 | }, 88 | }, 89 | }) 90 | 91 | results, err := designDB.View("test/multi_key", nil, map[string]interface{}{"keys": []int{1, 3, 5}}) 92 | if err != nil { 93 | t.Error("db view error", err) 94 | } 95 | 96 | rows, err := results.Rows() 97 | if err != nil { 98 | t.Error("rows error", err) 99 | } 100 | 101 | if len(rows) != 3 { 102 | t.Errorf("rows length %d want 3", len(rows)) 103 | } 104 | 105 | for idx, i := range []int{1, 3, 5} { 106 | if i != int(rows[idx].Key.(float64)) { 107 | t.Errorf("key = %d want %d", int(rows[idx].Key.(float64)), i) 108 | } 109 | } 110 | } 111 | 112 | func TestDesignDocInfo(t *testing.T) { 113 | designDB.Set("_design/test", map[string]interface{}{ 114 | "language": "javascript", 115 | "views": map[string]interface{}{ 116 | "test": map[string]string{"map": "function(doc) { emit(doc.type, null); }"}, 117 | }, 118 | }) 119 | info, _ := designDB.Info("test") 120 | compactRunning := info["view_index"].(map[string]interface{})["compact_running"].(bool) 121 | if compactRunning { 122 | t.Error("compact running true want false") 123 | } 124 | } 125 | 126 | func TestViewCompaction(t *testing.T) { 127 | designDB.Set("_design/test", map[string]interface{}{ 128 | "language": "javascript", 129 | "views": map[string]interface{}{ 130 | "multi_key": map[string]string{"map": "function(doc) { emit(doc.i, null); }"}, 131 | }, 132 | }) 133 | 134 | _, err := designDB.View("test/multi_key", nil, nil) 135 | if err != nil { 136 | t.Error("db view error", err) 137 | } 138 | err = designDB.Compact() 139 | if err != nil { 140 | t.Error("db compact error", err) 141 | } 142 | } 143 | 144 | func TestViewCleanup(t *testing.T) { 145 | designDB.Set("_design/test", map[string]interface{}{ 146 | "language": "javascript", 147 | "views": map[string]interface{}{ 148 | "multi_key": map[string]string{"map": "function(doc) { emit(doc.i, null); }"}, 149 | }, 150 | }) 151 | 152 | _, err := designDB.View("test/multi_key", nil, nil) 153 | if err != nil { 154 | t.Error("db view error", err) 155 | } 156 | 157 | ddoc, err := designDB.Get("_design/test", nil) 158 | if err != nil { 159 | t.Error("db get error", err) 160 | } 161 | ddoc["views"] = map[string]interface{}{ 162 | "ids": map[string]string{"map": "function(doc) { emit(doc._id, null); }"}, 163 | } 164 | _, err = designDB.Update([]map[string]interface{}{ddoc}, nil) 165 | if err != nil { 166 | t.Error("db update error", err) 167 | } 168 | 169 | designDB.View("test/ids", nil, nil) 170 | err = designDB.Cleanup() 171 | if err != nil { 172 | t.Error("db cleanup error", err) 173 | } 174 | } 175 | 176 | func TestViewWrapperFunction(t *testing.T) { 177 | ddoc, err := designDB.Get("_design/test", nil) 178 | if err != nil { 179 | t.Error("db get error", err) 180 | } 181 | 182 | ddoc["views"] = map[string]interface{}{ 183 | "ids": map[string]string{"map": "function(doc) { emit(doc._id, null); }"}, 184 | "multi_key": map[string]string{"map": "function(doc) { emit(doc.i, null); }"}, 185 | } 186 | _, err = designDB.Update([]map[string]interface{}{ddoc}, nil) 187 | if err != nil { 188 | t.Error("db set error", err) 189 | } 190 | 191 | results, err := designDB.View("test/multi_key", func(row Row) Row { 192 | key := row.Key.(float64) 193 | key *= key 194 | row.Key = int(key) 195 | return row 196 | }, nil) 197 | 198 | if err != nil { 199 | t.Error("db view error", err) 200 | } 201 | 202 | rows, err := results.Rows() 203 | if err != nil { 204 | t.Error("rows error", err) 205 | } 206 | 207 | for idx, i := range []int{1, 4, 9, 16, 25} { 208 | if i != rows[idx].Key.(int) { 209 | t.Errorf("key = %d want %d", rows[idx].Key.(int), i) 210 | } 211 | } 212 | } 213 | 214 | func TestUpdateSeq(t *testing.T) { 215 | err := designDB.Set("foo", map[string]interface{}{}) 216 | if err != nil { 217 | t.Error("db set error", err) 218 | } 219 | 220 | results, err := designDB.View("_all_docs", nil, map[string]interface{}{"update_seq": true}) 221 | if err != nil { 222 | t.Error("db view error", err) 223 | } 224 | 225 | _, err = results.Rows() 226 | if err != nil { 227 | t.Error("rows error", err) 228 | } 229 | 230 | updateSeq, err := results.UpdateSeq() 231 | if err != nil { 232 | t.Errorf("update seq error %s %d", err, updateSeq) 233 | } 234 | 235 | } 236 | 237 | func TestProperties(t *testing.T) { 238 | results, err := designDB.View("_all_docs", nil, nil) 239 | if err != nil { 240 | t.Error("db view error", err) 241 | } 242 | 243 | rows, err := results.Rows() 244 | if err != nil { 245 | t.Error("rows error", err) 246 | } 247 | 248 | if rows == nil { 249 | t.Error("rows nil") 250 | } 251 | 252 | totalRows, _ := results.TotalRows() 253 | if totalRows == -1 { 254 | t.Error("total rows invalid") 255 | } 256 | 257 | offset, _ := results.Offset() 258 | if offset == -1 { 259 | t.Error("offset invalid") 260 | } 261 | } 262 | 263 | func TestRowRepr(t *testing.T) { 264 | results, err := designDB.View("_all_docs", nil, nil) 265 | if err != nil { 266 | t.Error("db view error", err) 267 | } 268 | 269 | rows, err := results.Rows() 270 | if err != nil { 271 | t.Error("rows error", err) 272 | } 273 | 274 | if !strings.Contains(rows[0].String(), "id") { 275 | t.Errorf("row %s not contains id", rows[0]) 276 | } 277 | 278 | if !strings.Contains(rows[0].String(), "Row") { 279 | t.Errorf("row %s not contains Row", rows[0]) 280 | } 281 | 282 | results, err = designDB.View("test/multi_key", nil, nil) 283 | if err != nil { 284 | t.Error("db view error", err) 285 | } 286 | 287 | rows, err = results.Rows() 288 | if err != nil { 289 | t.Error("rows error", err) 290 | } 291 | 292 | if !strings.Contains(rows[0].String(), "id") { 293 | t.Errorf("row %s not contains id", rows[0]) 294 | } 295 | 296 | if !strings.Contains(rows[0].String(), "Row") { 297 | t.Errorf("row %s not contains Row", rows[0]) 298 | } 299 | } 300 | 301 | func TestAllRows(t *testing.T) { 302 | rch, err := iterDB.IterView("test/nums", 10, nil, nil) 303 | if err != nil { 304 | t.Fatal("db iter view error", err) 305 | } 306 | 307 | err = testViewResults(rch, 0, NumDocs, 1) 308 | if err != nil { 309 | t.Error("test view results error", err) 310 | } 311 | } 312 | 313 | func testViewResults(rch <-chan Row, begin, end, incr int) error { 314 | rowsCollected := []Row{} 315 | for row := range rch { 316 | rowsCollected = append(rowsCollected, row) 317 | } 318 | 319 | nums := iterateSlice(begin, end, incr) 320 | if len(rowsCollected) != min(len(nums), NumDocs) { 321 | return fmt.Errorf("number of docs %d want %d", len(rowsCollected), len(nums)) 322 | } 323 | 324 | docsLeft := make([]map[string]interface{}, len(nums)) 325 | for idx, row := range rowsCollected { 326 | docsLeft[idx] = docFromRow(row) 327 | } 328 | 329 | docsRight := make([]map[string]interface{}, len(nums)) 330 | for idx, num := range nums[:min(len(rowsCollected), NumDocs)] { 331 | docsRight[idx] = docFromNum(num) 332 | } 333 | 334 | if !reflect.DeepEqual(docsLeft, docsRight) { 335 | return errors.New("doc from row not equal to doc from num") 336 | } 337 | return nil 338 | } 339 | 340 | func iterateSlice(begin, end, incr int) []int { 341 | s := []int{} 342 | if begin <= end { 343 | for i := begin; i < end; i += incr { 344 | s = append(s, i) 345 | } 346 | } else { 347 | for i := begin; i > end; i += incr { 348 | s = append(s, i) 349 | } 350 | } 351 | return s 352 | } 353 | 354 | func TestBatchSizes(t *testing.T) { 355 | _, err := iterDB.IterView("test/nums", 0, nil, nil) 356 | if err != ErrBatchValue { 357 | t.Fatalf("db iter view %s want %s", err, ErrBatchValue) 358 | } 359 | 360 | _, err = iterDB.IterView("test/nums", -1, nil, nil) 361 | if err != ErrBatchValue { 362 | t.Fatalf("db iter view %s want %s", err, ErrBatchValue) 363 | } 364 | 365 | rch, err := iterDB.IterView("test/nums", 1, nil, nil) 366 | if err != nil { 367 | t.Fatal("db iter view error", err) 368 | } 369 | err = testViewResultsLength(rch, NumDocs) 370 | if err != nil { 371 | t.Fatal("test view results length error", err) 372 | } 373 | 374 | rch, err = iterDB.IterView("test/nums", NumDocs/2, nil, nil) 375 | if err != nil { 376 | t.Fatal("db iter view error", err) 377 | } 378 | err = testViewResultsLength(rch, NumDocs) 379 | if err != nil { 380 | t.Fatal("test view results length error", err) 381 | } 382 | 383 | rch, err = iterDB.IterView("test/nums", NumDocs*2, nil, nil) 384 | if err != nil { 385 | t.Fatal("db iter view error", err) 386 | } 387 | err = testViewResultsLength(rch, NumDocs) 388 | if err != nil { 389 | t.Fatal("test view results length error", err) 390 | } 391 | 392 | rch, err = iterDB.IterView("test/nums", NumDocs-1, nil, nil) 393 | if err != nil { 394 | t.Fatal("db iter view error", err) 395 | } 396 | err = testViewResultsLength(rch, NumDocs) 397 | if err != nil { 398 | t.Fatal("test view results length error", err) 399 | } 400 | 401 | rch, err = iterDB.IterView("test/nums", NumDocs, nil, nil) 402 | if err != nil { 403 | t.Fatal("db iter view error", err) 404 | } 405 | err = testViewResultsLength(rch, NumDocs) 406 | if err != nil { 407 | t.Fatal("test view results length error", err) 408 | } 409 | 410 | rch, err = iterDB.IterView("test/nums", NumDocs+1, nil, nil) 411 | if err != nil { 412 | t.Fatal("db iter view error", err) 413 | } 414 | err = testViewResultsLength(rch, NumDocs) 415 | if err != nil { 416 | t.Fatal("test view results length error", err) 417 | } 418 | } 419 | 420 | func testViewResultsLength(rch <-chan Row, length int) error { 421 | total := 0 422 | for _ = range rch { 423 | total++ 424 | } 425 | if total != length { 426 | return fmt.Errorf("length %d want %d", total, length) 427 | } 428 | return nil 429 | } 430 | 431 | func TestBatchSizesWithSkip(t *testing.T) { 432 | rch, err := iterDB.IterView("test/nums", NumDocs/10, nil, map[string]interface{}{ 433 | "skip": NumDocs / 2, 434 | }) 435 | if err != nil { 436 | t.Fatal("db iter view error", err) 437 | } 438 | 439 | err = testViewResultsLength(rch, NumDocs/2) 440 | if err != nil { 441 | t.Error("test batch sizes with skip error", err) 442 | } 443 | } 444 | 445 | func TestLimit(t *testing.T) { 446 | var limit int 447 | var err error 448 | var rch <-chan Row 449 | _, err = iterDB.IterView("test/nums", 10, nil, map[string]interface{}{ 450 | "limit": limit, 451 | }) 452 | if err != ErrLimitValue { 453 | t.Fatalf("db iter view %s want %s", err, ErrLimitValue) 454 | } 455 | 456 | for _, limit = range []int{1, NumDocs / 4, NumDocs - 1, NumDocs, NumDocs + 1} { 457 | rch, err = iterDB.IterView("test/nums", 10, nil, map[string]interface{}{ 458 | "limit": limit, 459 | }) 460 | if err != nil { 461 | t.Fatal("db iter view error", err) 462 | } 463 | err = testViewResults(rch, 0, limit, 1) 464 | if err != nil { 465 | t.Fatal("test view results error", err) 466 | } 467 | } 468 | 469 | limit = NumDocs / 4 470 | rch, err = iterDB.IterView("test/nums", limit, nil, map[string]interface{}{ 471 | "limit": limit, 472 | }) 473 | if err != nil { 474 | t.Fatal("db iter view error", err) 475 | } 476 | 477 | err = testViewResults(rch, 0, limit, 1) 478 | if err != nil { 479 | t.Error("test view results error", err) 480 | } 481 | } 482 | 483 | func TestDescending(t *testing.T) { 484 | rch, err := iterDB.IterView("test/nums", 10, nil, map[string]interface{}{"descending": true}) 485 | if err != nil { 486 | t.Fatal("db iter view error", err) 487 | } 488 | err = testViewResults(rch, NumDocs-1, -1, -1) 489 | if err != nil { 490 | t.Error("test view results error", err) 491 | } 492 | 493 | rch, err = iterDB.IterView("test/nums", 10, nil, map[string]interface{}{ 494 | "descending": true, 495 | "limit": NumDocs / 4, 496 | }) 497 | if err != nil { 498 | t.Fatal("db iter view error", err) 499 | } 500 | err = testViewResults(rch, NumDocs-1, NumDocs*3/4-1, -1) 501 | if err != nil { 502 | t.Error("test view results error", err) 503 | } 504 | } 505 | 506 | func TestStartKey(t *testing.T) { 507 | vch, err := iterDB.IterView("test/nums", 10, nil, map[string]interface{}{"startkey": NumDocs/2 - 1}) 508 | if err != nil { 509 | t.Fatal("db iter view error", err) 510 | } 511 | err = testViewResults(vch, NumDocs-2, NumDocs, 1) 512 | if err != nil { 513 | t.Fatal("test view results error", err) 514 | } 515 | 516 | vch, err = iterDB.IterView("test/nums", 10, nil, map[string]interface{}{"startkey": 1, "descending": true}) 517 | if err != nil { 518 | t.Error("db iter view error", err) 519 | } 520 | err = testViewResults(vch, 3, -1, -1) 521 | if err != nil { 522 | t.Error("teset view results error", err) 523 | } 524 | } 525 | 526 | func TestNullKeys(t *testing.T) { 527 | vch, err := iterDB.IterView("test/nulls", 10, nil, nil) 528 | if err != nil { 529 | t.Fatal("db iter view error", err) 530 | } 531 | err = testViewResultsLength(vch, NumDocs) 532 | if err != nil { 533 | t.Error("test view results length error", err) 534 | } 535 | } 536 | 537 | func TestViewDefinitionOptions(t *testing.T) { 538 | options := map[string]interface{}{"collation": "raw"} 539 | view, err := NewViewDefinition("bar", "baz", "function(doc) { emit(doc._id, doc._rev); }", "", "", nil, options) 540 | if err != nil { 541 | t.Fatal("create view definition error", err) 542 | } 543 | _, err = view.Sync(defnDB) 544 | if err != nil { 545 | t.Fatal("view definition sync error", err) 546 | } 547 | 548 | designDoc, err := defnDB.Get("_design/bar", nil) 549 | if err != nil { 550 | t.Fatal("db get error", err) 551 | } 552 | 553 | designOptions := designDoc["views"].(map[string]interface{})["baz"].(map[string]interface{})["options"] 554 | 555 | if !reflect.DeepEqual(options, designOptions) { 556 | t.Error("options not identical") 557 | } 558 | } 559 | 560 | func TestRetrieveViewDefinition(t *testing.T) { 561 | version, _ := server.Version() 562 | 563 | view, err := NewViewDefinition("foo", "bar", "baz", "", "", nil, nil) 564 | if err != nil { 565 | t.Fatal("create view definition error", err) 566 | } 567 | results, err := view.Sync(defnDB) 568 | if err != nil { 569 | t.Fatal("view definition sync error", err) 570 | } 571 | 572 | if len(results) <= 0 { 573 | t.Fatal("no results returned") 574 | } 575 | 576 | if strings.HasPrefix(version, "1") { 577 | if results[0].Err != nil { 578 | t.Error("update result error", results[0].Err) 579 | } 580 | } else if strings.HasPrefix(version, "2") { 581 | if results[0].Err != ErrInternalServerError { 582 | t.Errorf("update result error %v want %v", results[0].Err, ErrInternalServerError) 583 | } 584 | } 585 | 586 | if results[0].ID != "_design/foo" { 587 | t.Fatalf("results ID %s want _design/foo", results[0].ID) 588 | } 589 | 590 | _, err = defnDB.Get(results[0].ID, nil) 591 | if strings.HasPrefix(version, "1") { 592 | if err != nil { 593 | t.Fatal("db get error", err) 594 | } 595 | } else if strings.HasPrefix(version, "2") { 596 | if err != ErrNotFound { 597 | t.Fatalf("db get error %s want %s", err, ErrNotFound) 598 | } 599 | } 600 | 601 | } 602 | 603 | func TestSyncMany(t *testing.T) { 604 | jsFunc := "function(doc) { emit(doc._id, doc._rev); }" 605 | firstView, _ := NewViewDefinition("design_doc", "view_one", jsFunc, "", "", nil, nil) 606 | secondView, _ := NewViewDefinition("design_doc_two", "view_one", jsFunc, "", "", nil, nil) 607 | thirdView, _ := NewViewDefinition("design_doc", "view_two", jsFunc, "", "", nil, nil) 608 | results, err := SyncMany(defnDB, []*ViewDefinition{firstView, secondView, thirdView}, false, nil) 609 | if err != nil { 610 | t.Fatal("sync many error", err) 611 | } 612 | valid := 0 613 | for _, res := range results { 614 | if res.Err == nil { 615 | valid++ 616 | } 617 | } 618 | if valid != 2 { 619 | t.Errorf("returned %d results, there should be only 2 design documents", len(results)) 620 | } 621 | } 622 | 623 | func TestShowUrls(t *testing.T) { 624 | _, data, err := showListDB.Show("_design/foo/_show/bar", "", nil) 625 | if err != nil { 626 | t.Fatal("db show error", err) 627 | } 628 | 629 | if string(data) != "null:" { 630 | t.Errorf("db show returns %s want null:", string(data)) 631 | } 632 | 633 | _, data, err = showListDB.Show("foo/bar", "", nil) 634 | if err != nil { 635 | t.Fatal("db show error", err) 636 | } 637 | 638 | if string(data) != "null:" { 639 | t.Errorf("db show returns %s want null:", string(data)) 640 | } 641 | } 642 | 643 | func TestShowDocID(t *testing.T) { 644 | _, data, err := showListDB.Show("foo/bar", "", nil) 645 | if err != nil { 646 | t.Fatal("db show error", err) 647 | } 648 | 649 | if string(data) != "null:" { 650 | t.Errorf("db show returns %s want null:", string(data)) 651 | } 652 | 653 | _, data, err = showListDB.Show("foo/bar", "1", nil) 654 | if err != nil { 655 | t.Fatal("db show error", err) 656 | } 657 | 658 | if string(data) != "1:" { 659 | t.Errorf("db show returns %s want 1:", string(data)) 660 | } 661 | 662 | _, data, err = showListDB.Show("foo/bar", "2", nil) 663 | if err != nil { 664 | t.Fatal("db show error", err) 665 | } 666 | 667 | if string(data) != "2:" { 668 | t.Errorf("db show returns %s want 2:", string(data)) 669 | } 670 | } 671 | 672 | func TestShowParams(t *testing.T) { 673 | _, data, err := showListDB.Show("foo/bar", "", url.Values{"r": []string{"abc"}}) 674 | if err != nil { 675 | t.Fatal("db show error", err) 676 | } 677 | 678 | if string(data) != "null:abc" { 679 | t.Errorf("db show returns %s want null:abc", string(data)) 680 | } 681 | } 682 | 683 | func TestList(t *testing.T) { 684 | _, data, err := showListDB.List("foo/list", "foo/by_id", nil) 685 | if err != nil { 686 | t.Fatal("db list error", err) 687 | } 688 | 689 | if string(data) != `1\r\n2\r\n` { 690 | t.Errorf("db list returns %s want `1\r\n2\r\n`", string(data)) 691 | } 692 | 693 | _, data, err = showListDB.List("foo/list", "foo/by_id", map[string]interface{}{"include_header": true}) 694 | if err != nil { 695 | t.Fatal("db list error", err) 696 | } 697 | 698 | if string(data) != `id\r\n1\r\n2\r\n` { 699 | t.Errorf("db list returns %s want `id\r\n1\r\n2\r\n`", string(data)) 700 | } 701 | } 702 | 703 | func TestListKeys(t *testing.T) { 704 | _, data, err := showListDB.List("foo/list", "foo/by_id", map[string]interface{}{"keys": []string{"1"}}) 705 | if err != nil { 706 | t.Fatal("db list error", err) 707 | } 708 | 709 | if string(data) != `1\r\n` { 710 | t.Errorf("db list returns %s want `1\r\n`", string(data)) 711 | } 712 | } 713 | 714 | func TestListViewParams(t *testing.T) { 715 | _, data, err := showListDB.List("foo/list", "foo/by_name", map[string]interface{}{"startkey": "o", "endkey": "p"}) 716 | if err != nil { 717 | t.Fatal("db list error", err) 718 | } 719 | 720 | if string(data) != `1\r\n` { 721 | t.Errorf("db list returns %s want `1\r\n`", string(data)) 722 | } 723 | 724 | _, data, err = showListDB.List("foo/list", "foo/by_name", map[string]interface{}{"descending": true}) 725 | if err != nil { 726 | t.Fatal("db list error", err) 727 | } 728 | 729 | if string(data) != `2\r\n1\r\n` { 730 | t.Errorf("db list returns %s want `2\r\n1\r\n`", string(data)) 731 | } 732 | } 733 | 734 | func TestEmptyDoc(t *testing.T) { 735 | _, data, err := updateDB.UpdateDoc("foo/bar", "", nil) 736 | if err != nil { 737 | t.Fatal("db updatedoc error", err) 738 | } 739 | 740 | if string(data) != "empty doc" { 741 | t.Errorf("db list returns %s want empty doc", string(data)) 742 | } 743 | } 744 | 745 | func TestNewDoc(t *testing.T) { 746 | _, data, err := updateDB.UpdateDoc("foo/bar", "new", nil) 747 | if err != nil { 748 | t.Fatal("db updatedoc error", err) 749 | } 750 | 751 | if string(data) != "new doc" { 752 | t.Errorf("db list returns %s want new doc", string(data)) 753 | } 754 | } 755 | 756 | func TestUpdateDoc(t *testing.T) { 757 | _, data, err := updateDB.UpdateDoc("foo/bar", "existed", nil) 758 | if err != nil { 759 | t.Fatal("db updatedoc error", err) 760 | } 761 | 762 | if string(data) != "hello doc" { 763 | t.Errorf("db list returns %s want hello doc", string(data)) 764 | } 765 | } 766 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package couchdb provides components to work with CouchDB 2.x with Go. 2 | // 3 | // Resource is the low-level wrapper functions of HTTP methods 4 | // used for communicating with CouchDB Server. 5 | // 6 | // Server contains all the functions to work with CouchDB server, including some 7 | // basic functions to facilitate the basic user management provided by it. 8 | // 9 | // Database contains all the functions to work with CouchDB database, such as 10 | // documents manipulating and querying. 11 | // 12 | // ViewResults represents the results produced by design document views. When calling 13 | // any of its functions like Offset(), TotalRows(), UpdateSeq() or Rows(), it will 14 | // perform a query on views on server side, and returns results as slice of Row 15 | // 16 | // ViewDefinition is a definition of view stored in a specific design document, 17 | // you can define your own map-reduce functions and Sync with the database. 18 | // 19 | // Document represents a document object in database. All struct that can be mapped 20 | // into CouchDB document must have it embedded. For example: 21 | // 22 | // type User struct { 23 | // Name string `json:"name"` 24 | // Age int `json:"age"` 25 | // Document 26 | // } 27 | // user := User{"Mike", 18} 28 | // anotherUser := User{} 29 | // 30 | // Then you can call Store(db, &user) to store it into CouchDB or Load(db, user.GetID(), &anotherUser) 31 | // to get the data from database. 32 | // 33 | // ViewField represents a view definition value bound to Document. 34 | // 35 | // tools/replicate is a command-line tool for replicating databases from one CouchDB server to another. 36 | // This is mainly for backup purposes, but you can also use -continuous option to set up automatic replication. 37 | package couchdb 38 | -------------------------------------------------------------------------------- /mapping.go: -------------------------------------------------------------------------------- 1 | package couchdb 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "reflect" 7 | ) 8 | 9 | var ( 10 | // ErrSetID for setting ID to document which already has one. 11 | ErrSetID = errors.New("id can only be set on new documents") 12 | // ErrNotStruct for not a struct value 13 | ErrNotStruct = errors.New("value not of struct type") 14 | // ErrNotDocumentEmbedded for not a document-embedded value 15 | ErrNotDocumentEmbedded = errors.New("value not Document-embedded") 16 | zero = reflect.Value{} 17 | ) 18 | 19 | // Document represents a document object in database. 20 | type Document struct { 21 | id string 22 | rev string 23 | ID string `json:"_id,omitempty"` // for json only, call SetID/GetID instead 24 | Rev string `json:"_rev,omitempty"` // for json only, call GetRev instead 25 | } 26 | 27 | // DocumentWithID returns a new Document with ID. 28 | func DocumentWithID(id string) Document { 29 | return Document{ 30 | id: id, 31 | } 32 | } 33 | 34 | // SetID sets ID for new document or return error. 35 | func (d *Document) SetID(id string) error { 36 | if d.id != "" { 37 | return ErrSetID 38 | } 39 | d.id = id 40 | return nil 41 | } 42 | 43 | // GetID returns the document ID. 44 | func (d *Document) GetID() string { 45 | return d.id 46 | } 47 | 48 | // SetRev sets revision for document. 49 | func (d *Document) SetRev(rev string) { 50 | d.rev = rev 51 | } 52 | 53 | // GetRev returns the document revision. 54 | func (d *Document) GetRev() string { 55 | return d.rev 56 | } 57 | 58 | // Store stores the document in specified database. 59 | // obj: a Document-embedded struct value, its id and rev will be updated after stored, 60 | // so caller must pass a pointer value. 61 | func Store(db *Database, obj interface{}) error { 62 | ptrValue := reflect.ValueOf(obj) 63 | if ptrValue.Kind() != reflect.Ptr || ptrValue.Elem().Kind() != reflect.Struct { 64 | return ErrNotStruct 65 | } 66 | 67 | if ptrValue.Elem().FieldByName("Document") == zero { 68 | return ErrNotDocumentEmbedded 69 | } 70 | 71 | jsonIDField := ptrValue.Elem().FieldByName("ID") 72 | getIDMethod := ptrValue.MethodByName("GetID") 73 | 74 | idStr := getIDMethod.Call([]reflect.Value{})[0].Interface().(string) 75 | if idStr != "" { 76 | jsonIDField.SetString(idStr) 77 | } 78 | 79 | jsonRevField := ptrValue.Elem().FieldByName("Rev") 80 | getRevMethod := ptrValue.MethodByName("GetRev") 81 | revStr := getRevMethod.Call([]reflect.Value{})[0].Interface().(string) 82 | if revStr != "" { 83 | jsonRevField.SetString(revStr) 84 | } 85 | 86 | doc, err := ToJSONCompatibleMap(ptrValue.Elem().Interface()) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | id, rev, err := db.Save(doc, nil) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | setIDMethod := ptrValue.MethodByName("SetID") 97 | setRevMethod := ptrValue.MethodByName("SetRev") 98 | 99 | if idStr == "" { 100 | setIDMethod.Call([]reflect.Value{reflect.ValueOf(id)}) 101 | } 102 | 103 | setRevMethod.Call([]reflect.Value{reflect.ValueOf(rev)}) 104 | jsonRevField.SetString(rev) 105 | 106 | return nil 107 | } 108 | 109 | // Load loads the document in specified database. 110 | func Load(db *Database, docID string, obj interface{}) error { 111 | ptrValue := reflect.ValueOf(obj) 112 | if ptrValue.Kind() != reflect.Ptr || ptrValue.Elem().Kind() != reflect.Struct { 113 | return ErrNotStruct 114 | } 115 | 116 | if ptrValue.Elem().FieldByName("Document") == zero { 117 | return ErrNotDocumentEmbedded 118 | } 119 | 120 | doc, err := db.Get(docID, nil) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | err = FromJSONCompatibleMap(obj, doc) 126 | if err != nil { 127 | return err 128 | } 129 | 130 | if id, ok := doc["_id"]; ok { 131 | setIDMethod := ptrValue.MethodByName("SetID") 132 | setIDMethod.Call([]reflect.Value{reflect.ValueOf(id)}) 133 | } 134 | 135 | if rev, ok := doc["_rev"]; ok { 136 | setRevMethod := ptrValue.MethodByName("SetRev") 137 | setRevMethod.Call([]reflect.Value{reflect.ValueOf(rev)}) 138 | } 139 | 140 | return nil 141 | } 142 | 143 | // FromJSONCompatibleMap constructs a Document-embedded struct from a JSON-compatible map. 144 | func FromJSONCompatibleMap(obj interface{}, docMap map[string]interface{}) error { 145 | ptrValue := reflect.ValueOf(obj) 146 | if ptrValue.Kind() != reflect.Ptr || ptrValue.Elem().Kind() != reflect.Struct { 147 | return ErrNotStruct 148 | } 149 | 150 | if ptrValue.Elem().FieldByName("Document") == zero { 151 | return ErrNotDocumentEmbedded 152 | } 153 | 154 | data, err := json.Marshal(docMap) 155 | if err != nil { 156 | return err 157 | } 158 | 159 | err = json.Unmarshal(data, obj) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | if id, ok := docMap["_id"]; ok { 165 | setIDMethod := ptrValue.MethodByName("SetID") 166 | setIDMethod.Call([]reflect.Value{reflect.ValueOf(id)}) 167 | } 168 | 169 | if rev, ok := docMap["_rev"]; ok { 170 | setRevMethod := ptrValue.MethodByName("SetRev") 171 | setRevMethod.Call([]reflect.Value{reflect.ValueOf(rev)}) 172 | } 173 | 174 | return nil 175 | } 176 | 177 | // ToJSONCompatibleMap converts a Document-embedded struct into a JSON-compatible map, 178 | // e.g. anything that cannot be jsonified will be ignored silently. 179 | func ToJSONCompatibleMap(obj interface{}) (map[string]interface{}, error) { 180 | structValue := reflect.ValueOf(obj) 181 | if structValue.Kind() != reflect.Struct { 182 | return nil, ErrNotStruct 183 | } 184 | 185 | zero := reflect.Value{} 186 | if structValue.FieldByName("Document") == zero { 187 | return nil, ErrNotDocumentEmbedded 188 | } 189 | 190 | data, err := json.Marshal(obj) 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | doc := map[string]interface{}{} 196 | err = json.Unmarshal(data, &doc) 197 | if err != nil { 198 | return nil, err 199 | } 200 | 201 | return doc, nil 202 | } 203 | 204 | // ViewField represents a view definition value bound to Document. 205 | type ViewField func() (*ViewDefinition, error) 206 | 207 | // NewViewField returns a ViewField function. 208 | // design: the name of the design document. 209 | // 210 | // name: the name of the view. 211 | // 212 | // mapFun: the map function code. 213 | // 214 | // reduceFun: the reduce function code(optional). 215 | // 216 | // language: the name of the programming language used, default is javascript. 217 | // 218 | // wrapper: an optional function for processing the result rows after retrieved. 219 | // 220 | // options: view specific options. 221 | func NewViewField(design, name, mapFun, reduceFun, language string, wrapper func(Row) Row, options map[string]interface{}) ViewField { 222 | f := func() (*ViewDefinition, error) { 223 | return NewViewDefinition(design, name, mapFun, reduceFun, language, wrapper, options) 224 | } 225 | return ViewField(f) 226 | } 227 | -------------------------------------------------------------------------------- /mapping_test.go: -------------------------------------------------------------------------------- 1 | package couchdb 2 | 3 | import ( 4 | "math/big" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | type Post struct { 10 | Title string `json:"title"` 11 | Document 12 | } 13 | 14 | func TestAutomaticID(t *testing.T) { 15 | post := Post{Title: "Foo bar"} 16 | if post.GetID() != "" { 17 | t.Error("post ID not empty", post.ID) 18 | } 19 | 20 | err := Store(mappingDB, &post) 21 | if err != nil { 22 | t.Fatal("document store error", err) 23 | } 24 | 25 | if post.GetID() == "" { 26 | t.Error("post ID empty") 27 | } 28 | 29 | if post.GetRev() == "" { 30 | t.Error("post rev empty") 31 | } 32 | 33 | doc, err := mappingDB.Get(post.GetID(), nil) 34 | if err != nil { 35 | t.Fatal("db get error", err) 36 | } 37 | 38 | if doc["title"].(string) != "Foo bar" { 39 | t.Errorf("doc title %s want Foo bar", doc["title"].(string)) 40 | } 41 | 42 | if doc["_rev"].(string) != post.GetRev() { 43 | t.Errorf("post rev %s want %s", post.GetRev(), doc["_rev"].(string)) 44 | } 45 | } 46 | 47 | func TestExplicitIDByInit(t *testing.T) { 48 | post := Post{Document: DocumentWithID("foo_bar"), Title: "Foo bar"} 49 | if post.GetID() != "foo_bar" { 50 | t.Fatalf("post ID %s want foo_bar", post.GetID()) 51 | } 52 | 53 | err := Store(mappingDB, &post) 54 | if err != nil { 55 | t.Fatal("document store error", err) 56 | } 57 | 58 | doc, err := mappingDB.Get(post.GetID(), nil) 59 | if err != nil { 60 | t.Fatal("db get error", err) 61 | } 62 | 63 | if doc["title"].(string) != "Foo bar" { 64 | t.Errorf("doc title %s want Foo bar", doc["title"].(string)) 65 | } 66 | 67 | if doc["_id"].(string) != post.GetID() { 68 | t.Errorf("post id %s want %s", post.GetID(), doc["_id"].(string)) 69 | } 70 | 71 | if doc["_rev"].(string) != post.GetRev() { 72 | t.Errorf("post rev %s want %s", post.GetRev(), doc["_rev"].(string)) 73 | } 74 | } 75 | 76 | func TestExplicitIDBySetter(t *testing.T) { 77 | post := Post{Title: "Foo bar"} 78 | post.SetID("foo_baz") 79 | 80 | if post.GetID() != "foo_baz" { 81 | t.Errorf("post ID %s want foo_bar", post.GetID()) 82 | } 83 | 84 | err := Store(mappingDB, &post) 85 | if err != nil { 86 | t.Fatal("document store error", err) 87 | } 88 | 89 | doc, err := mappingDB.Get(post.GetID(), nil) 90 | if err != nil { 91 | t.Fatal("db get error", err) 92 | } 93 | 94 | if doc["title"].(string) != "Foo bar" { 95 | t.Errorf("doc title %s want Foo bar", doc["title"].(string)) 96 | } 97 | 98 | if doc["_id"].(string) != post.GetID() { 99 | t.Errorf("post id %s want %s", post.GetID(), doc["_id"].(string)) 100 | } 101 | 102 | if doc["_rev"].(string) != post.GetRev() { 103 | t.Errorf("post rev %s want %s", post.GetRev(), doc["_rev"].(string)) 104 | } 105 | } 106 | 107 | func TestChangeIDFailure(t *testing.T) { 108 | post := Post{Title: "Foo bar"} 109 | 110 | err := Store(mappingDB, &post) 111 | if err != nil { 112 | t.Fatal("document store error", err) 113 | } 114 | 115 | err = Load(mappingDB, post.GetID(), &post) 116 | if err != nil { 117 | t.Fatal("document load error", err) 118 | } 119 | 120 | err = post.SetID("foo_bar") 121 | if err != ErrSetID { 122 | t.Errorf("document set id error %v want %v", err, ErrSetID) 123 | } 124 | } 125 | 126 | type NotDocument struct{} 127 | 128 | func TestNotADocument(t *testing.T) { 129 | notDocument := NotDocument{} 130 | err := Store(mappingDB, ¬Document) 131 | if err != ErrNotDocumentEmbedded { 132 | t.Fatalf("store error %v want %v", err, ErrNotDocumentEmbedded) 133 | } 134 | } 135 | 136 | type User struct { 137 | Name string `json:"name"` 138 | Age int `json:"age"` 139 | Marriage Marriage `json:"marriage"` 140 | Document 141 | } 142 | 143 | type Marriage struct { 144 | Male bool `json:"male"` 145 | Married string `json:"married"` 146 | } 147 | 148 | func TestNestedStruct(t *testing.T) { 149 | jack := User{ 150 | Name: "Jack", 151 | Age: 18, 152 | Document: DocumentWithID("jack"), 153 | Marriage: Marriage{Male: true, Married: "Lucy"}, 154 | } 155 | 156 | err := Store(mappingDB, &jack) 157 | if err != nil { 158 | t.Fatal("store error", err) 159 | } 160 | 161 | doc, err := mappingDB.Get("jack", nil) 162 | if err != nil { 163 | t.Fatal("db get error", err) 164 | } 165 | 166 | objDoc, err := ToJSONCompatibleMap(jack) 167 | if err != nil { 168 | t.Fatal("to json compatible error", err) 169 | } 170 | 171 | if !reflect.DeepEqual(doc, objDoc) { 172 | t.Error("doc and obj not equal") 173 | } 174 | 175 | docObj := User{} 176 | err = FromJSONCompatibleMap(&docObj, doc) 177 | if err != nil { 178 | t.Fatal("from json compatible error", err) 179 | } 180 | 181 | if !reflect.DeepEqual(jack, docObj) { 182 | t.Error("objs not equal") 183 | } 184 | } 185 | 186 | func TestBatchUpdate(t *testing.T) { 187 | post1 := Post{Title: "Foo bar"} 188 | post2 := Post{Title: "Foo baz"} 189 | 190 | postMap1, err := ToJSONCompatibleMap(post1) 191 | if err != nil { 192 | t.Fatal("to json compatible error", err) 193 | } 194 | 195 | postMap2, err := ToJSONCompatibleMap(post2) 196 | if err != nil { 197 | t.Fatal("to json compatible error", err) 198 | } 199 | 200 | results, err := mappingDB.Update([]map[string]interface{}{postMap1, postMap2}, nil) 201 | if err != nil { 202 | t.Fatal("db update error", err) 203 | } 204 | 205 | if len(results) != 2 { 206 | t.Fatalf("len(results) = %d want 2", len(results)) 207 | } 208 | 209 | for idx, res := range results { 210 | if res.Err != nil { 211 | t.Errorf("result %d error %v", idx, res.Err) 212 | } 213 | } 214 | } 215 | 216 | func TestStoreExisting(t *testing.T) { 217 | post := Post{Title: "Foo bar"} 218 | err := Store(mappingDB, &post) 219 | if err != nil { 220 | t.Fatal("document store error", err) 221 | } 222 | 223 | err = Store(mappingDB, &post) 224 | if err != nil { 225 | t.Fatal("document store error", err) 226 | } 227 | 228 | results, err := mappingDB.View("_all_docs", nil, nil) 229 | if err != nil { 230 | t.Fatal("db view _all_docs error", err) 231 | } 232 | 233 | rows, err := results.Rows() 234 | if err != nil { 235 | t.Fatal("rows error", err) 236 | } 237 | 238 | total := 0 239 | for _, row := range rows { 240 | if post.GetID() == row.ID { 241 | total++ 242 | } 243 | } 244 | 245 | if total != 1 { 246 | t.Errorf("total %d want 1", total) 247 | } 248 | } 249 | 250 | type PostWithComment struct { 251 | Title string `json:"title"` 252 | Comments []map[string]string `json:"comments"` 253 | Document 254 | } 255 | 256 | func TestCompareDocWithObj(t *testing.T) { 257 | postDoc := map[string]interface{}{ 258 | "comments": []map[string]string{ 259 | {"author": "Joe", "content": "Hey"}, 260 | }, 261 | } 262 | err := mappingDB.Set("test", postDoc) 263 | if err != nil { 264 | t.Fatal("db set error", err) 265 | } 266 | post1 := PostWithComment{} 267 | err = Load(mappingDB, "test", &post1) 268 | if err != nil { 269 | t.Fatal("document load error", err) 270 | } 271 | post2 := PostWithComment{} 272 | err = FromJSONCompatibleMap(&post2, postDoc) 273 | if err != nil { 274 | t.Fatal("from map error", err) 275 | } 276 | if !reflect.DeepEqual(post1, post2) { 277 | t.Errorf("post1 %v != post2 %v", post1, post2) 278 | } 279 | } 280 | 281 | type Thing struct { 282 | Numbers []float64 283 | Document 284 | } 285 | 286 | func compareFloat(x, y float64) int { 287 | a := big.NewFloat(x) 288 | b := big.NewFloat(y) 289 | return a.Cmp(b) 290 | } 291 | 292 | func TestSliceFieldFloat(t *testing.T) { 293 | err := mappingDB.Set("float", map[string]interface{}{"numbers": []float64{1.0, 2.0}}) 294 | if err != nil { 295 | t.Fatal("db set error", err) 296 | } 297 | 298 | thing := Thing{} 299 | err = Load(mappingDB, "float", &thing) 300 | if err != nil { 301 | t.Fatal("document load error", err) 302 | } 303 | 304 | if compareFloat(thing.Numbers[0], 1.0) != 0 { 305 | t.Errorf("thing numbers[0] %v want 1.0", thing.Numbers[0]) 306 | } 307 | } 308 | 309 | func TestViewFieldProperty(t *testing.T) { 310 | err := Store(mappingDB, &testItem) 311 | if err != nil { 312 | t.Fatal("document store error", err) 313 | } 314 | 315 | viewDef, err := testItem.withIncludeDocs() 316 | if err != nil { 317 | t.Fatal("view with include docs error", err) 318 | } 319 | 320 | results, err := viewDef.View(mappingDB, nil) 321 | if err != nil { 322 | t.Fatal("view definition error", err) 323 | } 324 | 325 | rows, err := results.Rows() 326 | if err != nil { 327 | t.Fatal("rows error", err) 328 | } 329 | 330 | for _, row := range rows { 331 | val := row.Val.(map[string]interface{}) 332 | id, rev := val["_id"].(string), val["_rev"].(string) 333 | if id == testItem.GetID() { 334 | if rev != testItem.GetRev() { 335 | t.Errorf("rows[0] Rev %s want %s", rev, testItem.GetRev()) 336 | } 337 | } 338 | } 339 | 340 | } 341 | 342 | func TestView(t *testing.T) { 343 | err := Store(mappingDB, &testItem) 344 | if err != nil { 345 | t.Fatal("document store error", err) 346 | } 347 | 348 | results, err := mappingDB.View("test/withoutIncludeDocs", nil, nil) 349 | if err != nil { 350 | t.Fatal("db view error", err) 351 | } 352 | 353 | rows, err := results.Rows() 354 | if err != nil { 355 | t.Fatal("rows error", err) 356 | } 357 | 358 | for _, row := range rows { 359 | val := row.Val.(map[string]interface{}) 360 | id, rev := val["_id"].(string), val["_rev"].(string) 361 | if id == testItem.GetID() { 362 | if rev != testItem.GetRev() { 363 | t.Errorf("rows[0] Rev %s want %s", rev, testItem.GetRev()) 364 | } 365 | } 366 | } 367 | 368 | results, err = mappingDB.View("test/withIncludeDocs", nil, nil) 369 | if err != nil { 370 | t.Fatal("db view error", err) 371 | } 372 | 373 | rows, err = results.Rows() 374 | if err != nil { 375 | t.Fatal("rows error", err) 376 | } 377 | 378 | for _, row := range rows { 379 | val := row.Val.(map[string]interface{}) 380 | id, rev := val["_id"].(string), val["_rev"].(string) 381 | if id == testItem.GetID() { 382 | if rev != testItem.GetRev() { 383 | t.Errorf("rows[0] Rev %s want %s", rev, testItem.GetRev()) 384 | } 385 | } 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /resource.go: -------------------------------------------------------------------------------- 1 | package couchdb 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/json" 7 | "errors" 8 | "io" 9 | "io/ioutil" 10 | "net/http" 11 | "net/url" 12 | "path" 13 | "strings" 14 | ) 15 | 16 | var ( 17 | httpClient *http.Client 18 | 19 | // ErrNotModified for HTTP status code 304 20 | ErrNotModified = errors.New("status 304 - not modified") 21 | // ErrBadRequest for HTTP status code 400 22 | ErrBadRequest = errors.New("status 400 - bad request") 23 | // ErrUnauthorized for HTTP status code 401 24 | ErrUnauthorized = errors.New("status 401 - unauthorized") 25 | // ErrForbidden for HTTP status code 403 26 | ErrForbidden = errors.New("status 403 - forbidden") 27 | // ErrNotFound for HTTP status code 404 28 | ErrNotFound = errors.New("status 404 - not found") 29 | // ErrResourceNotAllowed for HTTP status code 405 30 | ErrResourceNotAllowed = errors.New("status 405 - resource not allowed") 31 | // ErrNotAcceptable for HTTP status code 406 32 | ErrNotAcceptable = errors.New("status 406 - not acceptable") 33 | // ErrConflict for HTTP status code 409 34 | ErrConflict = errors.New("status 409 - conflict") 35 | // ErrPreconditionFailed for HTTP status code 412 36 | ErrPreconditionFailed = errors.New("status 412 - precondition failed") 37 | // ErrBadContentType for HTTP status code 415 38 | ErrBadContentType = errors.New("status 415 - bad content type") 39 | // ErrRequestRangeNotSatisfiable for HTTP status code 416 40 | ErrRequestRangeNotSatisfiable = errors.New("status 416 - requested range not satisfiable") 41 | // ErrExpectationFailed for HTTP status code 417 42 | ErrExpectationFailed = errors.New("status 417 - expectation failed") 43 | // ErrInternalServerError for HTTP status code 500 44 | ErrInternalServerError = errors.New("status 500 - internal server error") 45 | 46 | statusErrMap = map[int]error{ 47 | 304: ErrNotModified, 48 | 400: ErrBadRequest, 49 | 401: ErrUnauthorized, 50 | 403: ErrForbidden, 51 | 404: ErrNotFound, 52 | 405: ErrResourceNotAllowed, 53 | 406: ErrNotAcceptable, 54 | 409: ErrConflict, 55 | 412: ErrPreconditionFailed, 56 | 415: ErrBadContentType, 57 | 416: ErrRequestRangeNotSatisfiable, 58 | 417: ErrExpectationFailed, 59 | 500: ErrInternalServerError, 60 | } 61 | ) 62 | 63 | func init() { 64 | httpClient = http.DefaultClient 65 | } 66 | 67 | // Resource handles all requests to CouchDB 68 | type Resource struct { 69 | header http.Header 70 | base *url.URL 71 | } 72 | 73 | // NewResource returns a newly-created Resource instance 74 | func NewResource(urlStr string, header http.Header) (*Resource, error) { 75 | u, err := url.Parse(urlStr) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | if strings.HasPrefix(urlStr, "https") { 81 | tr := &http.Transport{ 82 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 83 | } 84 | httpClient = &http.Client{ 85 | Transport: tr, 86 | } 87 | } 88 | 89 | h := http.Header{} 90 | if header != nil { 91 | h = header 92 | } 93 | 94 | return &Resource{ 95 | header: h, 96 | base: u, 97 | }, nil 98 | } 99 | 100 | func combine(base *url.URL, resPath string) (*url.URL, error) { 101 | if resPath == "" { 102 | return base, nil 103 | } 104 | u, err := base.Parse(path.Join(base.Path, resPath)) 105 | return u, err 106 | } 107 | 108 | // NewResourceWithURL returns newly created *Resource combined with resource string. 109 | func (r *Resource) NewResourceWithURL(resStr string) (*Resource, error) { 110 | u, err := combine(r.base, resStr) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | return &Resource{ 116 | header: r.header, 117 | base: u, 118 | }, nil 119 | } 120 | 121 | // Head is a wrapper around http.Head 122 | func (r *Resource) Head(path string, header http.Header, params url.Values) (http.Header, []byte, error) { 123 | u, err := combine(r.base, path) 124 | if err != nil { 125 | return nil, nil, err 126 | } 127 | return request(http.MethodHead, u, header, nil, params) 128 | } 129 | 130 | // Get is a wrapper around http.Get 131 | func (r *Resource) Get(path string, header http.Header, params url.Values) (http.Header, []byte, error) { 132 | u, err := combine(r.base, path) 133 | if err != nil { 134 | return nil, nil, err 135 | } 136 | return request(http.MethodGet, u, header, nil, params) 137 | } 138 | 139 | // Post is a wrapper around http.Post 140 | func (r *Resource) Post(path string, header http.Header, body []byte, params url.Values) (http.Header, []byte, error) { 141 | u, err := combine(r.base, path) 142 | if err != nil { 143 | return nil, nil, err 144 | } 145 | return request(http.MethodPost, u, header, bytes.NewReader(body), params) 146 | } 147 | 148 | // Delete is a wrapper around http.Delete 149 | func (r *Resource) Delete(path string, header http.Header, params url.Values) (http.Header, []byte, error) { 150 | u, err := combine(r.base, path) 151 | if err != nil { 152 | return nil, nil, err 153 | } 154 | return request(http.MethodDelete, u, header, nil, params) 155 | } 156 | 157 | // Put is a wrapper around http.Put 158 | func (r *Resource) Put(path string, header http.Header, body []byte, params url.Values) (http.Header, []byte, error) { 159 | u, err := combine(r.base, path) 160 | if err != nil { 161 | return nil, nil, err 162 | } 163 | return request(http.MethodPut, u, header, bytes.NewReader(body), params) 164 | } 165 | 166 | // GetJSON issues a GET to the specified URL, with data returned as json 167 | func (r *Resource) GetJSON(path string, header http.Header, params url.Values) (http.Header, []byte, error) { 168 | u, err := combine(r.base, path) 169 | if err != nil { 170 | return nil, nil, err 171 | } 172 | return request(http.MethodGet, u, header, nil, params) 173 | } 174 | 175 | // PostJSON issues a POST to the specified URL, with data returned as json 176 | func (r *Resource) PostJSON(path string, header http.Header, body map[string]interface{}, params url.Values) (http.Header, []byte, error) { 177 | u, err := combine(r.base, path) 178 | if err != nil { 179 | return nil, nil, err 180 | } 181 | 182 | jsonBody, err := json.Marshal(body) 183 | if err != nil { 184 | return nil, nil, err 185 | } 186 | 187 | return request(http.MethodPost, u, header, bytes.NewReader(jsonBody), params) 188 | } 189 | 190 | // DeleteJSON issues a DELETE to the specified URL, with data returned as json 191 | func (r *Resource) DeleteJSON(path string, header http.Header, params url.Values) (http.Header, []byte, error) { 192 | u, err := combine(r.base, path) 193 | if err != nil { 194 | return nil, nil, err 195 | } 196 | 197 | return request(http.MethodDelete, u, header, nil, params) 198 | } 199 | 200 | // PutJSON issues a PUT to the specified URL, with data returned as json 201 | func (r *Resource) PutJSON(path string, header http.Header, body map[string]interface{}, params url.Values) (http.Header, []byte, error) { 202 | u, err := combine(r.base, path) 203 | if err != nil { 204 | return nil, nil, err 205 | } 206 | 207 | jsonBody, err := json.Marshal(body) 208 | if err != nil { 209 | return nil, nil, err 210 | } 211 | 212 | return request(http.MethodPut, u, header, bytes.NewReader(jsonBody), params) 213 | } 214 | 215 | func checkHTTPStatusError(status int) error { 216 | err, ok := statusErrMap[status] 217 | if !ok { 218 | return nil 219 | } 220 | return err 221 | } 222 | 223 | // helper function to make real request 224 | func request(method string, u *url.URL, header http.Header, body io.Reader, params url.Values) (http.Header, []byte, error) { 225 | method = strings.ToUpper(method) 226 | 227 | u.RawQuery = params.Encode() 228 | var username, password string 229 | if u.User != nil { 230 | username = u.User.Username() 231 | password, _ = u.User.Password() 232 | } 233 | req, err := http.NewRequest(method, u.String(), body) 234 | if err != nil { 235 | return nil, nil, err 236 | } 237 | 238 | if len(username) > 0 && len(password) > 0 { 239 | req.SetBasicAuth(username, password) 240 | } 241 | 242 | // Accept and Content-type are highly recommended for CouchDB 243 | setDefault(&req.Header, "Accept", "application/json") 244 | setDefault(&req.Header, "Content-Type", "application/json") 245 | updateHeader(&req.Header, &header) 246 | updateHeader(&req.Header, cookieAuthHeader) 247 | 248 | rsp, err := httpClient.Do(req) 249 | if err != nil { 250 | return nil, nil, err 251 | } 252 | defer rsp.Body.Close() 253 | data, err := ioutil.ReadAll(rsp.Body) 254 | if err != nil { 255 | return nil, nil, err 256 | } 257 | 258 | return rsp.Header, data, checkHTTPStatusError(rsp.StatusCode) 259 | } 260 | 261 | // setDefault sets the default value if key not existe in header 262 | func setDefault(header *http.Header, key, value string) { 263 | if header.Get(key) == "" { 264 | header.Set(key, value) 265 | } 266 | } 267 | 268 | // updateHeader updates existing header with new values 269 | func updateHeader(header *http.Header, extra *http.Header) { 270 | if header != nil && extra != nil { 271 | for k := range *extra { 272 | header.Set(k, extra.Get(k)) 273 | } 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package couchdb 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | var cookieAuthHeader *http.Header 13 | 14 | // Server represents a CouchDB server instance. 15 | type Server struct { 16 | resource *Resource 17 | } 18 | 19 | // NewServer creates a CouchDB server instance in address urlStr. 20 | func NewServer(urlStr string) (*Server, error) { 21 | return newServer(urlStr, true) 22 | } 23 | 24 | // NewServerNoFullCommit creates a CouchDB server instance in address urlStr 25 | // with X-Couch-Full-Commit disabled. 26 | func NewServerNoFullCommit(urlStr string) (*Server, error) { 27 | return newServer(urlStr, false) 28 | } 29 | 30 | func newServer(urlStr string, fullCommit bool) (*Server, error) { 31 | res, err := NewResource(urlStr, nil) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | s := &Server{ 37 | resource: res, 38 | } 39 | 40 | if !fullCommit { 41 | s.resource.header.Set("X-Couch-Full-Commit", "false") 42 | } 43 | return s, nil 44 | } 45 | 46 | // Config returns the entire CouchDB server configuration as JSON structure. 47 | func (s *Server) Config(node string) (map[string]map[string]string, error) { 48 | _, data, err := s.resource.GetJSON(fmt.Sprintf("_node/%s/_config", node), nil, nil) 49 | if err != nil { 50 | return nil, err 51 | } 52 | var config map[string]map[string]string 53 | err = json.Unmarshal(data, &config) 54 | if err != nil { 55 | return nil, err 56 | } 57 | return config, nil 58 | } 59 | 60 | // Version returns the version info about CouchDB instance. 61 | func (s *Server) Version() (string, error) { 62 | var jsonMap map[string]interface{} 63 | 64 | _, data, err := s.resource.GetJSON("", nil, nil) 65 | if err != nil { 66 | return "", err 67 | } 68 | err = json.Unmarshal(data, &jsonMap) 69 | if err != nil { 70 | return "", err 71 | } 72 | 73 | return jsonMap["version"].(string), nil 74 | } 75 | 76 | func (s *Server) String() string { 77 | return fmt.Sprintf("Server %s", s.resource.base) 78 | } 79 | 80 | // ActiveTasks lists of running tasks. 81 | func (s *Server) ActiveTasks() ([]interface{}, error) { 82 | var tasks []interface{} 83 | _, data, err := s.resource.GetJSON("_active_tasks", nil, nil) 84 | if err != nil { 85 | return nil, err 86 | } 87 | err = json.Unmarshal(data, &tasks) 88 | if err != nil { 89 | return nil, err 90 | } 91 | return tasks, nil 92 | } 93 | 94 | // DBs returns a list of all the databases in the CouchDB server instance. 95 | func (s *Server) DBs() ([]string, error) { 96 | var dbs []string 97 | _, data, err := s.resource.GetJSON("_all_dbs", nil, nil) 98 | if err != nil { 99 | return nil, err 100 | } 101 | err = json.Unmarshal(data, &dbs) 102 | if err != nil { 103 | return nil, err 104 | } 105 | return dbs, nil 106 | } 107 | 108 | // Stats returns a JSON object containing the statistics for the running server. 109 | func (s *Server) Stats(node, entry string) (map[string]interface{}, error) { 110 | var stats map[string]interface{} 111 | _, data, err := s.resource.GetJSON(fmt.Sprintf("_node/%s/_stats/%s", node, entry), nil, url.Values{}) 112 | if err != nil { 113 | return nil, err 114 | } 115 | err = json.Unmarshal(data, &stats) 116 | if err != nil { 117 | return nil, err 118 | } 119 | return stats, nil 120 | } 121 | 122 | // Len returns the number of dbs in CouchDB server instance. 123 | func (s *Server) Len() (int, error) { 124 | dbs, err := s.DBs() 125 | if err != nil { 126 | return -1, err 127 | } 128 | return len(dbs), nil 129 | } 130 | 131 | // Create returns a database instance with the given name, returns true if created, 132 | // if database already existed, returns false, *Database will be nil if failed. 133 | func (s *Server) Create(name string) (*Database, error) { 134 | _, _, err := s.resource.PutJSON(name, nil, nil, nil) 135 | 136 | // ErrPreconditionFailed means database with the given name already existed 137 | if err != nil && err != ErrPreconditionFailed { 138 | return nil, err 139 | } 140 | 141 | db, getErr := s.Get(name) 142 | if getErr != nil { 143 | return nil, getErr 144 | } 145 | return db, err 146 | } 147 | 148 | // Delete deletes a database with the given name. Return false if failed. 149 | func (s *Server) Delete(name string) error { 150 | _, _, err := s.resource.DeleteJSON(name, nil, nil) 151 | return err 152 | } 153 | 154 | // Get gets a database instance with the given name. Return nil if failed. 155 | func (s *Server) Get(name string) (*Database, error) { 156 | res, err := s.resource.NewResourceWithURL(name) 157 | if err != nil { 158 | return nil, err 159 | } 160 | 161 | db, err := NewDatabaseWithResource(res) 162 | if err != nil { 163 | return nil, err 164 | } 165 | 166 | _, _, err = db.resource.Head("", nil, nil) 167 | if err != nil { 168 | return nil, err 169 | } 170 | return db, nil 171 | } 172 | 173 | // Contains returns true if a db with given name exsited. 174 | func (s *Server) Contains(name string) bool { 175 | _, _, err := s.resource.Head(name, nil, nil) 176 | return err == nil 177 | } 178 | 179 | // Membership displays the nodes that are part of the cluster as clusterNodes. 180 | // The field allNodes displays all nodes this node knows about, including the 181 | // ones that are part of cluster. 182 | func (s *Server) Membership() ([]string, []string, error) { 183 | var jsonMap map[string]*json.RawMessage 184 | 185 | _, data, err := s.resource.GetJSON("_membership", nil, nil) 186 | if err != nil { 187 | return nil, nil, err 188 | } 189 | 190 | err = json.Unmarshal(data, &jsonMap) 191 | if err != nil { 192 | return nil, nil, err 193 | } 194 | 195 | var allNodes []string 196 | var clusterNodes []string 197 | 198 | err = json.Unmarshal(*jsonMap["all_nodes"], &allNodes) 199 | if err != nil { 200 | return nil, nil, err 201 | } 202 | 203 | err = json.Unmarshal(*jsonMap["cluster_nodes"], &clusterNodes) 204 | if err != nil { 205 | return nil, nil, err 206 | } 207 | 208 | return allNodes, clusterNodes, nil 209 | } 210 | 211 | // Replicate requests, configure or stop a replication operation. 212 | func (s *Server) Replicate(source, target string, options map[string]interface{}) (map[string]interface{}, error) { 213 | var result map[string]interface{} 214 | 215 | body := map[string]interface{}{ 216 | "source": source, 217 | "target": target, 218 | } 219 | 220 | if options != nil { 221 | for k, v := range options { 222 | body[k] = v 223 | } 224 | } 225 | 226 | _, data, err := s.resource.PostJSON("_replicate", nil, body, nil) 227 | if err != nil { 228 | return nil, err 229 | } 230 | json.Unmarshal(data, &result) 231 | 232 | return result, nil 233 | } 234 | 235 | // UUIDs requests one or more Universally Unique Identifiers from the CouchDB instance. 236 | // The response is a JSON object providing a list of UUIDs. 237 | // count - Number of UUIDs to return. Default is 1. 238 | func (s *Server) UUIDs(count int) ([]string, error) { 239 | if count <= 0 { 240 | count = 1 241 | } 242 | 243 | values := url.Values{} 244 | values.Set("count", strconv.Itoa(count)) 245 | 246 | _, data, err := s.resource.GetJSON("_uuids", nil, values) 247 | if err != nil { 248 | return nil, err 249 | } 250 | 251 | var jsonMap map[string]*json.RawMessage 252 | err = json.Unmarshal(data, &jsonMap) 253 | if err != nil { 254 | return nil, err 255 | } 256 | 257 | var uuids []string 258 | err = json.Unmarshal(*jsonMap["uuids"], &uuids) 259 | if err != nil { 260 | return nil, err 261 | } 262 | 263 | return uuids, nil 264 | } 265 | 266 | // newResource returns an url string representing a resource under server. 267 | func (s *Server) newResource(resource string) string { 268 | resourceURL, err := s.resource.base.Parse(resource) 269 | if err != nil { 270 | return "" 271 | } 272 | return resourceURL.String() 273 | } 274 | 275 | // AddUser adds regular user in authentication database. 276 | // Returns id and rev of the registered user. 277 | func (s *Server) AddUser(name, password string, roles []string) (string, string, error) { 278 | var id, rev string 279 | db, err := s.Get("_users") 280 | if err != nil { 281 | return "", "", err 282 | } 283 | 284 | if roles == nil { 285 | roles = []string{} 286 | } 287 | 288 | userDoc := map[string]interface{}{ 289 | "_id": "org.couchdb.user:" + name, 290 | "name": name, 291 | "password": password, 292 | "roles": roles, 293 | "type": "user", 294 | } 295 | 296 | id, rev, err = db.Save(userDoc, nil) 297 | if err != nil { 298 | return id, rev, err 299 | } 300 | return id, rev, nil 301 | } 302 | 303 | // Login regular user in CouchDB, returns authentication token. 304 | func (s *Server) Login(name, password string) (string, error) { 305 | body := map[string]interface{}{ 306 | "name": name, 307 | "password": password, 308 | } 309 | header, _, err := s.resource.PostJSON("_session", nil, body, nil) 310 | if err != nil { 311 | return "", err 312 | } 313 | 314 | tokenPart := strings.Split(header.Get("Set-Cookie"), ";")[0] 315 | token := strings.Split(tokenPart, "=")[1] 316 | 317 | setupCookieAuth(token) 318 | 319 | return token, err 320 | } 321 | 322 | // VerifyToken returns error if user's token is invalid. 323 | func (s *Server) VerifyToken(token string) error { 324 | header := http.Header{} 325 | header.Set("Cookie", strings.Join([]string{"AuthSession", token}, "=")) 326 | _, _, err := s.resource.GetJSON("_session", header, nil) 327 | return err 328 | } 329 | 330 | // Logout regular user in CouchDB 331 | func (s *Server) Logout(token string) error { 332 | header := http.Header{} 333 | header.Set("Cookie", strings.Join([]string{"AuthSession", token}, "=")) 334 | _, _, err := s.resource.DeleteJSON("_session", header, nil) 335 | 336 | clearCookieAuth() 337 | 338 | return err 339 | } 340 | 341 | // RemoveUser removes regular user in authentication database. 342 | func (s *Server) RemoveUser(name string) error { 343 | db, err := s.Get("_users") 344 | if err != nil { 345 | return err 346 | } 347 | docID := "org.couchdb.user:" + name 348 | return db.Delete(docID) 349 | } 350 | 351 | func setupCookieAuth(token string) { 352 | cookieAuthHeader = &http.Header{} 353 | cookieAuthHeader.Add("Cookie", fmt.Sprintf("AuthSession=%s", token)) 354 | } 355 | 356 | func clearCookieAuth() { 357 | cookieAuthHeader = nil 358 | } -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package couchdb 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | const ( 12 | NumDocs = 100 13 | ) 14 | 15 | type unitTestItem struct { 16 | withIncludeDocs ViewField 17 | withoutIncludeDocs ViewField 18 | Document 19 | } 20 | 21 | var ( 22 | server *Server 23 | testsDB *Database 24 | movieDB *Database 25 | designDB *Database 26 | iterDB *Database 27 | defnDB *Database 28 | showListDB *Database 29 | updateDB *Database 30 | mappingDB *Database 31 | 32 | testItem = unitTestItem{ 33 | withIncludeDocs: NewViewField("test", "withIncludeDocs", allMapFunc, "", "", nil, map[string]interface{}{"include_docs": true}), 34 | withoutIncludeDocs: NewViewField("test", "withoutIncludeDocs", allMapFunc, "", "", nil, nil), 35 | } 36 | 37 | allMapFunc = `function(doc) { emit(doc._id, doc); }` 38 | 39 | movies = []map[string]interface{}{ 40 | { 41 | "_id": "976059", 42 | "title": "Spacecataz", 43 | "year": 2004, 44 | "rating": nil, 45 | "runtime": "10 min", 46 | "genre": []string{ 47 | "Animation", 48 | "Short", 49 | "Comedy", 50 | "Sci-Fi", 51 | }, 52 | "director": "Dave Willis", 53 | "writer": []string{ 54 | "Matt Maiellaro", 55 | "Dave Willis", 56 | }, 57 | "cast": []string{ 58 | "Dave Willis", 59 | "Matt Maiellaro", 60 | "Andy Merrill", 61 | "Mike Schatz", 62 | }, 63 | "poster": nil, 64 | "imdb": map[string]interface{}{ 65 | "rating": 8, 66 | "votes": 130, 67 | "id": "tt0976059", 68 | }, 69 | }, 70 | { 71 | "_id": "976197", 72 | "title": "American Psyche", 73 | "year": 2007, 74 | "rating": nil, 75 | "runtime": "55 min", 76 | "genre": []string{ 77 | "Documentary", 78 | }, 79 | "director": "Paul van den Boom", 80 | "writer": []string{ 81 | "Paul van den Boom (creator)", 82 | "Franois Le Goarant de Tromelin (creator)", 83 | }, 84 | "cast": []string{ 85 | "Katherine J. Eakin", 86 | "Mahnaz M. Shabbir", 87 | "Rene Doria", 88 | "Peter Koper", 89 | }, 90 | "poster": "http://ia.media-imdb.com/images/M/MV5BMTM3NTg5NDE2N15BMl5BanBnXkFtZTcwODI0MjM1MQ@@._V1_SX300.jpg", 91 | "imdb": map[string]interface{}{ 92 | "rating": 8.2, 93 | "votes": 77, 94 | "id": "tt0976197", 95 | }, 96 | }, 97 | { 98 | "_id": "976221", 99 | "title": "Voliminal: Inside the Nine", 100 | "year": 2006, 101 | "rating": nil, 102 | "runtime": "N/A", 103 | "genre": []string{ 104 | "Documentary", 105 | }, 106 | "director": "Shawn Crahan", 107 | "writer": nil, 108 | "cast": []string{ 109 | "Shawn Crahan", 110 | "Chris Fehn", 111 | "Paul Gray", 112 | "Craig Jones", 113 | }, 114 | "poster": nil, 115 | "imdb": map[string]interface{}{ 116 | "rating": 8.1, 117 | "votes": 125, 118 | "id": "tt0976221", 119 | }, 120 | }, 121 | { 122 | "_id": "97628", 123 | "title": "The Johnstown Flood", 124 | "year": 1989, 125 | "rating": nil, 126 | "runtime": "26 min", 127 | "genre": []string{ 128 | "Documentary", 129 | "Short", 130 | }, 131 | "director": "Charles Guggenheim", 132 | "writer": []string{ 133 | "Charles Guggenheim", 134 | }, 135 | "cast": []string{ 136 | "Len Cariou", 137 | "Elam Bender", 138 | "Randy Bender", 139 | "Clarita Berger", 140 | }, 141 | "poster": "http://ia.media-imdb.com/images/M/MV5BMTc2NTc3MzQ5MF5BMl5BanBnXkFtZTcwMjU5ODkwNg@@._V1_SX300.jpg", 142 | "imdb": map[string]interface{}{ 143 | "rating": 7.8, 144 | "votes": 75, 145 | "id": "tt0097628", 146 | }, 147 | }, 148 | { 149 | "_id": "97661", 150 | "title": "Gundam 0080: A War in the Pocket", 151 | "year": 1989, 152 | "rating": "NOT RATED", 153 | "runtime": "N/A", 154 | "genre": []string{ 155 | "Animation", 156 | "Action", 157 | "Drama", 158 | }, 159 | "director": "N/A", 160 | "writer": nil, 161 | "cast": []string{ 162 | "Daisuke Namikawa", 163 | "Kji Tsujitani", 164 | "Megumi Hayashibara", 165 | "Brianne Brozey", 166 | }, 167 | "poster": "http://ia.media-imdb.com/images/M/MV5BMTk3NjU2ODQ1Ml5BMl5BanBnXkFtZTcwNzY4MzYyMQ@@._V1_SX300.jpg", 168 | "imdb": map[string]interface{}{ 169 | "rating": 8, 170 | "votes": 475, 171 | "id": "tt0097661", 172 | }, 173 | }, 174 | { 175 | "_id": "97690", 176 | "title": "Kuduz", 177 | "year": 1989, 178 | "rating": nil, 179 | "runtime": "N/A", 180 | "genre": []string{ 181 | "Drama", 182 | }, 183 | "director": "Ademir Kenovic", 184 | "writer": []string{ 185 | "Ademir Kenovic", 186 | "Abdulah Sidran", 187 | }, 188 | "cast": []string{ 189 | "Slobodan Custic", 190 | "Snezana Bogdanovic", 191 | "Bozidar Bunjevac", 192 | "Branko Djuric", 193 | }, 194 | "poster": nil, 195 | "imdb": map[string]interface{}{ 196 | "rating": 8.1, 197 | "votes": 342, 198 | "id": "tt0097690", 199 | }, 200 | }, 201 | { 202 | "_id": "977224", 203 | "title": "Mere Oblivion", 204 | "year": 2007, 205 | "rating": nil, 206 | "runtime": "N/A", 207 | "genre": []string{ 208 | "Short", 209 | "Comedy", 210 | }, 211 | "director": "Burleigh Smith", 212 | "writer": []string{ 213 | "Burleigh Smith", 214 | }, 215 | "cast": []string{ 216 | "Burleigh Smith", 217 | "Elizabeth Caiacob", 218 | "Michael Su", 219 | "Kate Ritchie", 220 | }, 221 | "poster": nil, 222 | "imdb": map[string]interface{}{ 223 | "rating": 7.9, 224 | "votes": 284, 225 | "id": "tt0977224", 226 | }, 227 | }, 228 | { 229 | "_id": "97727", 230 | "title": "A legnyanya", 231 | "year": 1989, 232 | "rating": nil, 233 | "runtime": "80 min", 234 | "genre": []string{ 235 | "Comedy", 236 | }, 237 | "director": "Dezs Garas", 238 | "writer": []string{ 239 | "Dezs Garas (screenplay)", 240 | "Gyrgy Schwajda", 241 | }, 242 | "cast": []string{ 243 | "Ferenc Kllai", 244 | "Kroly Eperjes", 245 | "Judit Pogny", 246 | "Dezs Garas", 247 | }, 248 | "poster": nil, 249 | "imdb": map[string]interface{}{ 250 | "rating": 7.8, 251 | "votes": 786, 252 | "id": "tt0097727", 253 | }, 254 | }, 255 | { 256 | "_id": "97757", 257 | "title": "The Little Mermaid", 258 | "year": 1989, 259 | "rating": "G", 260 | "runtime": "83 min", 261 | "genre": []string{ 262 | "Animation", 263 | "Family", 264 | "Fantasy", 265 | }, 266 | "director": "Ron Clements, John Musker", 267 | "writer": []string{ 268 | "John Musker", 269 | "Ron Clements", 270 | "Hans Christian Andersen (fairy tale)", 271 | "Howard Ashman (additional dialogue)", 272 | "Gerrit Graham (additional dialogue)", 273 | "Sam Graham (additional dialogue)", 274 | "Chris Hubbell (additional dialogue)", 275 | }, 276 | "cast": []string{ 277 | "Rene Auberjonois", 278 | "Christopher Daniel Barnes", 279 | "Jodi Benson", 280 | "Pat Carroll", 281 | }, 282 | "poster": "http://ia.media-imdb.com/images/M/MV5BNTAxMzY0MjI1Nl5BMl5BanBnXkFtZTgwMTU2NTYxMTE@._V1_SX300.jpg", 283 | "imdb": map[string]interface{}{ 284 | "rating": 7.6, 285 | "votes": 138, 286 | "id": "tt0097757", 287 | }, 288 | }, 289 | { 290 | "_id": "977654", 291 | "title": "Hijos de la guerra", 292 | "year": 2007, 293 | "rating": nil, 294 | "runtime": "90 min", 295 | "genre": []string{ 296 | "Documentary", 297 | }, 298 | "director": "Alexandre Fuchs, Samantha Belmont, Jeremy Fourteau", 299 | "writer": []string{ 300 | "Jeremy Fourteau (story)", 301 | "Jeff Zimbalist", 302 | "Michael Zimbalist", 303 | }, 304 | "cast": nil, 305 | "poster": "http://ia.media-imdb.com/images/M/MV5BMTIwMzUyMjcwN15BMl5BanBnXkFtZTcwNzIyMzU0MQ@@._V1_SX300.jpg", 306 | "imdb": map[string]interface{}{ 307 | "rating": 8.1, 308 | "votes": 80, 309 | "id": "tt0977654", 310 | }, 311 | }, 312 | } 313 | ) 314 | 315 | func TestMain(m *testing.M) { 316 | setup() 317 | code := m.Run() 318 | teardown() 319 | os.Exit(code) 320 | } 321 | 322 | func setup() { 323 | setupServer("http://localhost:5984", 1) 324 | 325 | testsDB = setupDB("golang-tests", testsDB, 2) 326 | 327 | movieDB = setupDB("golang-movies", movieDB, 3) 328 | _, err := movieDB.Update(movies, nil) 329 | if err != nil { 330 | os.Exit(4) 331 | } 332 | 333 | designDB = setupDB("golang-design", designDB, 5) 334 | 335 | iterDB = setupDB("golang-iter", iterDB, 6) 336 | iterDesignDoc := map[string]interface{}{ 337 | "_id": "_design/test", 338 | "views": map[string]interface{}{ 339 | "nums": map[string]string{"map": "function(doc) { emit(doc.num, null); }"}, 340 | "nulls": map[string]string{"map": "function(doc) { emit(null, null); }"}, 341 | }, 342 | } 343 | _, _, err = iterDB.Save(iterDesignDoc, nil) 344 | if err != nil { 345 | os.Exit(7) 346 | } 347 | numDocs := make([]map[string]interface{}, NumDocs) 348 | for num := 0; num < NumDocs; num++ { 349 | doc := docFromNum(num) 350 | numDocs[num] = doc 351 | } 352 | _, err = iterDB.Update(numDocs, nil) 353 | if err != nil { 354 | fmt.Println(err) 355 | os.Exit(8) 356 | } 357 | 358 | defnDB = setupDB("golang-defn", defnDB, 9) 359 | 360 | showListDB = setupDB("golang-showlist", showListDB, 10) 361 | // setups for golang-showlist 362 | showFunc := ` 363 | function(doc, req) { 364 | return {"body": req.id + ":" + (req.query.r || "")}; 365 | }` 366 | 367 | listFunc := ` 368 | function(head, req) { 369 | start({headers: {'Content-Type': 'text/csv'}}); 370 | if (req.query.include_header) { 371 | send('id' + '\\r\\n'); 372 | } 373 | var row; 374 | while (row = getRow()) { 375 | send(row.id + '\\r\\n'); 376 | } 377 | } 378 | ` 379 | 380 | showListDesignDoc := map[string]interface{}{ 381 | "_id": "_design/foo", 382 | "shows": map[string]interface{}{"bar": showFunc}, 383 | "views": map[string]interface{}{ 384 | "by_id": map[string]string{"map": "function(doc) { emit(doc._id, null); }"}, 385 | "by_name": map[string]string{"map": "function(doc) { emit(doc.name, null); }"}, 386 | }, 387 | "lists": map[string]string{"list": listFunc}, 388 | } 389 | showListDocs := []map[string]interface{}{ 390 | {"_id": "1", "name": "one"}, 391 | {"_id": "2", "name": "two"}, 392 | } 393 | _, _, err = showListDB.Save(showListDesignDoc, nil) 394 | if err != nil { 395 | os.Exit(11) 396 | } 397 | 398 | _, err = showListDB.Update(showListDocs, nil) 399 | if err != nil { 400 | os.Exit(12) 401 | } 402 | 403 | updateDB = setupDB("golang-update", updateDB, 13) 404 | // setups for golang-update 405 | updateFunc := ` 406 | function(doc, req) { 407 | if (!doc) { 408 | if (req.id) { 409 | return [{_id: req.id}, "new doc"]; 410 | } 411 | return [null, "empty doc"]; 412 | } 413 | doc.name = "hello"; 414 | return [doc, "hello doc"]; 415 | } 416 | ` 417 | updateDesignDoc := map[string]interface{}{ 418 | "_id": "_design/foo", 419 | "language": "javascript", 420 | "updates": map[string]string{ 421 | "bar": updateFunc, 422 | }, 423 | } 424 | _, _, err = updateDB.Save(updateDesignDoc, nil) 425 | if err != nil { 426 | os.Exit(14) 427 | } 428 | 429 | updateDocs := []map[string]interface{}{ 430 | { 431 | "_id": "existed", 432 | "name": "bar", 433 | }, 434 | } 435 | _, err = updateDB.Update(updateDocs, nil) 436 | if err != nil { 437 | os.Exit(15) 438 | } 439 | 440 | mappingDB = setupDB("golang-mapping", mappingDB, 16) 441 | // setups for golang-mapping 442 | viewDefs1, err := testItem.withIncludeDocs() 443 | if err != nil { 444 | os.Exit(17) 445 | } 446 | 447 | viewDefs2, err := testItem.withoutIncludeDocs() 448 | if err != nil { 449 | os.Exit(18) 450 | } 451 | 452 | _, err = SyncMany(mappingDB, []*ViewDefinition{viewDefs1, viewDefs2}, false, nil) 453 | if err != nil { 454 | os.Exit(19) 455 | } 456 | } 457 | 458 | func teardown() { 459 | server.Delete("golang-tests") 460 | server.Delete("golang-movies") 461 | server.Delete("golang-design") 462 | server.Delete("golang-iter") 463 | server.Delete("golang-defn") 464 | server.Delete("golang-showlist") 465 | server.Delete("golang-update") 466 | server.Delete("golang-mapping") 467 | } 468 | 469 | func setupServer(url string, exitCode int) { 470 | var err error 471 | server, err = NewServer(url) 472 | if err != nil { 473 | os.Exit(exitCode) 474 | } 475 | server.Version() 476 | } 477 | 478 | func setupDB(name string, db *Database, exitCode int) *Database { 479 | server.Delete(name) 480 | var err error 481 | db, err = server.Create(name) 482 | if err != nil { 483 | os.Exit(2) 484 | } 485 | return db 486 | } 487 | 488 | func docFromNum(num int) map[string]interface{} { 489 | return map[string]interface{}{ 490 | "_id": fmt.Sprintf("%d", num), 491 | "num": int(num / 2), 492 | } 493 | } 494 | 495 | func docFromRow(row Row) map[string]interface{} { 496 | return map[string]interface{}{ 497 | "_id": row.ID, 498 | "num": int(row.Key.(float64)), 499 | } 500 | } 501 | 502 | func TestNewServer(t *testing.T) { 503 | testServer, err := NewServer(DefaultBaseURL) 504 | if err != nil { 505 | t.Fatal(`new server error`, err) 506 | } 507 | _, err = testServer.Version() 508 | if err != nil { 509 | t.Error(`server version error`, err) 510 | } 511 | } 512 | 513 | func TestNewServerNoFullCommit(t *testing.T) { 514 | testServer, err := NewServerNoFullCommit(DefaultBaseURL) 515 | if err != nil { 516 | t.Fatal(`new server full commit error`, err) 517 | } 518 | _, err = testServer.Version() 519 | if err != nil { 520 | t.Error(`server version error`, err) 521 | } 522 | } 523 | 524 | func TestServerExists(t *testing.T) { 525 | testServer, err := NewServer("http://localhost:9999") 526 | if err != nil { 527 | t.Error(`new server error`, err) 528 | } 529 | _, err = testServer.Version() 530 | if err == nil { 531 | t.Error(`server version ok`) 532 | } 533 | } 534 | 535 | func TestServerConfig(t *testing.T) { 536 | version, err := server.Version() 537 | if err != nil { 538 | t.Error("server version error", err) 539 | } 540 | // CouchDB 2.0 feature 541 | if strings.HasPrefix(version, "2") { 542 | config, err := server.Config("couchdb@localhost") 543 | if err != nil { 544 | t.Error(`server config error`, err) 545 | } 546 | if reflect.ValueOf(config).Kind() != reflect.Map { 547 | t.Error(`config not of type map`) 548 | } 549 | } 550 | } 551 | 552 | func TestServerString(t *testing.T) { 553 | testServer, err := NewServer(DefaultBaseURL) 554 | if err != nil { 555 | t.Error(`new server error`, err) 556 | } 557 | if testServer.String() != "Server http://localhost:5984" { 558 | t.Error(`server name invalid want "Server http://localhost:5984"`) 559 | } 560 | } 561 | 562 | func TestServerVars(t *testing.T) { 563 | version, err := server.Version() 564 | if err != nil { 565 | t.Error(`server version error`, err) 566 | } 567 | if reflect.ValueOf(version).Kind() != reflect.String { 568 | t.Error(`version not of string type`) 569 | } 570 | 571 | tasks, _ := server.ActiveTasks() 572 | if reflect.ValueOf(tasks).Kind() != reflect.Slice { 573 | t.Error(`tasks not of slice type`) 574 | } 575 | } 576 | 577 | func TestServerStats(t *testing.T) { 578 | version, err := server.Version() 579 | if err != nil { 580 | t.Error("server version error", err) 581 | } 582 | // CouchDB 2.0 feature 583 | if strings.HasPrefix(version, "2") { 584 | stats, err := server.Stats("couchdb@localhost", "") 585 | if err != nil { 586 | t.Error(`server stats error`, err) 587 | } 588 | if reflect.ValueOf(stats).Kind() != reflect.Map { 589 | t.Error(`stats not of map type`) 590 | } 591 | stats, err = server.Stats("couchdb@localhost", "couchdb") 592 | if err != nil { 593 | t.Error(`server stats httpd/requests error`, err) 594 | } 595 | if reflect.ValueOf(stats).Kind() != reflect.Map { 596 | t.Error(`httpd/requests stats not of map type`) 597 | } 598 | } 599 | } 600 | 601 | func TestDBs(t *testing.T) { 602 | aName, bName := "dba", "dbb" 603 | server.Create(aName) 604 | defer server.Delete(aName) 605 | 606 | server.Create(bName) 607 | defer server.Delete(bName) 608 | 609 | dbs, err := server.DBs() 610 | if err != nil { 611 | t.Error(`server DBs error`, err) 612 | } 613 | var aExist, bExist bool 614 | for _, v := range dbs { 615 | if v == aName { 616 | aExist = true 617 | } else if v == bName { 618 | bExist = true 619 | } 620 | } 621 | 622 | if !aExist { 623 | t.Errorf("db %s not existed in dbs", aName) 624 | } 625 | 626 | if !bExist { 627 | t.Errorf("db %s not existed in dbs", bName) 628 | } 629 | } 630 | 631 | func TestLen(t *testing.T) { 632 | aName, bName := "dba", "dbb" 633 | server.Create(aName) 634 | defer server.Delete(aName) 635 | server.Create(bName) 636 | defer server.Delete(bName) 637 | 638 | len, err := server.Len() 639 | if err != nil { 640 | t.Error(`server len error`, err) 641 | } 642 | if len < 2 { 643 | t.Error("server len should be >= 2") 644 | } 645 | } 646 | 647 | func TestGetDBMissing(t *testing.T) { 648 | _, err := server.Get("golang-missing") 649 | if err != ErrNotFound { 650 | t.Errorf("err = %v want ErrNotFound", err) 651 | } 652 | } 653 | 654 | func TestGetDB(t *testing.T) { 655 | _, err := server.Get("golang-tests") 656 | if err != nil { 657 | t.Error(`get db error`, err) 658 | } 659 | } 660 | 661 | func TestCreateDBConflict(t *testing.T) { 662 | conflictDBName := "golang-conflict" 663 | _, err := server.Create(conflictDBName) 664 | if err != nil { 665 | t.Error(`server create error`, err) 666 | } 667 | // defer s.Delete(conflictDBName) 668 | if !server.Contains(conflictDBName) { 669 | t.Error(`server not contains`, conflictDBName) 670 | } 671 | if _, err = server.Create(conflictDBName); err != ErrPreconditionFailed { 672 | t.Errorf("err = %v want ErrPreconditionFailed", err) 673 | } 674 | server.Delete(conflictDBName) 675 | } 676 | 677 | func TestCreateDB(t *testing.T) { 678 | _, err := server.Create("golang-create") 679 | if err != nil { 680 | t.Error(`get db failed`) 681 | } 682 | server.Delete("golang-create") 683 | } 684 | 685 | func TestCreateDBIllegal(t *testing.T) { 686 | if _, err := server.Create("_db"); err == nil { 687 | t.Error(`create illegal _db ok`) 688 | } 689 | } 690 | 691 | func TestDeleteDB(t *testing.T) { 692 | dbName := "golang-delete" 693 | server.Create(dbName) 694 | if !server.Contains(dbName) { 695 | t.Error(`server not contains`, dbName) 696 | } 697 | server.Delete(dbName) 698 | if server.Contains(dbName) { 699 | t.Error(`server contains`, dbName) 700 | } 701 | } 702 | 703 | func TestDeleteDBMissing(t *testing.T) { 704 | dbName := "golang-missing" 705 | err := server.Delete(dbName) 706 | if err != ErrNotFound { 707 | t.Errorf("err = %v want ErrNotFound", err) 708 | } 709 | } 710 | 711 | func TestReplicate(t *testing.T) { 712 | aName := "dba" 713 | dba, _ := server.Create(aName) 714 | defer server.Delete(aName) 715 | 716 | bName := "dbb" 717 | dbb, _ := server.Create(bName) 718 | defer server.Delete(bName) 719 | 720 | id, _, err := dba.Save(map[string]interface{}{"test": "a"}, nil) 721 | if err != nil { 722 | t.Error(`dba save error`, err) 723 | } 724 | result, _ := server.Replicate(aName, bName, nil) 725 | if v, ok := result["ok"]; !(ok && v.(bool)) { 726 | t.Error(`result should be ok`) 727 | } 728 | doc, err := dbb.Get(id, nil) 729 | if err != nil { 730 | t.Errorf("db %s get doc %s error %v", bName, id, err) 731 | } 732 | if v, ok := doc["test"]; ok { 733 | if "a" != v.(string) { 734 | t.Error(`doc[test] should be a, found`, v.(string)) 735 | } 736 | } 737 | 738 | doc["test"] = "b" 739 | dbb.Update([]map[string]interface{}{doc}, nil) 740 | result, err = server.Replicate(bName, aName, nil) 741 | if err != nil { 742 | t.Error(`server replicate error`, err) 743 | } 744 | if reflect.ValueOf(result).Kind() != reflect.Map { 745 | t.Error(`server replicate return non-map result`) 746 | } 747 | 748 | docA, err := dba.Get(id, nil) 749 | if err != nil { 750 | t.Errorf("db %s get doc %s error %v", aName, id, err) 751 | } 752 | if v, ok := docA["test"]; ok { 753 | if "b" != v.(string) { 754 | t.Error(`docA[test] should be b, found`, v.(string)) 755 | } 756 | } 757 | 758 | docB, err := dbb.Get(id, nil) 759 | if err != nil { 760 | t.Errorf("db %s get doc %s error %v", bName, id, err) 761 | } 762 | if v, ok := docB["test"]; ok { 763 | if "b" != v.(string) { 764 | t.Error(`docB[test] should be b, found`, v.(string)) 765 | } 766 | } 767 | } 768 | 769 | func TestReplicateContinuous(t *testing.T) { 770 | aName, bName := "dba", "dbb" 771 | server.Create(aName) 772 | defer server.Delete(aName) 773 | 774 | server.Create(bName) 775 | defer server.Delete(bName) 776 | 777 | result, err := server.Replicate(aName, bName, map[string]interface{}{"continuous": true}) 778 | if err != nil { 779 | t.Error(`server replicate error`, err) 780 | } 781 | 782 | if reflect.ValueOf(result).Kind() != reflect.Map { 783 | t.Error(`server replicate return non-map result`) 784 | } 785 | 786 | if v, ok := result["ok"]; !(ok && v.(bool)) { 787 | t.Error(`result should be ok`) 788 | } 789 | } 790 | 791 | func TestMembership(t *testing.T) { 792 | version, err := server.Version() 793 | if err != nil { 794 | t.Error("server version error", err) 795 | } 796 | // CouchDB 2.0 feature 797 | if strings.HasPrefix(version, "2") { 798 | allNodes, clusterNodes, err := server.Membership() 799 | if err != nil { 800 | t.Error(`server membership error`, err) 801 | } 802 | 803 | kind := reflect.ValueOf(allNodes).Kind() 804 | elemKind := reflect.TypeOf(allNodes).Elem().Kind() 805 | 806 | if kind != reflect.Slice || elemKind != reflect.String { 807 | t.Error(`clusterNodes should be slice of string`) 808 | } 809 | 810 | kind = reflect.ValueOf(clusterNodes).Kind() 811 | elemKind = reflect.TypeOf(clusterNodes).Elem().Kind() 812 | 813 | if kind != reflect.Slice || elemKind != reflect.String { 814 | t.Error(`allNodes should be slice of string`) 815 | } 816 | } 817 | } 818 | 819 | func TestUUIDs(t *testing.T) { 820 | uuids, err := server.UUIDs(10) 821 | if err != nil { 822 | t.Error(`server uuids error`, err) 823 | } 824 | if reflect.ValueOf(uuids).Kind() != reflect.Slice { 825 | t.Error(`server uuids should be of type slice`) 826 | } 827 | if len(uuids) != 10 { 828 | t.Error(`server uuids should be of length 10, not`, len(uuids)) 829 | } 830 | } 831 | 832 | func TestBasicAuth(t *testing.T) { 833 | testServer, _ := NewServer("http://root:password@localhost:5984/") 834 | _, err := testServer.Create("golang-auth") 835 | if err != ErrUnauthorized { 836 | t.Errorf("err = %v want ErrUnauthorized", err) 837 | } 838 | } 839 | 840 | func TestUserManagement(t *testing.T) { 841 | user := "foo" 842 | password := "secret" 843 | roles := []string{"hero"} 844 | 845 | if !server.Contains("_users") { 846 | _, err := server.Create("_users") 847 | if err != nil { 848 | t.Error("create db _users error", err) 849 | } 850 | } 851 | 852 | _, _, err := server.AddUser(user, password, roles) 853 | if err != nil { 854 | t.Errorf("add user %s password %s roles %v error %v", user, password, roles, err) 855 | } 856 | 857 | token, err := server.Login(user, password) 858 | if err != nil { 859 | t.Errorf("login user %s password %s roles %v error %s", user, password, roles, err) 860 | } 861 | 862 | if err = server.VerifyToken(token); err != nil { 863 | t.Error("verify token error", err) 864 | } 865 | 866 | if err = server.Logout(token); err != nil { 867 | t.Error("logout error", err) 868 | } 869 | 870 | if err = server.RemoveUser("foo"); err != nil { 871 | t.Error("server remove user error", err) 872 | } 873 | } 874 | -------------------------------------------------------------------------------- /tools/replicate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "net/url" 9 | "os" 10 | "path" 11 | "strings" 12 | "time" 13 | 14 | "github.com/leesper/couchdb-golang" 15 | ) 16 | 17 | type tuple struct { 18 | left, right string 19 | } 20 | 21 | func init() { 22 | log.SetFlags(log.Lshortfile) 23 | } 24 | 25 | func findPath(s string) (string, string) { 26 | if s == "." { 27 | return couchdb.DefaultBaseURL, "" 28 | } 29 | 30 | if !strings.HasPrefix(s, "http") { 31 | return couchdb.DefaultBaseURL, s 32 | } 33 | 34 | u, err := url.Parse(s) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | 39 | var base string 40 | if u.User != nil { 41 | base = fmt.Sprintf("%s://%s@%s", u.Scheme, u.User.String(), u.Host) 42 | } else { 43 | base = fmt.Sprintf("%s://%s", u.Scheme, u.Host) 44 | } 45 | res, err := couchdb.NewResource(base, nil) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | _, data, err := res.GetJSON("", nil, nil) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | info := map[string]interface{}{} 55 | err = json.Unmarshal(data, &info) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | 60 | if _, ok := info["couchdb"]; !ok { 61 | log.Fatal(fmt.Sprintf("%s does not appear to be a CouchDB", s)) 62 | } 63 | 64 | return base, strings.TrimLeft(u.Path, "/") 65 | } 66 | 67 | func main() { 68 | isContinous := flag.Bool("continous", false, "trigger continous replication in couchdb") 69 | isCompact := flag.Bool("compact", false, "compact target database after replication") 70 | flag.Usage = func() { 71 | fmt.Fprintf(os.Stderr, "usage: %s [options] \n", os.Args[0]) 72 | flag.PrintDefaults() 73 | } 74 | flag.Parse() 75 | if len(os.Args) != 3 { 76 | log.Fatal("need source and target arguments") 77 | } 78 | 79 | src, tgt := os.Args[1], os.Args[2] 80 | 81 | sbase, spath := findPath(src) 82 | source, err := couchdb.NewServer(sbase) 83 | if err != nil { 84 | log.Fatal(err) 85 | } 86 | 87 | tbase, tpath := findPath(tgt) 88 | target, err := couchdb.NewServer(tbase) 89 | if err != nil { 90 | log.Fatal(err) 91 | } 92 | 93 | // check dtabase name specs 94 | if strings.Contains(tpath, "*") { 95 | log.Fatal("invalid target path: must be single db or empty") 96 | } 97 | 98 | all, err := source.DBs() 99 | if err != nil { 100 | log.Fatal(err) 101 | } 102 | 103 | if spath == "" { 104 | log.Fatal("source database must be specified") 105 | } 106 | 107 | sources := []string{} 108 | for _, db := range all { 109 | if !strings.HasPrefix(db, "_") { // skip reserved names 110 | var ok bool 111 | ok, err = path.Match(spath, db) 112 | if err != nil { 113 | log.Fatal(err) 114 | } 115 | if ok { 116 | sources = append(sources, db) 117 | } 118 | } 119 | } 120 | 121 | if len(sources) == 0 { 122 | log.Fatalf("no source databases match glob %s", spath) 123 | } 124 | 125 | databases := []tuple{} 126 | if len(sources) > 1 && tpath != "" { 127 | log.Fatal("target path must be empty with multiple sources") 128 | } else if len(sources) == 1 { 129 | databases = append(databases, tuple{sources[0], tpath}) 130 | } else { 131 | for _, source := range sources { 132 | databases = append(databases, tuple{source, source}) 133 | } 134 | } 135 | 136 | // actual replication 137 | for _, t := range databases { 138 | sdb, tdb := t.left, t.right 139 | start := time.Now() 140 | fmt.Printf("%s -> %s\n", sdb, tdb) 141 | 142 | if !target.Contains(tdb) { 143 | _, err = target.Create(tdb) 144 | if err != nil { 145 | log.Println(err) 146 | continue 147 | } 148 | } 149 | 150 | sdb = fmt.Sprintf("%s/%s", sbase, sdb) 151 | var opts map[string]interface{} 152 | if *isContinous { 153 | opts = map[string]interface{}{"continuous": true} 154 | } 155 | _, err = target.Replicate(sdb, tdb, opts) 156 | if err != nil { 157 | log.Println(err) 158 | } 159 | fmt.Printf("%.1fs\n", time.Now().Sub(start).Seconds()) 160 | } 161 | 162 | if *isCompact { 163 | for _, t := range databases { 164 | tdb := t.right 165 | fmt.Println("compact", tdb) 166 | targetDB, _ := target.Get(tdb) 167 | targetDB.Compact() 168 | } 169 | } 170 | } 171 | --------------------------------------------------------------------------------