go_service/vendor/github.com/leesper/couchdb-golang/database.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)
}