1package kivik 2 3import ( 4 "context" 5 "encoding/json" 6 "io" 7 "io/ioutil" 8 "reflect" 9 "strings" 10 11 "github.com/go-kivik/kivik/driver" 12 "github.com/go-kivik/kivik/errors" 13) 14 15// DB is a handle to a specific database. 16type DB struct { 17 client *Client 18 name string 19 driverDB driver.DB 20} 21 22// Client returns the Client used to connect to the database. 23func (db *DB) Client() *Client { 24 return db.client 25} 26 27// Name returns the database name as passed when creating the DB connection. 28func (db *DB) Name() string { 29 return db.name 30} 31 32// AllDocs returns a list of all documents in the database. 33func (db *DB) AllDocs(ctx context.Context, options ...Options) (*Rows, error) { 34 opts, err := mergeOptions(options...) 35 if err != nil { 36 return nil, err 37 } 38 rowsi, err := db.driverDB.AllDocs(ctx, opts) 39 if err != nil { 40 return nil, err 41 } 42 return newRows(ctx, rowsi), nil 43} 44 45// Query executes the specified view function from the specified design 46// document. ddoc and view may or may not be be prefixed with '_design/' 47// and '_view/' respectively. No other 48func (db *DB) Query(ctx context.Context, ddoc, view string, options ...Options) (*Rows, error) { 49 opts, err := mergeOptions(options...) 50 if err != nil { 51 return nil, err 52 } 53 ddoc = strings.TrimPrefix(ddoc, "_design/") 54 view = strings.TrimPrefix(view, "_view/") 55 rowsi, err := db.driverDB.Query(ctx, ddoc, view, opts) 56 if err != nil { 57 return nil, err 58 } 59 return newRows(ctx, rowsi), nil 60} 61 62// Row contains the result of calling Get for a single document. For most uses, 63// it is sufficient just to call the ScanDoc method. For more advanced uses, the 64// fields may be accessed directly. 65type Row struct { 66 // ContentLength records the size of the JSON representation of the document 67 // as requestd. The value -1 indicates that the length is unknown. Values 68 // >= 0 indicate that the given number of bytes may be read from Body. 69 ContentLength int64 70 71 // Rev is the revision ID of the returned document. 72 Rev string 73 74 // Body represents the document's content. 75 // 76 // Kivik will always return a non-nil Body, except when Err is non-nil. The 77 // ScanDoc method will close Body. When not using ScanDoc, it is the 78 // caller's responsibility to close Body 79 Body io.ReadCloser 80 81 // Err contains any error that occurred while fetching the document. It is 82 // typically returned by ScanDoc. 83 Err error 84 85 // Attachments is experimental 86 Attachments *AttachmentsIterator 87} 88 89// ScanDoc unmarshals the data from the fetched row into dest. It is an 90// intelligent wrapper around json.Unmarshal which also handles 91// multipart/related responses. When done, the underlying reader is closed. 92func (r *Row) ScanDoc(dest interface{}) error { 93 if r.Err != nil { 94 return r.Err 95 } 96 if reflect.TypeOf(dest).Kind() != reflect.Ptr { 97 return errNonPtr 98 } 99 defer r.Body.Close() // nolint: errcheck 100 return errors.WrapStatus(StatusBadResponse, json.NewDecoder(r.Body).Decode(dest)) 101} 102 103// Get fetches the requested document. Any errors are deferred until the 104// row.ScanDoc call. 105func (db *DB) Get(ctx context.Context, docID string, options ...Options) *Row { 106 opts, err := mergeOptions(options...) 107 if err != nil { 108 return &Row{Err: err} 109 } 110 doc, err := db.driverDB.Get(ctx, docID, opts) 111 if err != nil { 112 return &Row{Err: err} 113 } 114 row := &Row{ 115 ContentLength: doc.ContentLength, 116 Rev: doc.Rev, 117 Body: doc.Body, 118 } 119 if doc.Attachments != nil { 120 row.Attachments = &AttachmentsIterator{atti: doc.Attachments} 121 } 122 return row 123} 124 125// GetMeta returns the size and rev of the specified document. GetMeta accepts 126// the same options as the Get method. 127func (db *DB) GetMeta(ctx context.Context, docID string, options ...Options) (size int64, rev string, err error) { 128 opts, err := mergeOptions(options...) 129 if err != nil { 130 return 0, "", err 131 } 132 if r, ok := db.driverDB.(driver.MetaGetter); ok { 133 return r.GetMeta(ctx, docID, opts) 134 } 135 row := db.Get(ctx, docID, nil) 136 if row.Err != nil { 137 return 0, "", row.Err 138 } 139 if row.Rev != "" { 140 _ = row.Body.Close() 141 return row.ContentLength, row.Rev, nil 142 } 143 var doc struct { 144 Rev string `json:"_rev"` 145 } 146 // These last two lines cannot be combined for GopherJS due to a bug. 147 // See https://github.com/gopherjs/gopherjs/issues/608 148 err = row.ScanDoc(&doc) 149 return row.ContentLength, doc.Rev, err 150} 151 152// CreateDoc creates a new doc with an auto-generated unique ID. The generated 153// docID and new rev are returned. 154func (db *DB) CreateDoc(ctx context.Context, doc interface{}, options ...Options) (docID, rev string, err error) { 155 opts, err := mergeOptions(options...) 156 if err != nil { 157 return "", "", err 158 } 159 return db.driverDB.CreateDoc(ctx, doc, opts) 160} 161 162// normalizeFromJSON unmarshals a []byte, json.RawMessage or io.Reader to a 163// map[string]interface{}, or passed through any other types. 164func normalizeFromJSON(i interface{}) (interface{}, error) { 165 var body []byte 166 switch t := i.(type) { 167 case []byte: 168 body = t 169 case json.RawMessage: 170 body = t 171 default: 172 r, ok := i.(io.Reader) 173 if !ok { 174 return i, nil 175 } 176 var err error 177 body, err = ioutil.ReadAll(r) 178 if err != nil { 179 return nil, errors.WrapStatus(StatusUnknownError, err) 180 } 181 } 182 var x map[string]interface{} 183 if err := json.Unmarshal(body, &x); err != nil { 184 return nil, errors.WrapStatus(StatusBadRequest, err) 185 } 186 return x, nil 187} 188 189func extractDocID(i interface{}) (string, bool) { 190 if i == nil { 191 return "", false 192 } 193 var id string 194 var ok bool 195 switch t := i.(type) { 196 case map[string]interface{}: 197 id, ok = t["_id"].(string) 198 case map[string]string: 199 id, ok = t["_id"] 200 default: 201 data, err := json.Marshal(i) 202 if err != nil { 203 return "", false 204 } 205 var result struct { 206 ID string `json:"_id"` 207 } 208 if err := json.Unmarshal(data, &result); err != nil { 209 return "", false 210 } 211 id = result.ID 212 ok = result.ID != "" 213 } 214 if !ok { 215 return "", false 216 } 217 return id, true 218} 219 220// Put creates a new doc or updates an existing one, with the specified docID. 221// If the document already exists, the current revision must be included in doc, 222// with JSON key '_rev', otherwise a conflict will occur. The new rev is 223// returned. 224// 225// doc may be one of: 226// 227// - An object to be marshaled to JSON. The resulting JSON structure must 228// conform to CouchDB standards. 229// - A []byte value, containing a valid JSON document 230// - A json.RawMessage value containing a valid JSON document 231// - An io.Reader, from which a valid JSON document may be read. 232func (db *DB) Put(ctx context.Context, docID string, doc interface{}, options ...Options) (rev string, err error) { 233 if docID == "" { 234 return "", missingArg("docID") 235 } 236 i, err := normalizeFromJSON(doc) 237 if err != nil { 238 return "", err 239 } 240 opts, err := mergeOptions(options...) 241 if err != nil { 242 return "", err 243 } 244 return db.driverDB.Put(ctx, docID, i, opts) 245} 246 247// Delete marks the specified document as deleted. 248func (db *DB) Delete(ctx context.Context, docID, rev string, options ...Options) (newRev string, err error) { 249 if docID == "" { 250 return "", missingArg("docID") 251 } 252 opts, err := mergeOptions(options...) 253 if err != nil { 254 return "", err 255 } 256 return db.driverDB.Delete(ctx, docID, rev, opts) 257} 258 259// Flush requests a flush of disk cache to disk or other permanent storage. 260// 261// See http://docs.couchdb.org/en/2.0.0/api/database/compact.html#db-ensure-full-commit 262func (db *DB) Flush(ctx context.Context) error { 263 if flusher, ok := db.driverDB.(driver.Flusher); ok { 264 return flusher.Flush(ctx) 265 } 266 return errors.Status(StatusNotImplemented, "kivik: flush not supported by driver") 267} 268 269// DBStats contains database statistics.. 270type DBStats struct { 271 // Name is the name of the database. 272 Name string `json:"db_name"` 273 // CompactRunning is true if the database is currently being compacted. 274 CompactRunning bool `json:"compact_running"` 275 // DocCount is the number of documents are currently stored in the database. 276 DocCount int64 `json:"doc_count"` 277 // DeletedCount is a count of documents which have been deleted from the 278 // database. 279 DeletedCount int64 `json:"doc_del_count"` 280 // UpdateSeq is the current update sequence for the database. 281 UpdateSeq string `json:"update_seq"` 282 // DiskSize is the number of bytes used on-disk to store the database. 283 DiskSize int64 `json:"disk_size"` 284 // ActiveSize is the number of bytes used on-disk to store active documents. 285 // If this number is lower than DiskSize, then compaction would free disk 286 // space. 287 ActiveSize int64 `json:"data_size"` 288 // ExternalSize is the size of the documents in the database, as represented 289 // as JSON, before compression. 290 ExternalSize int64 `json:"-"` 291} 292 293// Stats returns database statistics. 294func (db *DB) Stats(ctx context.Context) (*DBStats, error) { 295 i, err := db.driverDB.Stats(ctx) 296 if err != nil { 297 return nil, err 298 } 299 stats := DBStats(*i) 300 return &stats, nil 301} 302 303// Compact begins compaction of the database. Check the CompactRunning field 304// returned by Info() to see if the compaction has completed. 305// See http://docs.couchdb.org/en/2.0.0/api/database/compact.html#db-compact 306func (db *DB) Compact(ctx context.Context) error { 307 return db.driverDB.Compact(ctx) 308} 309 310// CompactView compats the view indexes associated with the specified design 311// document. 312// See http://docs.couchdb.org/en/2.0.0/api/database/compact.html#db-compact-design-doc 313func (db *DB) CompactView(ctx context.Context, ddocID string) error { 314 return db.driverDB.CompactView(ctx, ddocID) 315} 316 317// ViewCleanup removes view index files that are no longer required as a result 318// of changed views within design documents. 319// See http://docs.couchdb.org/en/2.0.0/api/database/compact.html#db-view-cleanup 320func (db *DB) ViewCleanup(ctx context.Context) error { 321 return db.driverDB.ViewCleanup(ctx) 322} 323 324// Security returns the database's security document. 325// See http://couchdb.readthedocs.io/en/latest/api/database/security.html#get--db-_security 326func (db *DB) Security(ctx context.Context) (*Security, error) { 327 s, err := db.driverDB.Security(ctx) 328 if err != nil { 329 return nil, err 330 } 331 return &Security{ 332 Admins: Members(s.Admins), 333 Members: Members(s.Members), 334 }, err 335} 336 337// SetSecurity sets the database's security document. 338// See http://couchdb.readthedocs.io/en/latest/api/database/security.html#put--db-_security 339func (db *DB) SetSecurity(ctx context.Context, security *Security) error { 340 if security == nil { 341 return missingArg("security") 342 } 343 sec := &driver.Security{ 344 Admins: driver.Members(security.Admins), 345 Members: driver.Members(security.Members), 346 } 347 return db.driverDB.SetSecurity(ctx, sec) 348} 349 350// Copy copies the source document to a new document with an ID of targetID. If 351// the database backend does not support COPY directly, the operation will be 352// emulated with a Get followed by Put. The target will be an exact copy of the 353// source, with only the ID and revision changed. 354// 355// See http://docs.couchdb.org/en/2.0.0/api/document/common.html#copy--db-docid 356func (db *DB) Copy(ctx context.Context, targetID, sourceID string, options ...Options) (targetRev string, err error) { 357 if targetID == "" { 358 return "", missingArg("targetID") 359 } 360 if sourceID == "" { 361 return "", missingArg("sourceID") 362 } 363 opts, err := mergeOptions(options...) 364 if err != nil { 365 return "", err 366 } 367 if copier, ok := db.driverDB.(driver.Copier); ok { 368 return copier.Copy(ctx, targetID, sourceID, opts) 369 } 370 var doc map[string]interface{} 371 if err = db.Get(ctx, sourceID, opts).ScanDoc(&doc); err != nil { 372 return "", err 373 } 374 delete(doc, "_rev") 375 doc["_id"] = targetID 376 delete(opts, "rev") // rev has a completely different meaning for Copy and Put 377 return db.Put(ctx, targetID, doc, opts) 378} 379 380// PutAttachment uploads the supplied content as an attachment to the specified 381// document. 382func (db *DB) PutAttachment(ctx context.Context, docID, rev string, att *Attachment, options ...Options) (newRev string, err error) { 383 if docID == "" { 384 return "", missingArg("docID") 385 } 386 if e := att.validate(); e != nil { 387 return "", e 388 } 389 opts, err := mergeOptions(options...) 390 if err != nil { 391 return "", err 392 } 393 a := driver.Attachment(*att) 394 return db.driverDB.PutAttachment(ctx, docID, rev, &a, opts) 395} 396 397// GetAttachment returns a file attachment associated with the document. 398func (db *DB) GetAttachment(ctx context.Context, docID, rev, filename string, options ...Options) (*Attachment, error) { 399 if docID == "" { 400 return nil, missingArg("docID") 401 } 402 if filename == "" { 403 return nil, missingArg("filename") 404 } 405 opts, e := mergeOptions(options...) 406 if e != nil { 407 return nil, e 408 } 409 att, err := db.driverDB.GetAttachment(ctx, docID, rev, filename, opts) 410 if err != nil { 411 return nil, err 412 } 413 a := Attachment(*att) 414 return &a, nil 415} 416 417type nilContentReader struct{} 418 419var _ io.ReadCloser = &nilContentReader{} 420 421func (c nilContentReader) Read(_ []byte) (int, error) { return 0, io.EOF } 422func (c nilContentReader) Close() error { return nil } 423 424var nilContent = nilContentReader{} 425 426// GetAttachmentMeta returns meta data about an attachment. The attachment 427// content returned will be empty. 428func (db *DB) GetAttachmentMeta(ctx context.Context, docID, rev, filename string, options ...Options) (*Attachment, error) { 429 if docID == "" { 430 return nil, missingArg("docID") 431 } 432 if filename == "" { 433 return nil, missingArg("filename") 434 } 435 var att *Attachment 436 if metaer, ok := db.driverDB.(driver.AttachmentMetaGetter); ok { 437 opts, err := mergeOptions(options...) 438 if err != nil { 439 return nil, err 440 } 441 a, err := metaer.GetAttachmentMeta(ctx, docID, rev, filename, opts) 442 if err != nil { 443 return nil, err 444 } 445 att = new(Attachment) 446 *att = Attachment(*a) 447 } else { 448 var err error 449 att, err = db.GetAttachment(ctx, docID, rev, filename, options...) 450 if err != nil { 451 return nil, err 452 } 453 } 454 if att.Content != nil { 455 _ = att.Content.Close() // Ensure this is closed 456 } 457 att.Content = nilContent 458 return att, nil 459} 460 461// DeleteAttachment delets an attachment from a document, returning the 462// document's new revision. 463func (db *DB) DeleteAttachment(ctx context.Context, docID, rev, filename string, options ...Options) (newRev string, err error) { 464 if docID == "" { 465 return "", missingArg("docID") 466 } 467 if filename == "" { 468 return "", missingArg("filename") 469 } 470 opts, err := mergeOptions(options...) 471 if err != nil { 472 return "", err 473 } 474 return db.driverDB.DeleteAttachment(ctx, docID, rev, filename, opts) 475} 476