1507 lines
41 KiB
Go
1507 lines
41 KiB
Go
package couchdb
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"math"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// DefaultBaseURL is the default address of CouchDB server.
|
|
DefaultBaseURL = "http://localhost:5984"
|
|
)
|
|
|
|
var (
|
|
// ErrBatchValue for invalid batch parameter of IterView
|
|
ErrBatchValue = errors.New("batch must be 1 or more")
|
|
// ErrLimitValue for invalid limit parameter of IterView
|
|
ErrLimitValue = errors.New("limit must be 1 or more")
|
|
)
|
|
|
|
// getDefaultCouchDBURL returns the default CouchDB server url.
|
|
func getDefaultCouchDBURL() string {
|
|
var couchdbURLEnviron string
|
|
for _, couchdbURLEnviron = range os.Environ() {
|
|
if strings.HasPrefix(couchdbURLEnviron, "COUCHDB_URL") {
|
|
break
|
|
}
|
|
}
|
|
if len(couchdbURLEnviron) == 0 {
|
|
couchdbURLEnviron = DefaultBaseURL
|
|
} else {
|
|
couchdbURLEnviron = strings.Split(couchdbURLEnviron, "=")[1]
|
|
}
|
|
return couchdbURLEnviron
|
|
}
|
|
|
|
// Database represents a CouchDB database instance.
|
|
type Database struct {
|
|
resource *Resource
|
|
}
|
|
|
|
// NewDatabase returns a CouchDB database instance.
|
|
func NewDatabase(urlStr string) (*Database, error) {
|
|
var dbURLStr string
|
|
if !strings.HasPrefix(urlStr, "http") {
|
|
base, err := url.Parse(getDefaultCouchDBURL())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dbURL, err := base.Parse(urlStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dbURLStr = dbURL.String()
|
|
} else {
|
|
dbURLStr = urlStr
|
|
}
|
|
|
|
res, err := NewResource(dbURLStr, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newDatabase(res)
|
|
}
|
|
|
|
// NewDatabaseWithResource returns a CouchDB database instance with resource obj.
|
|
func NewDatabaseWithResource(res *Resource) (*Database, error) {
|
|
return newDatabase(res)
|
|
}
|
|
|
|
func newDatabase(res *Resource) (*Database, error) {
|
|
return &Database{
|
|
resource: res,
|
|
}, nil
|
|
}
|
|
|
|
// Available returns error if the database is not good to go.
|
|
func (d *Database) Available() error {
|
|
_, _, err := d.resource.Head("", nil, nil)
|
|
return err
|
|
}
|
|
|
|
// Save creates a new document or update an existing document.
|
|
// If doc has no _id the server will generate a random UUID and a new document will be created.
|
|
// Otherwise the doc's _id will be used to identify the document to create or update.
|
|
// Trying to update an existing document with an incorrect _rev will cause failure.
|
|
// *NOTE* It is recommended to avoid saving doc without _id and instead generate document ID on client side.
|
|
// To avoid such problems you can generate a UUID on the client side.
|
|
// GenerateUUID provides a simple, platform-independent implementation.
|
|
// You can also use other third-party packages instead.
|
|
// doc: the document to create or update.
|
|
func (d *Database) Save(doc map[string]interface{}, options url.Values) (string, string, error) {
|
|
var id, rev string
|
|
|
|
var httpFunc func(string, http.Header, map[string]interface{}, url.Values) (http.Header, []byte, error)
|
|
if v, ok := doc["_id"]; ok {
|
|
httpFunc = docResource(d.resource, v.(string)).PutJSON
|
|
} else {
|
|
httpFunc = d.resource.PostJSON
|
|
}
|
|
|
|
_, data, err := httpFunc("", nil, doc, options)
|
|
if err != nil {
|
|
return id, rev, err
|
|
}
|
|
|
|
var jsonMap map[string]interface{}
|
|
jsonMap, err = parseData(data)
|
|
if err != nil {
|
|
return id, rev, err
|
|
}
|
|
|
|
if v, ok := jsonMap["id"]; ok {
|
|
id = v.(string)
|
|
doc["_id"] = id
|
|
}
|
|
|
|
if v, ok := jsonMap["rev"]; ok {
|
|
rev = v.(string)
|
|
doc["_rev"] = rev
|
|
}
|
|
|
|
return id, rev, nil
|
|
}
|
|
|
|
// Get returns the document with the specified ID.
|
|
func (d *Database) Get(docid string, options url.Values) (map[string]interface{}, error) {
|
|
docRes := docResource(d.resource, docid)
|
|
_, data, err := docRes.GetJSON("", nil, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var doc map[string]interface{}
|
|
doc, err = parseData(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return doc, nil
|
|
}
|
|
|
|
// Delete deletes the document with the specified ID.
|
|
func (d *Database) Delete(docid string) error {
|
|
docRes := docResource(d.resource, docid)
|
|
header, _, err := docRes.Head("", nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rev := strings.Trim(header.Get("ETag"), `"`)
|
|
return deleteDoc(docRes, rev)
|
|
}
|
|
|
|
// DeleteDoc deletes the specified document
|
|
func (d *Database) DeleteDoc(doc map[string]interface{}) error {
|
|
id, ok := doc["_id"]
|
|
if !ok || id == nil {
|
|
return errors.New("document ID not existed")
|
|
}
|
|
|
|
rev, ok := doc["_rev"]
|
|
if !ok || rev == nil {
|
|
return errors.New("document rev not existed")
|
|
}
|
|
|
|
docRes := docResource(d.resource, id.(string))
|
|
return deleteDoc(docRes, rev.(string))
|
|
}
|
|
|
|
func deleteDoc(docRes *Resource, rev string) error {
|
|
_, _, err := docRes.DeleteJSON("", nil, url.Values{"rev": []string{rev}})
|
|
return err
|
|
}
|
|
|
|
// Set creates or updates a document with the specified ID.
|
|
func (d *Database) Set(docid string, doc map[string]interface{}) error {
|
|
docRes := docResource(d.resource, docid)
|
|
_, data, err := docRes.PutJSON("", nil, doc, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
result, err := parseData(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
doc["_id"] = result["id"].(string)
|
|
doc["_rev"] = result["rev"].(string)
|
|
return nil
|
|
}
|
|
|
|
// Contains returns true if the database contains a document with the specified ID.
|
|
func (d *Database) Contains(docid string) error {
|
|
docRes := docResource(d.resource, docid)
|
|
_, _, err := docRes.Head("", nil, nil)
|
|
return err
|
|
}
|
|
|
|
// UpdateResult represents result of an update.
|
|
type UpdateResult struct {
|
|
ID, Rev string
|
|
Err error
|
|
}
|
|
|
|
// Update performs a bulk update or creation of the given documents in a single HTTP request.
|
|
// It returns a 3-tuple (id, rev, error)
|
|
func (d *Database) Update(docs []map[string]interface{}, options map[string]interface{}) ([]UpdateResult, error) {
|
|
results := make([]UpdateResult, len(docs))
|
|
body := map[string]interface{}{}
|
|
if options != nil {
|
|
for k, v := range options {
|
|
body[k] = v
|
|
}
|
|
}
|
|
body["docs"] = docs
|
|
|
|
_, data, err := d.resource.PostJSON("_bulk_docs", nil, body, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var jsonArr []map[string]interface{}
|
|
err = json.Unmarshal(data, &jsonArr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i, v := range jsonArr {
|
|
var retErr error
|
|
var result UpdateResult
|
|
if val, ok := v["error"]; ok {
|
|
errMsg := val.(string)
|
|
switch errMsg {
|
|
case "conflict":
|
|
retErr = ErrConflict
|
|
case "forbidden":
|
|
retErr = ErrForbidden
|
|
default:
|
|
retErr = ErrInternalServerError
|
|
}
|
|
result = UpdateResult{
|
|
ID: v["id"].(string),
|
|
Rev: "",
|
|
Err: retErr,
|
|
}
|
|
} else {
|
|
id, rev := v["id"].(string), v["rev"].(string)
|
|
result = UpdateResult{
|
|
ID: id,
|
|
Rev: rev,
|
|
Err: retErr,
|
|
}
|
|
doc := docs[i]
|
|
doc["_id"] = id
|
|
doc["_rev"] = rev
|
|
}
|
|
results[i] = result
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// DocIDs returns the IDs of all documents in database.
|
|
func (d *Database) DocIDs() ([]string, error) {
|
|
docRes := docResource(d.resource, "_all_docs")
|
|
_, data, err := docRes.GetJSON("", nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var jsonMap map[string]*json.RawMessage
|
|
err = json.Unmarshal(data, &jsonMap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var jsonArr []*json.RawMessage
|
|
json.Unmarshal(*jsonMap["rows"], &jsonArr)
|
|
ids := make([]string, len(jsonArr))
|
|
for i, v := range jsonArr {
|
|
var row map[string]interface{}
|
|
err = json.Unmarshal(*v, &row)
|
|
if err != nil {
|
|
return ids, err
|
|
}
|
|
ids[i] = row["id"].(string)
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
// Name returns the name of database.
|
|
func (d *Database) Name() (string, error) {
|
|
var name string
|
|
info, err := d.Info("")
|
|
if err != nil {
|
|
return name, err
|
|
}
|
|
return info["db_name"].(string), nil
|
|
}
|
|
|
|
// Info returns the information about the database or design document
|
|
func (d *Database) Info(ddoc string) (map[string]interface{}, error) {
|
|
var data []byte
|
|
var err error
|
|
if ddoc == "" {
|
|
_, data, err = d.resource.GetJSON("", nil, url.Values{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
_, data, err = d.resource.GetJSON(fmt.Sprintf("_design/%s/_info", ddoc), nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
var info map[string]interface{}
|
|
err = json.Unmarshal(data, &info)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
func (d *Database) String() string {
|
|
return fmt.Sprintf("Database %s", d.resource.base)
|
|
}
|
|
|
|
// Commit flushes any recent changes to the specified database to disk.
|
|
// If the server is configured to delay commits or previous requests use the special
|
|
// "X-Couch-Full-Commit: false" header to disable immediate commits, this method
|
|
// can be used to ensure that non-commited changes are commited to physical storage.
|
|
func (d *Database) Commit() error {
|
|
_, _, err := d.resource.PostJSON("_ensure_full_commit", nil, nil, nil)
|
|
return err
|
|
}
|
|
|
|
// Compact compacts the database by compressing the disk database file.
|
|
func (d *Database) Compact() error {
|
|
_, _, err := d.resource.PostJSON("_compact", nil, nil, nil)
|
|
return err
|
|
}
|
|
|
|
// Revisions returns all available revisions of the given document in reverse
|
|
// order, e.g. latest first.
|
|
func (d *Database) Revisions(docid string, options url.Values) ([]map[string]interface{}, error) {
|
|
docRes := docResource(d.resource, docid)
|
|
_, data, err := docRes.GetJSON("", nil, url.Values{"revs": []string{"true"}})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var jsonMap map[string]*json.RawMessage
|
|
err = json.Unmarshal(data, &jsonMap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var revsMap map[string]interface{}
|
|
err = json.Unmarshal(*jsonMap["_revisions"], &revsMap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
startRev := int(revsMap["start"].(float64))
|
|
val := reflect.ValueOf(revsMap["ids"])
|
|
if options == nil {
|
|
options = url.Values{}
|
|
}
|
|
docs := make([]map[string]interface{}, val.Len())
|
|
for i := 0; i < val.Len(); i++ {
|
|
rev := fmt.Sprintf("%d-%s", startRev-i, val.Index(i).Interface().(string))
|
|
options.Set("rev", rev)
|
|
doc, err := d.Get(docid, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
docs[i] = doc
|
|
}
|
|
return docs, nil
|
|
}
|
|
|
|
// GetAttachment returns the file attachment associated with the document.
|
|
// The raw data is returned as a []byte.
|
|
func (d *Database) GetAttachment(doc map[string]interface{}, name string) ([]byte, error) {
|
|
docid, ok := doc["_id"]
|
|
if !ok {
|
|
return nil, errors.New("doc _id not existed")
|
|
}
|
|
return d.getAttachment(docid.(string), name)
|
|
}
|
|
|
|
// GetAttachmentID returns the file attachment associated with the document ID.
|
|
// The raw data is returned as []byte.
|
|
func (d *Database) GetAttachmentID(docid, name string) ([]byte, error) {
|
|
return d.getAttachment(docid, name)
|
|
}
|
|
|
|
func (d *Database) getAttachment(docid, name string) ([]byte, error) {
|
|
docRes := docResource(docResource(d.resource, docid), name)
|
|
_, data, err := docRes.Get("", nil, nil)
|
|
return data, err
|
|
}
|
|
|
|
// PutAttachment uploads the supplied []byte as an attachment to the specified document.
|
|
// doc: the document that the attachment belongs to. Must have _id and _rev inside.
|
|
// content: the data to be attached to doc.
|
|
// name: name of attachment.
|
|
// mimeType: MIME type of content.
|
|
func (d *Database) PutAttachment(doc map[string]interface{}, content []byte, name, mimeType string) error {
|
|
if id, ok := doc["_id"]; !ok || id.(string) == "" {
|
|
return errors.New("doc _id not existed")
|
|
}
|
|
if rev, ok := doc["_rev"]; !ok || rev.(string) == "" {
|
|
return errors.New("doc _rev not extisted")
|
|
}
|
|
|
|
id, rev := doc["_id"].(string), doc["_rev"].(string)
|
|
|
|
docRes := docResource(docResource(d.resource, id), name)
|
|
header := http.Header{}
|
|
header.Set("Content-Type", mimeType)
|
|
params := url.Values{}
|
|
params.Set("rev", rev)
|
|
|
|
_, data, err := docRes.Put("", header, content, params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
result, err := parseData(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
doc["_rev"] = result["rev"].(string)
|
|
return nil
|
|
}
|
|
|
|
// DeleteAttachment deletes the specified attachment
|
|
func (d *Database) DeleteAttachment(doc map[string]interface{}, name string) error {
|
|
if id, ok := doc["_id"]; !ok || id.(string) == "" {
|
|
return errors.New("doc _id not existed")
|
|
}
|
|
if rev, ok := doc["_rev"]; !ok || rev.(string) == "" {
|
|
return errors.New("doc _rev not extisted")
|
|
}
|
|
|
|
id, rev := doc["_id"].(string), doc["_rev"].(string)
|
|
|
|
params := url.Values{}
|
|
params.Set("rev", rev)
|
|
docRes := docResource(docResource(d.resource, id), name)
|
|
_, data, err := docRes.DeleteJSON("", nil, params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
result, err := parseData(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
doc["_rev"] = result["rev"]
|
|
|
|
return nil
|
|
}
|
|
|
|
// Copy copies an existing document to a new or existing document.
|
|
func (d *Database) Copy(srcID, destID, destRev string) (string, error) {
|
|
docRes := docResource(d.resource, srcID)
|
|
var destination string
|
|
if destRev != "" {
|
|
destination = fmt.Sprintf("%s?rev=%s", destID, destRev)
|
|
} else {
|
|
destination = destID
|
|
}
|
|
header := http.Header{
|
|
"Destination": []string{destination},
|
|
}
|
|
_, data, err := request("COPY", docRes.base, header, nil, nil)
|
|
var rev string
|
|
if err != nil {
|
|
return rev, err
|
|
}
|
|
result, err := parseData(data)
|
|
if err != nil {
|
|
return rev, err
|
|
}
|
|
rev = result["rev"].(string)
|
|
return rev, nil
|
|
}
|
|
|
|
// Changes returns a sorted list of changes feed made to documents in the database.
|
|
func (d *Database) Changes(options url.Values) (map[string]interface{}, error) {
|
|
_, data, err := d.resource.GetJSON("_changes", nil, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result, err := parseData(data)
|
|
return result, err
|
|
}
|
|
|
|
// Purge performs complete removing of the given documents.
|
|
func (d *Database) Purge(docs []map[string]interface{}) (map[string]interface{}, error) {
|
|
revs := map[string][]string{}
|
|
for _, doc := range docs {
|
|
id, rev := doc["_id"].(string), doc["_rev"].(string)
|
|
if _, ok := revs[id]; !ok {
|
|
revs[id] = []string{}
|
|
}
|
|
revs[id] = append(revs[id], rev)
|
|
}
|
|
|
|
body := map[string]interface{}{}
|
|
for k, v := range revs {
|
|
body[k] = v
|
|
}
|
|
_, data, err := d.resource.PostJSON("_purge", nil, body, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return parseData(data)
|
|
}
|
|
|
|
func parseData(data []byte) (map[string]interface{}, error) {
|
|
result := map[string]interface{}{}
|
|
err := json.Unmarshal(data, &result)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
if _, ok := result["error"]; ok {
|
|
reason := result["reason"].(string)
|
|
return result, errors.New(reason)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func parseRaw(data []byte) (map[string]*json.RawMessage, error) {
|
|
result := map[string]*json.RawMessage{}
|
|
err := json.Unmarshal(data, &result)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
if _, ok := result["error"]; ok {
|
|
var reason string
|
|
json.Unmarshal(*result["reason"], &reason)
|
|
return result, errors.New(reason)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// GenerateUUID returns a random 128-bit UUID
|
|
func GenerateUUID() string {
|
|
b := make([]byte, 16)
|
|
_, err := rand.Read(b)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
uuid := fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
|
|
return uuid
|
|
}
|
|
|
|
// SetSecurity sets the security object for the given database.
|
|
func (d *Database) SetSecurity(securityDoc map[string]interface{}) error {
|
|
_, _, err := d.resource.PutJSON("_security", nil, securityDoc, nil)
|
|
return err
|
|
}
|
|
|
|
// GetSecurity returns the current security object from the given database.
|
|
func (d *Database) GetSecurity() (map[string]interface{}, error) {
|
|
_, data, err := d.resource.GetJSON("_security", nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parseData(data)
|
|
}
|
|
|
|
// Len returns the number of documents stored in it.
|
|
func (d *Database) Len() (int, error) {
|
|
info, err := d.Info("")
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return int(info["doc_count"].(float64)), nil
|
|
}
|
|
|
|
// GetRevsLimit gets the current revs_limit(revision limit) setting.
|
|
func (d *Database) GetRevsLimit() (int, error) {
|
|
_, data, err := d.resource.Get("_revs_limit", nil, nil)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
limit, err := strconv.Atoi(strings.Trim(string(data), "\n"))
|
|
if err != nil {
|
|
return limit, err
|
|
}
|
|
return limit, nil
|
|
}
|
|
|
|
// SetRevsLimit sets the maximum number of document revisions that will be
|
|
// tracked by CouchDB.
|
|
func (d *Database) SetRevsLimit(limit int) error {
|
|
_, _, err := d.resource.Put("_revs_limit", nil, []byte(strconv.Itoa(limit)), nil)
|
|
return err
|
|
}
|
|
|
|
// docResource returns a Resource instance for docID
|
|
func docResource(res *Resource, docID string) *Resource {
|
|
if len(docID) == 0 {
|
|
return res
|
|
}
|
|
|
|
docRes := res
|
|
if docID[:1] == "_" {
|
|
paths := strings.SplitN(docID, "/", 2)
|
|
for _, p := range paths {
|
|
docRes, _ = docRes.NewResourceWithURL(p)
|
|
}
|
|
return docRes
|
|
}
|
|
|
|
docRes, _ = res.NewResourceWithURL(url.QueryEscape(docID))
|
|
return docRes
|
|
}
|
|
|
|
// Cleanup removes all view index files no longer required by CouchDB.
|
|
func (d *Database) Cleanup() error {
|
|
_, _, err := d.resource.PostJSON("_view_cleanup", nil, nil, nil)
|
|
return err
|
|
}
|
|
|
|
// Query returns documents using a conditional selector statement in Golang.
|
|
//
|
|
// selector: A filter string declaring which documents to return, formatted as a Golang statement.
|
|
//
|
|
// fields: Specifying which fields to be returned, if passing nil the entire
|
|
// is returned, no automatic inclusion of _id or other metadata fields.
|
|
//
|
|
// 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.
|
|
//
|
|
// limit: Maximum number of results returned, passing nil to use default value(25).
|
|
//
|
|
// skip: Skip the first 'n' results, where 'n' is the number specified, passing nil for no-skip.
|
|
//
|
|
// index: Instruct a query to use a specific index, specified either as "<design_document>" or
|
|
// ["<design_document>", "<index_name>"], passing nil to use primary index(_all_docs) by default.
|
|
//
|
|
// Inner functions for selector syntax
|
|
//
|
|
// nor(condexprs...) matches if none of the conditions in condexprs match($nor).
|
|
//
|
|
// For example: nor(year == 1990, year == 1989, year == 1997) returns all documents
|
|
// whose year field not in 1989, 1990 and 1997.
|
|
//
|
|
// all(field, array) matches an array value if it contains all the elements of the argument array($all).
|
|
//
|
|
// For example: all(genre, []string{"Comedy", "Short"} returns all documents whose
|
|
// genre field contains "Comedy" and "Short".
|
|
//
|
|
// any(field, condexpr) matches an array field with at least one element meets the specified condition($elemMatch).
|
|
//
|
|
// For example: any(genre, genre == "Short" || genre == "Horror") returns all documents whose
|
|
// genre field contains "Short" or "Horror" or both.
|
|
//
|
|
// exists(field, boolean) checks whether the field exists or not, regardless of its value($exists).
|
|
//
|
|
// For example: exists(director, false) returns all documents who does not have a director field.
|
|
//
|
|
// typeof(field, type) checks the document field's type, valid types are
|
|
// "null", "boolean", "number", "string", "array", "object"($type).
|
|
//
|
|
// For example: typeof(genre, "array") returns all documents whose genre field is of array type.
|
|
//
|
|
// in(field, array) the field must exist in the array provided($in).
|
|
//
|
|
// For example: in(director, []string{"Mike Portnoy", "Vitali Kanevsky"}) returns all documents
|
|
// whose director field is "Mike Portnoy" or "Vitali Kanevsky".
|
|
//
|
|
// nin(field, array) the document field must not exist in the array provided($nin).
|
|
//
|
|
// For example: nin(year, []int{1990, 1992, 1998}) returns all documents whose year field is not
|
|
// in 1990, 1992 or 1998.
|
|
//
|
|
// size(field, int) matches the length of an array field in a document($size).
|
|
//
|
|
// For example: size(genre, 2) returns all documents whose genre field is of length 2.
|
|
//
|
|
// mod(field, divisor, remainder) matches documents where field % divisor == remainder($mod).
|
|
//
|
|
// For example: mod(year, 2, 1) returns all documents whose year field is an odd number.
|
|
//
|
|
// regex(field, regexstr) a regular expression pattern to match against the document field.
|
|
//
|
|
// For example: regex(title, "^A") returns all documents whose title is begin with an "A".
|
|
//
|
|
// Inner functions for sort syntax
|
|
//
|
|
// asc(field) sorts the field in ascending order, this is the default option while
|
|
// desc(field) sorts the field in descending order.
|
|
func (d *Database) Query(fields []string, selector string, sorts []string, limit, skip, index interface{}) ([]map[string]interface{}, error) {
|
|
selectorJSON, err := parseSelectorSyntax(selector)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
find := map[string]interface{}{
|
|
"selector": selectorJSON,
|
|
}
|
|
|
|
if limitVal, ok := limit.(int); ok {
|
|
find["limit"] = limitVal
|
|
}
|
|
|
|
if skipVal, ok := skip.(int); ok {
|
|
find["skip"] = skipVal
|
|
}
|
|
|
|
if sorts != nil {
|
|
sortsJSON, err := parseSortSyntax(sorts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
find["sort"] = sortsJSON
|
|
}
|
|
|
|
if fields != nil {
|
|
find["fields"] = fields
|
|
}
|
|
|
|
if index != nil {
|
|
find["use_index"] = index
|
|
}
|
|
|
|
return d.queryJSON(find)
|
|
}
|
|
|
|
// QueryJSON returns documents using a declarative JSON querying syntax.
|
|
func (d *Database) QueryJSON(query string) ([]map[string]interface{}, error) {
|
|
queryMap := map[string]interface{}{}
|
|
err := json.Unmarshal([]byte(query), &queryMap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return d.queryJSON(queryMap)
|
|
}
|
|
|
|
func (d *Database) queryJSON(queryMap map[string]interface{}) ([]map[string]interface{}, error) {
|
|
_, data, err := d.resource.PostJSON("_find", nil, queryMap, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := parseRaw(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
docs := []map[string]interface{}{}
|
|
err = json.Unmarshal(*result["docs"], &docs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return docs, nil
|
|
}
|
|
|
|
// parseSelectorSyntax returns a map representing the selector JSON struct.
|
|
func parseSelectorSyntax(selector string) (interface{}, error) {
|
|
// protect selector against query selector injection attacks
|
|
if strings.Contains(selector, "$") {
|
|
return nil, fmt.Errorf("no $s are allowed in selector: %s", selector)
|
|
}
|
|
|
|
// parse selector into abstract syntax tree (ast)
|
|
expr, err := parser.ParseExpr(selector)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// recursively processing ast into json object
|
|
selectObj, err := parseAST(expr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return selectObj, nil
|
|
}
|
|
|
|
// parseSortSyntax returns a slice of sort JSON struct.
|
|
func parseSortSyntax(sorts []string) (interface{}, error) {
|
|
if sorts == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
sortObjs := []interface{}{}
|
|
for _, sort := range sorts {
|
|
sortExpr, err := parser.ParseExpr(sort)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sortObj, err := parseAST(sortExpr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sortObjs = append(sortObjs, sortObj)
|
|
}
|
|
|
|
return sortObjs, nil
|
|
}
|
|
|
|
// parseAST converts and returns a JSON struct according to
|
|
// CouchDB mango query syntax for the abstract syntax tree represented by expr.
|
|
func parseAST(expr ast.Expr) (interface{}, error) {
|
|
switch expr := expr.(type) {
|
|
case *ast.BinaryExpr:
|
|
// fmt.Println("BinaryExpr", expr)
|
|
return parseBinary(expr.Op, expr.X, expr.Y)
|
|
case *ast.UnaryExpr:
|
|
// fmt.Println("UnaryExpr", expr)
|
|
return parseUnary(expr.Op, expr.X)
|
|
case *ast.CallExpr:
|
|
// fmt.Println("CallExpr", expr, expr.Fun, expr.Args)
|
|
return parseFuncCall(expr.Fun, expr.Args)
|
|
case *ast.Ident:
|
|
// fmt.Println("Ident", expr)
|
|
switch expr.Name {
|
|
case "nil": // for nil value such as _id > nil
|
|
return nil, nil
|
|
case "true": // for boolean value true
|
|
return true, nil
|
|
case "false":
|
|
return false, nil // for boolean value false
|
|
default:
|
|
return expr.Name, nil
|
|
}
|
|
case *ast.BasicLit:
|
|
// fmt.Println("BasicLit", expr)
|
|
switch expr.Kind {
|
|
case token.INT:
|
|
intVal, err := strconv.Atoi(expr.Value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return intVal, nil
|
|
case token.FLOAT:
|
|
floatVal, err := strconv.ParseFloat(expr.Value, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return floatVal, nil
|
|
case token.STRING:
|
|
return strings.Trim(expr.Value, `"`), nil
|
|
default:
|
|
return nil, fmt.Errorf("token type %s not supported", expr.Kind.String())
|
|
}
|
|
case *ast.SelectorExpr:
|
|
// fmt.Println("SelectorExpr", expr.X, expr.Sel)
|
|
xExpr, err := parseAST(expr.X)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return fmt.Sprintf("%s.%s", xExpr, expr.Sel.Name), nil
|
|
case *ast.ParenExpr:
|
|
pExpr, err := parseAST(expr.X)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pExpr, nil
|
|
case *ast.CompositeLit:
|
|
if _, ok := expr.Type.(*ast.ArrayType); !ok {
|
|
return nil, fmt.Errorf("not an ArrayType for a composite literal %v", expr.Type)
|
|
}
|
|
elements := make([]interface{}, len(expr.Elts))
|
|
for idx, elt := range expr.Elts {
|
|
e, err := parseAST(elt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
elements[idx] = e
|
|
}
|
|
return elements, nil
|
|
default:
|
|
return nil, fmt.Errorf("expressions other than unary, binary and function call are not allowed %v", expr)
|
|
}
|
|
}
|
|
|
|
// parseBinary parses and returns a JSON struct according to
|
|
// CouchDB mango query syntax for the supported binary operators.
|
|
func parseBinary(operator token.Token, leftOperand, rightOperand ast.Expr) (interface{}, error) {
|
|
left, err := parseAST(leftOperand)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
right, err := parseAST(rightOperand)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// <, <=, ==, !=, >=, >, &&, ||
|
|
switch operator {
|
|
case token.LSS:
|
|
return map[string]interface{}{
|
|
left.(string): map[string]interface{}{"$lt": right},
|
|
}, nil
|
|
case token.LEQ:
|
|
return map[string]interface{}{
|
|
left.(string): map[string]interface{}{"$lte": right},
|
|
}, nil
|
|
case token.EQL:
|
|
return map[string]interface{}{
|
|
left.(string): map[string]interface{}{"$eq": right},
|
|
}, nil
|
|
case token.NEQ:
|
|
return map[string]interface{}{
|
|
left.(string): map[string]interface{}{"$ne": right},
|
|
}, nil
|
|
case token.GEQ:
|
|
return map[string]interface{}{
|
|
left.(string): map[string]interface{}{"$gte": right},
|
|
}, nil
|
|
case token.GTR:
|
|
return map[string]interface{}{
|
|
left.(string): map[string]interface{}{"$gt": right},
|
|
}, nil
|
|
case token.LAND:
|
|
return map[string]interface{}{
|
|
"$and": []interface{}{left, right},
|
|
}, nil
|
|
case token.LOR:
|
|
return map[string]interface{}{
|
|
"$or": []interface{}{left, right},
|
|
}, nil
|
|
}
|
|
return nil, fmt.Errorf("binary operator %s not supported", operator)
|
|
}
|
|
|
|
// parseUnary parses and returns a JSON struct according to
|
|
// CouchDB mango query syntax for supported unary operators.
|
|
func parseUnary(operator token.Token, operandExpr ast.Expr) (interface{}, error) {
|
|
operand, err := parseAST(operandExpr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch operator {
|
|
case token.NOT:
|
|
return map[string]interface{}{
|
|
"$not": operand,
|
|
}, nil
|
|
}
|
|
return nil, fmt.Errorf("unary operator %s not supported", operator)
|
|
}
|
|
|
|
// parseFuncCall parses and returns a JSON struct according to
|
|
// CouchDB mango query syntax for supported meta functions.
|
|
func parseFuncCall(funcExpr ast.Expr, args []ast.Expr) (interface{}, error) {
|
|
funcIdent := funcExpr.(*ast.Ident)
|
|
functionName := funcIdent.Name
|
|
switch functionName {
|
|
case "nor":
|
|
if len(args) < 1 {
|
|
return nil, fmt.Errorf("function nor(exprs...) need at least 1 arguments, not %d", len(args))
|
|
}
|
|
|
|
selectors := make([]interface{}, len(args))
|
|
for idx, arg := range args {
|
|
selector, err := parseAST(arg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
selectors[idx] = selector
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"$nor": selectors,
|
|
}, nil
|
|
case "all":
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("function all(field, array) need 2 arguments, not %d", len(args))
|
|
}
|
|
|
|
fieldExpr, err := parseAST(args[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, ok := fieldExpr.(string); !ok {
|
|
return nil, fmt.Errorf("invalid field expression type %s", fieldExpr)
|
|
}
|
|
|
|
arrayExpr, err := parseAST(args[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
fieldExpr.(string): map[string]interface{}{"$all": arrayExpr},
|
|
}, nil
|
|
case "any":
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("function any(field, condition) need 2 arguments, not %d", len(args))
|
|
}
|
|
|
|
fieldExpr, err := parseAST(args[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, ok := fieldExpr.(string); !ok {
|
|
return nil, fmt.Errorf("invalid field expression type %s", fieldExpr)
|
|
}
|
|
|
|
anyExpr, err := parseAST(args[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
anyExpr, err = removeFieldKey(fieldExpr.(string), anyExpr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
fieldExpr.(string): map[string]interface{}{"$elemMatch": anyExpr},
|
|
}, nil
|
|
case "exists":
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("function exists(field, boolean) need 2 arguments, not %d", len(args))
|
|
}
|
|
|
|
fieldExpr, err := parseAST(args[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, ok := fieldExpr.(string); !ok {
|
|
return nil, fmt.Errorf("invalid field expression type %s", fieldExpr)
|
|
}
|
|
|
|
boolExpr, err := parseAST(args[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
fieldExpr.(string): map[string]interface{}{"$exists": boolExpr},
|
|
}, nil
|
|
case "typeof":
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("function typeof(field, type) need 2 arguments, not %d", len(args))
|
|
}
|
|
|
|
fieldExpr, err := parseAST(args[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, ok := fieldExpr.(string); !ok {
|
|
return nil, fmt.Errorf("invalid field expression type %s", fieldExpr)
|
|
}
|
|
|
|
typeStr, err := parseAST(args[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
fieldExpr.(string): map[string]interface{}{"$type": typeStr},
|
|
}, nil
|
|
case "in":
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("function in(field, array) need 2 arguments, not %d", len(args))
|
|
}
|
|
|
|
fieldExpr, err := parseAST(args[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, ok := fieldExpr.(string); !ok {
|
|
return nil, fmt.Errorf("invalid field expression type %s", fieldExpr)
|
|
}
|
|
|
|
arrExpr, err := parseAST(args[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
fieldExpr.(string): map[string]interface{}{"$in": arrExpr},
|
|
}, nil
|
|
case "nin":
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("function nin(field, array) need 2 arguments, not %d", len(args))
|
|
}
|
|
|
|
fieldExpr, err := parseAST(args[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, ok := fieldExpr.(string); !ok {
|
|
return nil, fmt.Errorf("invalid field expression type %s", fieldExpr)
|
|
}
|
|
|
|
arrExpr, err := parseAST(args[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
fieldExpr.(string): map[string]interface{}{"$nin": arrExpr},
|
|
}, nil
|
|
case "size":
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("function size(field, int) need 2 arguments, not %d", len(args))
|
|
}
|
|
|
|
fieldExpr, err := parseAST(args[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, ok := fieldExpr.(string); !ok {
|
|
return nil, fmt.Errorf("invalid field expression type %s", fieldExpr)
|
|
}
|
|
|
|
intExpr, err := parseAST(args[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
fieldExpr.(string): map[string]interface{}{"$size": intExpr},
|
|
}, nil
|
|
case "mod":
|
|
if len(args) != 3 {
|
|
return nil, fmt.Errorf("function mod(field, divisor, remainder) need 3 arguments, not %d", len(args))
|
|
}
|
|
|
|
fieldExpr, err := parseAST(args[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, ok := fieldExpr.(string); !ok {
|
|
return nil, fmt.Errorf("invalid field expression type %s", fieldExpr)
|
|
}
|
|
|
|
divisorExpr, err := parseAST(args[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
divisor, ok := divisorExpr.(int)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid divisor %s", divisorExpr)
|
|
}
|
|
|
|
remainderExpr, err := parseAST(args[2])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
remainder, ok := remainderExpr.(int)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid remainder %s", remainderExpr)
|
|
}
|
|
|
|
expr, err := parser.ParseExpr(fmt.Sprintf("%#v", []int{divisor, remainder}))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
arrExpr, err := parseAST(expr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
fieldExpr.(string): map[string]interface{}{"$mod": arrExpr},
|
|
}, nil
|
|
case "regex":
|
|
if len(args) != 2 {
|
|
return nil, fmt.Errorf("function regex(field, regexstr) need 2 arguments, not %d", len(args))
|
|
}
|
|
|
|
fieldExpr, err := parseAST(args[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, ok := fieldExpr.(string); !ok {
|
|
return nil, fmt.Errorf("invalid field expression type %s", fieldExpr)
|
|
}
|
|
|
|
regexExpr, err := parseAST(args[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
fieldExpr.(string): map[string]interface{}{"$regex": regexExpr},
|
|
}, nil
|
|
case "asc": // for sort syntax
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("function asc(field) need 1 argument, not %d", len(args))
|
|
}
|
|
|
|
fieldExpr, err := parseAST(args[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, ok := fieldExpr.(string); !ok {
|
|
return nil, fmt.Errorf("invalid field expression type %s", fieldExpr)
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
fieldExpr.(string): "asc",
|
|
}, nil
|
|
case "desc": // for sort syntax
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("function desc(field) need 1 argument, not %d", len(args))
|
|
}
|
|
|
|
fieldExpr, err := parseAST(args[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, ok := fieldExpr.(string); !ok {
|
|
return nil, fmt.Errorf("invalid field expression type %s", fieldExpr)
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
fieldExpr.(string): "desc",
|
|
}, nil
|
|
}
|
|
return nil, fmt.Errorf("function %s() not supported", functionName)
|
|
}
|
|
|
|
// removeFieldKey removes the key which equals to fieldName,
|
|
// moves its value one level up in the map.
|
|
func removeFieldKey(fieldName string, exprMap interface{}) (interface{}, error) {
|
|
mapValue := reflect.ValueOf(exprMap)
|
|
if mapValue.Kind() != reflect.Map {
|
|
return nil, errors.New("not a map type")
|
|
}
|
|
mapKeys := mapValue.MapKeys()
|
|
for _, mapKey := range mapKeys {
|
|
// exprMap is a interface type contains map[string]interface{}
|
|
// so MapIndex returns a value whose Kind is Interface, so we
|
|
// have to call its Interface() methods then pass to ValueOf()
|
|
// to get the underline map type.
|
|
value := reflect.ValueOf(mapValue.MapIndex(mapKey).Interface())
|
|
if value.Kind() == reflect.Slice {
|
|
for idx := 0; idx < value.Len(); idx++ {
|
|
elemVal := value.Index(idx)
|
|
processed, err := removeFieldKey(fieldName, elemVal.Interface())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
elemVal.Set(reflect.ValueOf(processed))
|
|
}
|
|
mapValue.SetMapIndex(mapKey, value)
|
|
} else if value.Kind() == reflect.Map {
|
|
if mapKey.Interface().(string) == fieldName { // found
|
|
if value.Len() != 1 {
|
|
return nil, fmt.Errorf("field map length %d, not 1", value.Len())
|
|
}
|
|
// setting to empty value deletes the key
|
|
mapValue.SetMapIndex(mapKey, reflect.Value{})
|
|
keys := value.MapKeys()
|
|
// moves the value one level up
|
|
for _, key := range keys {
|
|
val := value.MapIndex(key)
|
|
mapValue.SetMapIndex(key, val)
|
|
}
|
|
} else {
|
|
processed, err := removeFieldKey(fieldName, value.Interface())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mapValue.SetMapIndex(mapKey, reflect.ValueOf(processed))
|
|
}
|
|
}
|
|
}
|
|
return mapValue.Interface(), nil
|
|
}
|
|
|
|
// beautifulJSONString returns a beautified string representing the JSON struct.
|
|
func beautifulJSONString(jsonable interface{}) (string, error) {
|
|
b, err := json.Marshal(jsonable)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var out bytes.Buffer
|
|
err = json.Indent(&out, b, "", "\t")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return out.String(), nil
|
|
}
|
|
|
|
// PutIndex creates a new index in database.
|
|
//
|
|
// indexFields: a JSON array of field names following the sort syntax.
|
|
//
|
|
// ddoc: optional, name of the design document in which the index will be created.
|
|
// By default each index will be created in its own design document. Indexes can be
|
|
// grouped into design documents for efficiency. However a change to one index
|
|
// in a design document will invalidate all other indexes in the same document.
|
|
//
|
|
// name: optional, name of the index. A name generated automatically if not provided.
|
|
func (d *Database) PutIndex(indexFields []string, ddoc, name string) (string, string, error) {
|
|
var design, index string
|
|
if len(indexFields) == 0 {
|
|
return design, index, errors.New("index fields cannot be empty")
|
|
}
|
|
|
|
indexObjs, err := parseSortSyntax(indexFields)
|
|
if err != nil {
|
|
return design, index, err
|
|
}
|
|
|
|
indexJSON := map[string]interface{}{}
|
|
indexJSON["index"] = map[string]interface{}{
|
|
"fields": indexObjs,
|
|
}
|
|
|
|
if len(ddoc) > 0 {
|
|
indexJSON["ddoc"] = ddoc
|
|
}
|
|
|
|
if len(name) > 0 {
|
|
indexJSON["name"] = name
|
|
}
|
|
|
|
_, data, err := d.resource.PostJSON("_index", nil, indexJSON, nil)
|
|
if err != nil {
|
|
return design, index, err
|
|
}
|
|
|
|
result, err := parseData(data)
|
|
if err != nil {
|
|
return design, index, err
|
|
}
|
|
design = result["id"].(string)
|
|
index = result["name"].(string)
|
|
|
|
return design, index, nil
|
|
}
|
|
|
|
// GetIndex gets all indexes created in database.
|
|
func (d *Database) GetIndex() (map[string]*json.RawMessage, error) {
|
|
_, data, err := d.resource.GetJSON("_index", nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parseRaw(data)
|
|
}
|
|
|
|
// DeleteIndex deletes index in database.
|
|
func (d *Database) DeleteIndex(ddoc, name string) error {
|
|
indexRes := docResource(d.resource, fmt.Sprintf("_index/%s/json/%s", ddoc, name))
|
|
_, _, err := indexRes.DeleteJSON("", nil, nil)
|
|
return err
|
|
}
|
|
|
|
// designPath resturns a make-up design path based on designDoc and designType
|
|
// for example designPath("design/foo", "_view") returns "_design/design/_view/foo"
|
|
func designPath(designDoc, designType string) string {
|
|
if strings.HasPrefix(designDoc, "_") {
|
|
return designDoc
|
|
}
|
|
parts := strings.SplitN(designDoc, "/", 2)
|
|
if len(parts) == 1 {
|
|
return parts[0]
|
|
}
|
|
return strings.Join([]string{"_design", parts[0], designType, parts[1]}, "/")
|
|
}
|
|
|
|
// View executes a predefined design document view and returns the results.
|
|
//
|
|
// name: the name of the view, for user-defined views use the format "design_docid/viewname",
|
|
// that is, the document ID of the design document and the name of the view, separated by a /.
|
|
//
|
|
// wrapper: an optional function for processing the result rows after retrieved.
|
|
//
|
|
// options: optional query parameters.
|
|
func (d *Database) View(name string, wrapper func(Row) Row, options map[string]interface{}) (*ViewResults, error) {
|
|
designDocPath := designPath(name, "_view")
|
|
return newViewResults(d.resource, designDocPath, options, wrapper), nil
|
|
}
|
|
|
|
// IterView returns a channel fetching rows in batches which iterates a row at a time(pagination).
|
|
//
|
|
// name: the name of the view, for user-defined views use the format "design_docid/viewname",
|
|
// that is, the document ID of the design document and the name of the view, separated by a /.
|
|
//
|
|
// wrapper: an optional function for processing the result rows after retrieved.
|
|
//
|
|
// options: optional query parameters.
|
|
func (d *Database) IterView(name string, batch int, wrapper func(Row) Row, options map[string]interface{}) (<-chan Row, error) {
|
|
if batch <= 0 {
|
|
return nil, ErrBatchValue
|
|
}
|
|
|
|
if options == nil {
|
|
options = map[string]interface{}{}
|
|
}
|
|
|
|
_, ok := options["limit"]
|
|
var limit int
|
|
if ok {
|
|
if options["limit"].(int) <= 0 {
|
|
return nil, ErrLimitValue
|
|
}
|
|
limit = options["limit"].(int)
|
|
}
|
|
|
|
// Row generator
|
|
rchan := make(chan Row)
|
|
var err error
|
|
go func() {
|
|
defer close(rchan)
|
|
for {
|
|
loopLimit := batch
|
|
if ok {
|
|
loopLimit = min(batch, limit)
|
|
}
|
|
// get rows in batch with one extra for start of next batch
|
|
options["limit"] = loopLimit + 1
|
|
var results *ViewResults
|
|
results, err = d.View(name, wrapper, options)
|
|
if err != nil {
|
|
break
|
|
}
|
|
var rows []Row
|
|
rows, err = results.Rows()
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
// send all rows to channel except the last extra one
|
|
for _, row := range rows[:min(len(rows), loopLimit)] {
|
|
rchan <- row
|
|
}
|
|
|
|
if ok {
|
|
limit -= min(len(rows), batch)
|
|
}
|
|
|
|
if len(rows) <= batch || (ok && limit == 0) {
|
|
break
|
|
}
|
|
options["startkey"] = rows[len(rows)-1].Key
|
|
options["startkey_docid"] = rows[len(rows)-1].ID
|
|
options["skip"] = 0
|
|
}
|
|
}()
|
|
return rchan, nil
|
|
}
|
|
|
|
func min(a, b int) int {
|
|
return int(math.Min(float64(a), float64(b)))
|
|
}
|
|
|
|
// Show calls a server-side 'show' function.
|
|
//
|
|
// name: the name of the show function in the format "designdoc/showname"
|
|
//
|
|
// docID: optional document ID to pass to the show function
|
|
//
|
|
// params: optional query parameters
|
|
func (d *Database) Show(name, docID string, params url.Values) (http.Header, []byte, error) {
|
|
designDocPath := designPath(name, "_show")
|
|
if docID != "" {
|
|
designDocPath = fmt.Sprintf("%s/%s", designDocPath, docID)
|
|
}
|
|
return d.resource.Get(designDocPath, nil, params)
|
|
}
|
|
|
|
// List formats a view using a server-side 'list' function.
|
|
//
|
|
// name: the name of the list function in the format "designdoc/listname"
|
|
//
|
|
// view: the name of the view in the format "designdoc/viewname"
|
|
//
|
|
// options: optional query parameters
|
|
func (d *Database) List(name, view string, options map[string]interface{}) (http.Header, []byte, error) {
|
|
designDocPath := designPath(name, "_list")
|
|
res := docResource(d.resource, fmt.Sprintf("%s/%s", designDocPath, strings.Split(view, "/")[1]))
|
|
return viewLikeResourceRequest(res, options)
|
|
}
|
|
|
|
// UpdateDoc calls server-side update handler.
|
|
//
|
|
// name: the name of the update handler function in the format "designdoc/updatename".
|
|
//
|
|
// docID: optional document ID to pass to the show function
|
|
//
|
|
// params: optional query parameters
|
|
func (d *Database) UpdateDoc(name, docID string, params url.Values) (http.Header, []byte, error) {
|
|
designDocPath := designPath(name, "_update")
|
|
if docID == "" {
|
|
return d.resource.Post(designDocPath, nil, nil, params)
|
|
}
|
|
|
|
designDocPath = fmt.Sprintf("%s/%s", designDocPath, docID)
|
|
return d.resource.Put(designDocPath, nil, nil, params)
|
|
}
|