github.com/go-kivik/kivik/v4@v4.3.2/db.go (about) 1 // Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 // use this file except in compliance with the License. You may obtain a copy of 3 // the License at 4 // 5 // http://www.apache.org/licenses/LICENSE-2.0 6 // 7 // Unless required by applicable law or agreed to in writing, software 8 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 // License for the specific language governing permissions and limitations under 11 // the License. 12 13 package kivik 14 15 import ( 16 "context" 17 "encoding/json" 18 "errors" 19 "io" 20 "net/http" 21 "strings" 22 "sync" 23 24 "github.com/google/uuid" 25 26 "github.com/go-kivik/kivik/v4/driver" 27 internal "github.com/go-kivik/kivik/v4/int/errors" 28 ) 29 30 // DB is a handle to a specific database. 31 type DB struct { 32 client *Client 33 name string 34 driverDB driver.DB 35 err error 36 37 closed bool 38 mu sync.Mutex 39 wg sync.WaitGroup 40 } 41 42 func (db *DB) startQuery() (end func(), _ error) { 43 db.mu.Lock() 44 defer db.mu.Unlock() 45 if db.closed { 46 return nil, ErrDatabaseClosed 47 } 48 endQuery, err := db.client.startQuery() 49 if err != nil { 50 return nil, err 51 } 52 var once sync.Once 53 db.wg.Add(1) 54 return func() { 55 once.Do(func() { 56 db.mu.Lock() 57 db.wg.Done() 58 endQuery() 59 db.mu.Unlock() 60 }) 61 }, nil 62 } 63 64 // Client returns the client used to connect to the database. 65 func (db *DB) Client() *Client { 66 return db.client 67 } 68 69 // Name returns the database name as passed when creating the DB connection. 70 func (db *DB) Name() string { 71 return db.name 72 } 73 74 // Err returns the error, if any, that occurred while connecting to or creating 75 // the database. This error will be deferred until the next call, normally, so 76 // using this method is only necessary if you need to directly check the error, 77 // and intend to do nothing else with the DB object. 78 func (db *DB) Err() error { 79 return db.err 80 } 81 82 // AllDocs returns a list of all documents in the database. 83 func (db *DB) AllDocs(ctx context.Context, options ...Option) *ResultSet { 84 if db.err != nil { 85 return &ResultSet{iter: errIterator(db.err)} 86 } 87 endQuery, err := db.startQuery() 88 if err != nil { 89 return &ResultSet{iter: errIterator(err)} 90 } 91 rowsi, err := db.driverDB.AllDocs(ctx, multiOptions(options)) 92 if err != nil { 93 endQuery() 94 return &ResultSet{iter: errIterator(err)} 95 } 96 return newResultSet(ctx, endQuery, rowsi) 97 } 98 99 // DesignDocs returns a list of all documents in the database. 100 func (db *DB) DesignDocs(ctx context.Context, options ...Option) *ResultSet { 101 if db.err != nil { 102 return &ResultSet{iter: errIterator(db.err)} 103 } 104 ddocer, ok := db.driverDB.(driver.DesignDocer) 105 if !ok { 106 return &ResultSet{iter: errIterator(&internal.Error{Status: http.StatusNotImplemented, Err: errors.New("kivik: design doc view not supported by driver")})} 107 } 108 109 endQuery, err := db.startQuery() 110 if err != nil { 111 return &ResultSet{iter: errIterator(err)} 112 } 113 rowsi, err := ddocer.DesignDocs(ctx, multiOptions(options)) 114 if err != nil { 115 endQuery() 116 return &ResultSet{iter: errIterator(err)} 117 } 118 return newResultSet(ctx, endQuery, rowsi) 119 } 120 121 // LocalDocs returns a list of all documents in the database. 122 func (db *DB) LocalDocs(ctx context.Context, options ...Option) *ResultSet { 123 if db.err != nil { 124 return &ResultSet{iter: errIterator(db.err)} 125 } 126 ldocer, ok := db.driverDB.(driver.LocalDocer) 127 if !ok { 128 return &ResultSet{iter: errIterator(&internal.Error{Status: http.StatusNotImplemented, Err: errors.New("kivik: local doc view not supported by driver")})} 129 } 130 endQuery, err := db.startQuery() 131 if err != nil { 132 return &ResultSet{iter: errIterator(err)} 133 } 134 rowsi, err := ldocer.LocalDocs(ctx, multiOptions(options)) 135 if err != nil { 136 endQuery() 137 return &ResultSet{iter: errIterator(err)} 138 } 139 return newResultSet(ctx, endQuery, rowsi) 140 } 141 142 // Query executes the specified view function from the specified design 143 // document. ddoc and view may or may not be be prefixed with '_design/' 144 // and '_view/' respectively. 145 // 146 // See [views] in the CouchDB documentation. 147 // 148 // If supported by the backend and database (i.e. CouchDB 2.2+), you may pass 149 // multiple queries to a single view by passing an option called `queries` with 150 // a multi-query object as a value. 151 // 152 // See [multiple queries] in the CouchDB documentation. 153 // 154 // [views]: https://docs.couchdb.org/en/stable/api/ddoc/views.html# 155 // [multiple queries]: https://docs.couchdb.org/en/stable/api/ddoc/views.html#sending-multiple-queries-to-a-view 156 func (db *DB) Query(ctx context.Context, ddoc, view string, options ...Option) *ResultSet { 157 if db.err != nil { 158 return &ResultSet{iter: errIterator(db.err)} 159 } 160 endQuery, err := db.startQuery() 161 if err != nil { 162 return &ResultSet{iter: errIterator(err)} 163 } 164 ddoc = strings.TrimPrefix(ddoc, "_design/") 165 view = strings.TrimPrefix(view, "_view/") 166 rowsi, err := db.driverDB.Query(ctx, ddoc, view, multiOptions(options)) 167 if err != nil { 168 endQuery() 169 return &ResultSet{iter: errIterator(err)} 170 } 171 return newResultSet(ctx, endQuery, rowsi) 172 } 173 174 // Document is a single document result returned by [DB.Get]. 175 type Document struct { 176 err error 177 rev string 178 body io.Reader 179 attachments driver.Attachments 180 181 mu sync.Mutex 182 } 183 184 // Err returns the error, if any, that was encountered fetching the document. 185 func (r *Document) Err() error { 186 return r.err 187 } 188 189 // Rev returns the document revision. 190 func (r *Document) Rev() (string, error) { 191 return r.rev, r.err 192 } 193 194 // ScanDoc unmarshals the document into i. 195 func (r *Document) ScanDoc(i interface{}) error { 196 if r.err != nil { 197 return r.err 198 } 199 return json.NewDecoder(r.body).Decode(i) 200 } 201 202 // Attachments returns an attachments iterator if the document includes 203 // attachments and they are not inline. 204 func (r *Document) Attachments() (*AttachmentsIterator, error) { 205 if r.err != nil { 206 return nil, r.err 207 } 208 if r.attachments == nil { 209 return nil, errNoAttachments 210 } 211 r.mu.Lock() 212 return &AttachmentsIterator{ 213 atti: r.attachments, 214 onClose: r.mu.Unlock, 215 }, nil 216 } 217 218 // Close closes the document resources. 219 func (r *Document) Close() error { 220 r.mu.Lock() 221 defer r.mu.Unlock() 222 if atts := r.attachments; atts != nil { 223 _ = atts.Close() 224 } 225 if c, ok := r.body.(io.Closer); ok { 226 return c.Close() 227 } 228 return nil 229 } 230 231 // Get fetches the requested document. Any errors are deferred until the first 232 // call to [ResultSet.ScanDoc] or any other result set method. 233 func (db *DB) Get(ctx context.Context, docID string, options ...Option) *Document { 234 if db.err != nil { 235 return &Document{err: db.err} 236 } 237 endQuery, err := db.startQuery() 238 if err != nil { 239 return &Document{err: err} 240 } 241 defer endQuery() 242 result, err := db.driverDB.Get(ctx, docID, multiOptions(options)) 243 if err != nil { 244 return &Document{err: err} 245 } 246 return &Document{ 247 rev: result.Rev, 248 body: result.Body, 249 attachments: result.Attachments, 250 } 251 } 252 253 // OpenRevs returns documents of specified leaf revisions. Additionally, it 254 // accepts a revs value of "all" to return all leaf revisions. 255 // 256 // This function is experimental, and may change without notice. 257 func (db *DB) OpenRevs(ctx context.Context, docID string, revs []string, options ...Option) *ResultSet { 258 if db.err != nil { 259 return &ResultSet{iter: errIterator(db.err)} 260 } 261 if openRever, ok := db.driverDB.(driver.OpenRever); ok { 262 endQuery, err := db.startQuery() 263 if err != nil { 264 return &ResultSet{iter: errIterator(err)} 265 } 266 rowsi, err := openRever.OpenRevs(ctx, docID, revs, multiOptions(options)) 267 if err != nil { 268 endQuery() 269 return &ResultSet{iter: errIterator(err)} 270 } 271 return newResultSet(ctx, endQuery, rowsi) 272 } 273 return &ResultSet{iter: errIterator(errOpenRevsNotImplemented)} 274 } 275 276 // GetRev returns the active rev of the specified document. GetRev accepts 277 // the same options as [DB.Get]. 278 func (db *DB) GetRev(ctx context.Context, docID string, options ...Option) (rev string, err error) { 279 if db.err != nil { 280 return "", db.err 281 } 282 opts := multiOptions(options) 283 if r, ok := db.driverDB.(driver.RevGetter); ok { 284 endQuery, err := db.startQuery() 285 if err != nil { 286 return "", err 287 } 288 defer endQuery() 289 return r.GetRev(ctx, docID, opts) 290 } 291 row := db.Get(ctx, docID, opts) 292 var doc struct { 293 Rev string `json:"_rev"` 294 } 295 // These last two lines cannot be combined for GopherJS due to a bug. 296 // See https://github.com/gopherjs/gopherjs/issues/608 297 err = row.ScanDoc(&doc) 298 return doc.Rev, err 299 } 300 301 // CreateDoc creates a new doc with an auto-generated unique ID. The generated 302 // docID and new rev are returned. 303 func (db *DB) CreateDoc(ctx context.Context, doc interface{}, options ...Option) (docID, rev string, err error) { 304 if db.err != nil { 305 return "", "", db.err 306 } 307 if docCreator, ok := db.driverDB.(driver.DocCreator); ok { 308 endQuery, err := db.startQuery() 309 if err != nil { 310 return "", "", err 311 } 312 defer endQuery() 313 return docCreator.CreateDoc(ctx, doc, multiOptions(options)) 314 } 315 docID, ok := extractDocID(doc) 316 if !ok { 317 // TODO: Consider making uuid algorithm configurable 318 docID = uuid.NewString() 319 } 320 rev, err = db.Put(ctx, docID, doc, options...) 321 return docID, rev, err 322 } 323 324 // normalizeFromJSON unmarshals a []byte, json.RawMessage or io.Reader to a 325 // map[string]interface{}, or passed through any other types. 326 func normalizeFromJSON(i interface{}) (interface{}, error) { 327 switch t := i.(type) { 328 case json.Marshaler: 329 return t, nil 330 case io.Reader: 331 body, err := io.ReadAll(t) 332 if err != nil { 333 return nil, &internal.Error{Status: http.StatusBadRequest, Err: err} 334 } 335 return json.RawMessage(body), nil 336 default: 337 return i, nil 338 } 339 } 340 341 func extractDocID(i interface{}) (string, bool) { 342 if i == nil { 343 return "", false 344 } 345 var id string 346 var ok bool 347 switch t := i.(type) { 348 case map[string]interface{}: 349 id, ok = t["_id"].(string) 350 case map[string]string: 351 id, ok = t["_id"] 352 default: 353 data, err := json.Marshal(i) 354 if err != nil { 355 return "", false 356 } 357 var result struct { 358 ID string `json:"_id"` 359 } 360 if err := json.Unmarshal(data, &result); err != nil { 361 return "", false 362 } 363 id = result.ID 364 ok = result.ID != "" 365 } 366 if !ok { 367 return "", false 368 } 369 return id, true 370 } 371 372 // Put creates a new doc or updates an existing one, with the specified docID. 373 // If the document already exists, the current revision must be included in doc, 374 // with JSON key '_rev', otherwise a conflict will occur. The new rev is 375 // returned. 376 // 377 // doc may be one of: 378 // 379 // - A value to be marshaled to JSON. The resulting JSON structure must 380 // conform to CouchDB standards. 381 // - An [encoding/json.RawMessage] value containing a valid JSON document 382 // - An [io.Reader], from which a valid JSON document may be read. 383 func (db *DB) Put(ctx context.Context, docID string, doc interface{}, options ...Option) (rev string, err error) { 384 if db.err != nil { 385 return "", db.err 386 } 387 if docID == "" { 388 return "", missingArg("docID") 389 } 390 endQuery, err := db.startQuery() 391 if err != nil { 392 return "", err 393 } 394 defer endQuery() 395 i, err := normalizeFromJSON(doc) 396 if err != nil { 397 return "", err 398 } 399 return db.driverDB.Put(ctx, docID, i, multiOptions(options)) 400 } 401 402 // Delete marks the specified document as deleted. The revision may be provided 403 // via options, which takes priority over the rev argument. 404 func (db *DB) Delete(ctx context.Context, docID, rev string, options ...Option) (newRev string, err error) { 405 if db.err != nil { 406 return "", db.err 407 } 408 endQuery, err := db.startQuery() 409 if err != nil { 410 return "", err 411 } 412 defer endQuery() 413 if docID == "" { 414 return "", missingArg("docID") 415 } 416 opts := append(multiOptions{Rev(rev)}, options...) 417 return db.driverDB.Delete(ctx, docID, opts) 418 } 419 420 // Flush requests a flush of disk cache to disk or other permanent storage. 421 // 422 // See the [CouchDB documentation]. 423 // 424 // [CouchDB documentation]: http://docs.couchdb.org/en/2.0.0/api/database/compact.html#db-ensure-full-commit 425 func (db *DB) Flush(ctx context.Context) error { 426 if db.err != nil { 427 return db.err 428 } 429 endQuery, err := db.startQuery() 430 if err != nil { 431 return err 432 } 433 defer endQuery() 434 if flusher, ok := db.driverDB.(driver.Flusher); ok { 435 return flusher.Flush(ctx) 436 } 437 return &internal.Error{Status: http.StatusNotImplemented, Err: errors.New("kivik: flush not supported by driver")} 438 } 439 440 // DBStats contains database statistics.. 441 type DBStats struct { 442 // Name is the name of the database. 443 Name string `json:"db_name"` 444 // CompactRunning is true if the database is currently being compacted. 445 CompactRunning bool `json:"compact_running"` 446 // DocCount is the number of documents are currently stored in the database. 447 DocCount int64 `json:"doc_count"` 448 // DeletedCount is a count of documents which have been deleted from the 449 // database. 450 DeletedCount int64 `json:"doc_del_count"` 451 // UpdateSeq is the current update sequence for the database. 452 UpdateSeq string `json:"update_seq"` 453 // DiskSize is the number of bytes used on-disk to store the database. 454 DiskSize int64 `json:"disk_size"` 455 // ActiveSize is the number of bytes used on-disk to store active documents. 456 // If this number is lower than [DBStats.DiskSize], then compaction would 457 // free disk space. 458 ActiveSize int64 `json:"data_size"` 459 // ExternalSize is the size of the documents in the database, as represented 460 // as JSON, before compression. 461 ExternalSize int64 `json:"-"` 462 // Cluster reports the cluster replication configuration variables. 463 Cluster *ClusterConfig `json:"cluster,omitempty"` 464 // RawResponse is the raw response body returned by the server, useful if 465 // you need additional backend-specific information. 466 // 467 // For the format of this document, see the [CouchDB documentation]. 468 // 469 // [CouchDB documentation]: http://docs.couchdb.org/en/2.1.1/api/database/common.html#get--db 470 RawResponse json.RawMessage `json:"-"` 471 } 472 473 // ClusterConfig contains the cluster configuration for the database. 474 type ClusterConfig struct { 475 Replicas int `json:"n"` 476 Shards int `json:"q"` 477 ReadQuorum int `json:"r"` 478 WriteQuorum int `json:"w"` 479 } 480 481 // Stats returns database statistics. See the [CouchDB documentation]. 482 // 483 // [CouchDB documentation]: https://docs.couchdb.org/en/stable/api/database/common.html#get--db 484 func (db *DB) Stats(ctx context.Context) (*DBStats, error) { 485 if db.err != nil { 486 return nil, db.err 487 } 488 endQuery, err := db.startQuery() 489 if err != nil { 490 return nil, err 491 } 492 defer endQuery() 493 i, err := db.driverDB.Stats(ctx) 494 if err != nil { 495 return nil, err 496 } 497 return driverStats2kivikStats(i), nil 498 } 499 500 func driverStats2kivikStats(i *driver.DBStats) *DBStats { 501 var cluster *ClusterConfig 502 if i.Cluster != nil { 503 c := ClusterConfig(*i.Cluster) 504 cluster = &c 505 } 506 return &DBStats{ 507 Name: i.Name, 508 CompactRunning: i.CompactRunning, 509 DocCount: i.DocCount, 510 DeletedCount: i.DeletedCount, 511 UpdateSeq: i.UpdateSeq, 512 DiskSize: i.DiskSize, 513 ActiveSize: i.ActiveSize, 514 ExternalSize: i.ExternalSize, 515 Cluster: cluster, 516 RawResponse: i.RawResponse, 517 } 518 } 519 520 // Compact begins compaction of the database. Check the CompactRunning field 521 // returned by [DB.Stats] to see if the compaction has completed. 522 // 523 // See the [CouchDB documentation]. 524 // 525 // This method may return immediately, or may wait for the compaction to 526 // complete before returning, depending on the backend implementation. In 527 // particular, CouchDB triggers the compaction and returns immediately, whereas 528 // PouchDB waits until compaction has completed, before returning. 529 // 530 // [CouchDB documentation]: http://docs.couchdb.org/en/2.0.0/api/database/compact.html#db-compact 531 func (db *DB) Compact(ctx context.Context) error { 532 if db.err != nil { 533 return db.err 534 } 535 endQuery, err := db.startQuery() 536 if err != nil { 537 return err 538 } 539 defer endQuery() 540 return db.driverDB.Compact(ctx) 541 } 542 543 // CompactView compats the view indexes associated with the specified design 544 // document. 545 // 546 // See the [CouchDB documentation]. 547 // 548 // This method may return immediately, or may wait for the compaction to 549 // complete before returning, depending on the backend implementation. In 550 // particular, CouchDB triggers the compaction and returns immediately, whereas 551 // PouchDB waits until compaction has completed, before returning. 552 // 553 // [CouchDB documentation]: http://docs.couchdb.org/en/2.0.0/api/database/compact.html#db-compact-design-doc 554 func (db *DB) CompactView(ctx context.Context, ddocID string) error { 555 if db.err != nil { 556 return db.err 557 } 558 endQuery, err := db.startQuery() 559 if err != nil { 560 return err 561 } 562 defer endQuery() 563 return db.driverDB.CompactView(ctx, ddocID) 564 } 565 566 // ViewCleanup removes view index files that are no longer required as a result 567 // of changed views within design documents. 568 // 569 // See the [CouchDB documentation]. 570 // 571 // [CouchDB documentation]: http://docs.couchdb.org/en/2.0.0/api/database/compact.html#db-view-cleanup 572 func (db *DB) ViewCleanup(ctx context.Context) error { 573 if db.err != nil { 574 return db.err 575 } 576 endQuery, err := db.startQuery() 577 if err != nil { 578 return err 579 } 580 defer endQuery() 581 return db.driverDB.ViewCleanup(ctx) 582 } 583 584 // Security returns the database's security document. 585 // 586 // See the [CouchDB documentation]. 587 // 588 // [CouchDB documentation]: http://couchdb.readthedocs.io/en/latest/api/database/security.html#get--db-_security 589 func (db *DB) Security(ctx context.Context) (*Security, error) { 590 if db.err != nil { 591 return nil, db.err 592 } 593 secDB, ok := db.driverDB.(driver.SecurityDB) 594 if !ok { 595 return nil, errSecurityNotImplemented 596 } 597 endQuery, err := db.startQuery() 598 if err != nil { 599 return nil, err 600 } 601 defer endQuery() 602 s, err := secDB.Security(ctx) 603 if err != nil { 604 return nil, err 605 } 606 return &Security{ 607 Admins: Members(s.Admins), 608 Members: Members(s.Members), 609 Cloudant: s.Cloudant, 610 CouchdbAuthOnly: s.CouchdbAuthOnly, 611 }, err 612 } 613 614 // SetSecurity sets the database's security document. 615 // 616 // See the [CouchDB documentation]. 617 // 618 // [CouchDB documentation]: http://couchdb.readthedocs.io/en/latest/api/database/security.html#put--db-_security 619 func (db *DB) SetSecurity(ctx context.Context, security *Security) error { 620 if db.err != nil { 621 return db.err 622 } 623 secDB, ok := db.driverDB.(driver.SecurityDB) 624 if !ok { 625 return errSecurityNotImplemented 626 } 627 if security == nil { 628 return missingArg("security") 629 } 630 endQuery, err := db.startQuery() 631 if err != nil { 632 return err 633 } 634 defer endQuery() 635 sec := &driver.Security{ 636 Admins: driver.Members(security.Admins), 637 Members: driver.Members(security.Members), 638 Cloudant: security.Cloudant, 639 CouchdbAuthOnly: security.CouchdbAuthOnly, 640 } 641 return secDB.SetSecurity(ctx, sec) 642 } 643 644 // Copy copies the source document to a new document with an ID of targetID. If 645 // the database backend does not support COPY directly, the operation will be 646 // emulated with a Get followed by Put. The target will be an exact copy of the 647 // source, with only the ID and revision changed. 648 // 649 // See the [CouchDB documentation]: 650 // 651 // [CouchDB documentation]: http://docs.couchdb.org/en/2.0.0/api/document/common.html#copy--db-docid 652 func (db *DB) Copy(ctx context.Context, targetID, sourceID string, options ...Option) (targetRev string, err error) { 653 if db.err != nil { 654 return "", db.err 655 } 656 if targetID == "" { 657 return "", missingArg("targetID") 658 } 659 if sourceID == "" { 660 return "", missingArg("sourceID") 661 } 662 opts := multiOptions(options) 663 if copier, ok := db.driverDB.(driver.Copier); ok { 664 endQuery, err := db.startQuery() 665 if err != nil { 666 return "", err 667 } 668 defer endQuery() 669 return copier.Copy(ctx, targetID, sourceID, opts) 670 } 671 var doc map[string]interface{} 672 if err = db.Get(ctx, sourceID, options...).ScanDoc(&doc); err != nil { 673 return "", err 674 } 675 delete(doc, "_rev") 676 doc["_id"] = targetID 677 opts2 := map[string]interface{}{} 678 opts.Apply(opts2) 679 delete(opts2, "rev") // rev has a completely different meaning for Copy and Put 680 return db.Put(ctx, targetID, doc, Params(opts2)) 681 } 682 683 // PutAttachment uploads the supplied content as an attachment to the specified 684 // document. 685 func (db *DB) PutAttachment(ctx context.Context, docID string, att *Attachment, options ...Option) (newRev string, err error) { 686 if db.err != nil { 687 return "", db.err 688 } 689 if docID == "" { 690 return "", missingArg("docID") 691 } 692 if e := att.validate(); e != nil { 693 return "", e 694 } 695 endQuery, err := db.startQuery() 696 if err != nil { 697 return "", err 698 } 699 defer endQuery() 700 a := driver.Attachment(*att) 701 return db.driverDB.PutAttachment(ctx, docID, &a, multiOptions(options)) 702 } 703 704 // GetAttachment returns a file attachment associated with the document. 705 func (db *DB) GetAttachment(ctx context.Context, docID, filename string, options ...Option) (*Attachment, error) { 706 if db.err != nil { 707 return nil, db.err 708 } 709 endQuery, err := db.startQuery() 710 if err != nil { 711 return nil, err 712 } 713 defer endQuery() 714 if docID == "" { 715 return nil, missingArg("docID") 716 } 717 if filename == "" { 718 return nil, missingArg("filename") 719 } 720 att, err := db.driverDB.GetAttachment(ctx, docID, filename, multiOptions(options)) 721 if err != nil { 722 return nil, err 723 } 724 a := Attachment(*att) 725 return &a, nil 726 } 727 728 type nilContentReader struct{} 729 730 var _ io.ReadCloser = &nilContentReader{} 731 732 func (c nilContentReader) Read(_ []byte) (int, error) { return 0, io.EOF } 733 func (c nilContentReader) Close() error { return nil } 734 735 var nilContent = nilContentReader{} 736 737 // GetAttachmentMeta returns meta data about an attachment. The attachment 738 // content returned will be empty. 739 func (db *DB) GetAttachmentMeta(ctx context.Context, docID, filename string, options ...Option) (*Attachment, error) { 740 if db.err != nil { 741 return nil, db.err 742 } 743 if docID == "" { 744 return nil, missingArg("docID") 745 } 746 if filename == "" { 747 return nil, missingArg("filename") 748 } 749 var att *Attachment 750 if metaer, ok := db.driverDB.(driver.AttachmentMetaGetter); ok { 751 endQuery, err := db.startQuery() 752 if err != nil { 753 return nil, err 754 } 755 defer endQuery() 756 a, err := metaer.GetAttachmentMeta(ctx, docID, filename, multiOptions(options)) 757 if err != nil { 758 return nil, err 759 } 760 att = new(Attachment) 761 *att = Attachment(*a) 762 } else { 763 var err error 764 att, err = db.GetAttachment(ctx, docID, filename, options...) 765 if err != nil { 766 return nil, err 767 } 768 } 769 if att.Content != nil { 770 _ = att.Content.Close() // Ensure this is closed 771 } 772 att.Content = nilContent 773 return att, nil 774 } 775 776 // DeleteAttachment deletes an attachment from a document, returning the 777 // document's new revision. The revision may be provided via options, which 778 // takes priority over the rev argument. 779 func (db *DB) DeleteAttachment(ctx context.Context, docID, rev, filename string, options ...Option) (newRev string, err error) { 780 if db.err != nil { 781 return "", db.err 782 } 783 endQuery, err := db.startQuery() 784 if err != nil { 785 return "", err 786 } 787 defer endQuery() 788 if docID == "" { 789 return "", missingArg("docID") 790 } 791 if filename == "" { 792 return "", missingArg("filename") 793 } 794 opts := append(multiOptions{Rev(rev)}, options...) 795 return db.driverDB.DeleteAttachment(ctx, docID, filename, opts) 796 } 797 798 // PurgeResult is the result of a purge request. 799 type PurgeResult struct { 800 // Seq is the purge sequence number. 801 Seq int64 `json:"purge_seq"` 802 // Purged is a map of document ids to revisions, indicated the 803 // document/revision pairs that were successfully purged. 804 Purged map[string][]string `json:"purged"` 805 } 806 807 // Purge permanently removes the reference to deleted documents from the 808 // database. Normal deletion only marks the document with the key/value pair 809 // `_deleted=true`, to ensure proper replication of deleted documents. By 810 // using Purge, the document can be completely removed. But note that this 811 // operation is not replication safe, so great care must be taken when using 812 // Purge, and this should only be used as a last resort. 813 // 814 // Purge expects as input a map with document ID as key, and slice of 815 // revisions as value. 816 func (db *DB) Purge(ctx context.Context, docRevMap map[string][]string) (*PurgeResult, error) { 817 if db.err != nil { 818 return nil, db.err 819 } 820 endQuery, err := db.startQuery() 821 if err != nil { 822 return nil, err 823 } 824 defer endQuery() 825 if purger, ok := db.driverDB.(driver.Purger); ok { 826 res, err := purger.Purge(ctx, docRevMap) 827 if err != nil { 828 return nil, err 829 } 830 r := PurgeResult(*res) 831 return &r, nil 832 } 833 return nil, &internal.Error{Status: http.StatusNotImplemented, Message: "kivik: purge not supported by driver"} 834 } 835 836 // BulkGetReference is a reference to a document given to pass to [DB.BulkGet]. 837 type BulkGetReference struct { 838 ID string `json:"id"` 839 Rev string `json:"rev,omitempty"` 840 AttsSince string `json:"atts_since,omitempty"` 841 } 842 843 // BulkGet can be called to query several documents in bulk. It is well suited 844 // for fetching a specific revision of documents, as replicators do for example, 845 // or for getting revision history. 846 // 847 // See the [CouchDB documentation]. 848 // 849 // [CouchDB documentation]: http://docs.couchdb.org/en/stable/api/database/bulk-api.html#db-bulk-get 850 func (db *DB) BulkGet(ctx context.Context, docs []BulkGetReference, options ...Option) *ResultSet { 851 if db.err != nil { 852 return &ResultSet{iter: errIterator(db.err)} 853 } 854 bulkGetter, ok := db.driverDB.(driver.BulkGetter) 855 if !ok { 856 return &ResultSet{iter: errIterator(&internal.Error{Status: http.StatusNotImplemented, Message: "kivik: bulk get not supported by driver"})} 857 } 858 859 endQuery, err := db.startQuery() 860 if err != nil { 861 return &ResultSet{iter: errIterator(err)} 862 } 863 refs := make([]driver.BulkGetReference, len(docs)) 864 for i, ref := range docs { 865 refs[i] = driver.BulkGetReference(ref) 866 } 867 rowsi, err := bulkGetter.BulkGet(ctx, refs, multiOptions(options)) 868 if err != nil { 869 endQuery() 870 return &ResultSet{iter: errIterator(err)} 871 } 872 return newResultSet(ctx, endQuery, rowsi) 873 } 874 875 // Close cleans up any resources used by the DB. Close is safe to call 876 // concurrently with other DB operations and will block until all other DB 877 // operations finish. After calling Close, any other DB operations will return 878 // [ErrDatabaseClosed]. 879 func (db *DB) Close() error { 880 if db.err != nil { 881 return db.err 882 } 883 db.mu.Lock() 884 db.closed = true 885 db.mu.Unlock() 886 db.wg.Wait() 887 return db.driverDB.Close() 888 } 889 890 // RevDiff represents a rev diff for a single document, as returned by the 891 // [DB.RevsDiff] method. 892 type RevDiff struct { 893 Missing []string `json:"missing,omitempty"` 894 PossibleAncestors []string `json:"possible_ancestors,omitempty"` 895 } 896 897 // Diffs is a collection of [RevDiff] values as returned by [DB.RevsDiff]. The 898 // map key is the document ID. 899 type Diffs map[string]RevDiff 900 901 // RevsDiff returns the subset of document/revision IDs that do not correspond 902 // to revisions stored in the database. This is used by the replication 903 // protocol, and is normally never needed otherwise. revMap must marshal to the 904 // [expected format]. 905 // 906 // Use [ResultSet.ID] to return the current document ID, and 907 // [ResultSet.ScanValue] to access the full JSON value. The [RevsDiff] type 908 // matches this format and is provided as a convenience for unmarshaling. 909 // 910 // { 911 // "missing": ["rev1",...], 912 // "possible_ancestors": ["revA",...] 913 // } 914 // 915 // [expected format]: http://docs.couchdb.org/en/stable/api/database/misc.html#db-revs-diff 916 func (db *DB) RevsDiff(ctx context.Context, revMap interface{}) *ResultSet { 917 if db.err != nil { 918 return &ResultSet{iter: errIterator(db.err)} 919 } 920 if rd, ok := db.driverDB.(driver.RevsDiffer); ok { 921 endQuery, err := db.startQuery() 922 if err != nil { 923 return &ResultSet{iter: errIterator(err)} 924 } 925 rowsi, err := rd.RevsDiff(ctx, revMap) 926 if err != nil { 927 endQuery() 928 return &ResultSet{iter: errIterator(err)} 929 } 930 return newResultSet(ctx, endQuery, rowsi) 931 } 932 return &ResultSet{iter: errIterator(&internal.Error{Status: http.StatusNotImplemented, Message: "kivik: _revs_diff not supported by driver"})} 933 } 934 935 // PartitionStats contains partition statistics. 936 type PartitionStats struct { 937 DBName string 938 DocCount int64 939 DeletedDocCount int64 940 Partition string 941 ActiveSize int64 942 ExternalSize int64 943 RawResponse json.RawMessage 944 } 945 946 // PartitionStats returns statistics about the named partition. 947 // 948 // See the [CouchDB documentation]. 949 // 950 // [CouchDB documentation]: https://docs.couchdb.org/en/stable/api/partitioned-dbs.html#db-partition-partition 951 func (db *DB) PartitionStats(ctx context.Context, name string) (*PartitionStats, error) { 952 if db.err != nil { 953 return nil, db.err 954 } 955 endQuery, err := db.startQuery() 956 if err != nil { 957 return nil, err 958 } 959 defer endQuery() 960 if pdb, ok := db.driverDB.(driver.PartitionedDB); ok { 961 stats, err := pdb.PartitionStats(ctx, name) 962 if err != nil { 963 return nil, err 964 } 965 s := PartitionStats(*stats) 966 return &s, nil 967 } 968 return nil, &internal.Error{Status: http.StatusNotImplemented, Message: "kivik: partitions not supported by driver"} 969 }