github.com/cayleygraph/cayley@v0.7.7/graph/nosql/quadstore.go (about) 1 // Copyright 2014 The Cayley Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package nosql 16 17 import ( 18 "context" 19 "encoding/base64" 20 "fmt" 21 "time" 22 23 "github.com/cayleygraph/cayley/clog" 24 "github.com/cayleygraph/cayley/graph" 25 "github.com/cayleygraph/cayley/graph/iterator" 26 "github.com/cayleygraph/cayley/internal/lru" 27 "github.com/cayleygraph/quad" 28 "github.com/cayleygraph/quad/pquads" 29 "github.com/hidal-go/hidalgo/legacy/nosql" 30 ) 31 32 const DefaultDBName = "cayley" 33 34 type Registration struct { 35 NewFunc NewFunc 36 InitFunc InitFunc 37 IsPersistent bool 38 Traits 39 } 40 41 type Traits = nosql.Traits 42 43 func init() { 44 for _, reg := range nosql.List() { 45 Register(reg.Name, Registration{ 46 NewFunc: func(addr string, options graph.Options) (nosql.Database, error) { 47 return reg.Open(addr, DefaultDBName, nosql.Options(options)) 48 }, 49 InitFunc: func(addr string, options graph.Options) (nosql.Database, error) { 50 return reg.New(addr, DefaultDBName, nosql.Options(options)) 51 }, 52 IsPersistent: !reg.Volatile, Traits: reg.Traits, 53 }) 54 } 55 } 56 57 type InitFunc func(string, graph.Options) (nosql.Database, error) 58 type NewFunc func(string, graph.Options) (nosql.Database, error) 59 60 func Register(name string, r Registration) { 61 graph.RegisterQuadStore(name, graph.QuadStoreRegistration{ 62 InitFunc: func(addr string, opt graph.Options) error { 63 if !r.IsPersistent { 64 return nil 65 } 66 db, err := r.InitFunc(addr, opt) 67 if err != nil { 68 return err 69 } 70 defer db.Close() 71 if err = Init(db, opt); err != nil { 72 return err 73 } 74 return db.Close() 75 }, 76 NewFunc: func(addr string, opt graph.Options) (graph.QuadStore, error) { 77 db, err := r.NewFunc(addr, opt) 78 if err != nil { 79 return nil, err 80 } 81 if !r.IsPersistent { 82 if err = Init(db, opt); err != nil { 83 db.Close() 84 return nil, err 85 } 86 } 87 nopt := r.Traits 88 qs, err := NewQuadStore(db, &nopt, opt) 89 if err != nil { 90 return nil, err 91 } 92 return qs, nil 93 }, 94 IsPersistent: r.IsPersistent, 95 }) 96 } 97 98 func Init(db nosql.Database, opt graph.Options) error { 99 return ensureIndexes(context.TODO(), db) 100 } 101 102 func NewQuadStore(db nosql.Database, nopt *Traits, opt graph.Options) (*QuadStore, error) { 103 if err := ensureIndexes(context.TODO(), db); err != nil { 104 return nil, err 105 } 106 qs := &QuadStore{ 107 db: db, 108 ids: lru.New(1 << 16), 109 sizes: lru.New(1 << 16), 110 } 111 if nopt != nil { 112 qs.opt = *nopt 113 } 114 return qs, nil 115 } 116 117 type NodeHash string 118 119 func (NodeHash) IsNode() bool { return false } 120 func (v NodeHash) Key() interface{} { return v } 121 func (v NodeHash) key() nosql.Key { return nosql.Key{string(v)} } 122 123 type QuadHash [4]string 124 125 func (QuadHash) IsNode() bool { return false } 126 func (v QuadHash) Key() interface{} { return v } 127 128 func (h QuadHash) Get(d quad.Direction) string { 129 var ind int 130 switch d { 131 case quad.Subject: 132 ind = 0 133 case quad.Predicate: 134 ind = 1 135 case quad.Object: 136 ind = 2 137 case quad.Label: 138 ind = 3 139 } 140 return h[ind] 141 } 142 143 const ( 144 colLog = "log" 145 colNodes = "nodes" 146 colQuads = "quads" 147 148 fldLogID = "id" 149 150 fldSubject = "subject" 151 fldPredicate = "predicate" 152 fldObject = "object" 153 fldLabel = "label" 154 fldQuadAdded = "added" 155 fldQuadDeleted = "deleted" 156 157 fldHash = "hash" 158 fldValue = "value" 159 fldSize = "refs" 160 161 fldValData = "str" 162 fldIRI = "iri" 163 fldBNode = "bnode" 164 fldType = "type" 165 fldLang = "lang" 166 fldValInt = "int" 167 fldValStrInt = "int_str" 168 fldValFloat = "float" 169 fldValBool = "bool" 170 fldValTime = "ts" 171 fldValPb = "pb" 172 ) 173 174 type QuadStore struct { 175 db nosql.Database 176 ids *lru.Cache 177 sizes *lru.Cache 178 opt Traits 179 } 180 181 func ensureIndexes(ctx context.Context, db nosql.Database) error { 182 err := db.EnsureIndex(ctx, colLog, nosql.Index{ 183 Fields: []string{fldLogID}, 184 Type: nosql.StringExact, 185 }, nil) 186 if err != nil { 187 return err 188 } 189 err = db.EnsureIndex(ctx, colNodes, nosql.Index{ 190 Fields: []string{fldHash}, 191 Type: nosql.StringExact, 192 }, nil) 193 if err != nil { 194 return err 195 } 196 err = db.EnsureIndex(ctx, colQuads, nosql.Index{ 197 Fields: []string{ 198 fldSubject, 199 fldPredicate, 200 fldObject, 201 fldLabel, 202 }, 203 Type: nosql.StringExact, 204 }, []nosql.Index{ 205 {Fields: []string{fldSubject}, Type: nosql.StringExact}, 206 {Fields: []string{fldPredicate}, Type: nosql.StringExact}, 207 {Fields: []string{fldObject}, Type: nosql.StringExact}, 208 {Fields: []string{fldLabel}, Type: nosql.StringExact}, 209 }) 210 if err != nil { 211 return err 212 } 213 return nil 214 } 215 216 func getKeyForQuad(t quad.Quad) nosql.Key { 217 return nosql.Key{ 218 hashOf(t.Subject), 219 hashOf(t.Predicate), 220 hashOf(t.Object), 221 hashOf(t.Label), 222 } 223 } 224 225 func hashOf(s quad.Value) string { 226 if s == nil { 227 return "" 228 } 229 h := quad.HashOf(s) 230 return base64.StdEncoding.EncodeToString(h) 231 } 232 233 func (qs *QuadStore) nameToKey(name quad.Value) nosql.Key { 234 node := qs.hashOf(name) 235 return node.key() 236 } 237 238 func (qs *QuadStore) updateNodeBy(ctx context.Context, key nosql.Key, name quad.Value, inc int) error { 239 if inc == 0 { 240 return nil 241 } 242 d := toDocumentValue(&qs.opt, name) 243 err := qs.db.Update(colNodes, key).Upsert(d).Inc(fldSize, inc).Do(ctx) 244 if err != nil { 245 return fmt.Errorf("error updating node: %v", err) 246 } 247 return nil 248 } 249 250 func (qs *QuadStore) cleanupNodes(ctx context.Context, keys []nosql.Key) error { 251 err := qs.db.Delete(colNodes).Keys(keys...).WithFields(nosql.FieldFilter{ 252 Path: []string{fldSize}, 253 Filter: nosql.Equal, 254 Value: nosql.Int(0), 255 }).Do(ctx) 256 if err != nil { 257 err = fmt.Errorf("error cleaning up nodes: %v", err) 258 } 259 return err 260 } 261 262 func (qs *QuadStore) updateQuad(ctx context.Context, q quad.Quad, proc graph.Procedure) error { 263 var setname string 264 if proc == graph.Add { 265 setname = fldQuadAdded 266 } else if proc == graph.Delete { 267 setname = fldQuadDeleted 268 } 269 doc := nosql.Document{ 270 fldSubject: nosql.String(hashOf(q.Subject)), 271 fldPredicate: nosql.String(hashOf(q.Predicate)), 272 fldObject: nosql.String(hashOf(q.Object)), 273 } 274 if l := hashOf(q.Label); l != "" { 275 doc[fldLabel] = nosql.String(l) 276 } 277 err := qs.db.Update(colQuads, getKeyForQuad(q)).Upsert(doc). 278 Inc(setname, 1).Do(ctx) 279 if err != nil { 280 err = fmt.Errorf("quad update failed: %v", err) 281 } 282 return err 283 } 284 285 func checkQuadValid(q nosql.Document) bool { 286 added, _ := asInt(q[fldQuadAdded]) 287 deleted, _ := asInt(q[fldQuadDeleted]) 288 return added > deleted 289 } 290 291 func (qs *QuadStore) checkValidQuad(ctx context.Context, key nosql.Key) (bool, error) { 292 q, err := qs.db.FindByKey(ctx, colQuads, key) 293 if err == nosql.ErrNotFound { 294 return false, nil 295 } 296 if err != nil { 297 err = fmt.Errorf("error checking quad validity: %v", err) 298 return false, err 299 } 300 return checkQuadValid(q), nil 301 } 302 303 func (qs *QuadStore) batchInsert(col string) nosql.DocWriter { 304 return nosql.BatchInsert(qs.db, col) 305 } 306 307 func (qs *QuadStore) appendLog(ctx context.Context, deltas []graph.Delta) ([]nosql.Key, error) { 308 w := qs.batchInsert(colLog) 309 defer w.Close() 310 for _, d := range deltas { 311 data, err := pquads.MakeQuad(d.Quad).Marshal() 312 if err != nil { 313 return w.Keys(), err 314 } 315 var action string 316 if d.Action == graph.Add { 317 action = "AddQuadPQ" 318 } else { 319 action = "DeleteQuadPQ" 320 } 321 err = w.WriteDoc(ctx, nil, nosql.Document{ 322 "op": nosql.String(action), 323 "data": nosql.Bytes(data), 324 "ts": nosql.Time(time.Now().UTC()), 325 }) 326 if err != nil { 327 return w.Keys(), err 328 } 329 } 330 err := w.Flush(ctx) 331 return w.Keys(), err 332 } 333 334 func (qs *QuadStore) NewQuadWriter() (quad.WriteCloser, error) { 335 return &quadWriter{qs: qs}, nil 336 } 337 338 type quadWriter struct { 339 qs *QuadStore 340 deltas []graph.Delta 341 } 342 343 func (w *quadWriter) WriteQuad(q quad.Quad) error { 344 _, err := w.WriteQuads([]quad.Quad{q}) 345 return err 346 } 347 348 func (w *quadWriter) WriteQuads(buf []quad.Quad) (int, error) { 349 // TODO(dennwc): write an optimized implementation 350 w.deltas = w.deltas[:0] 351 if cap(w.deltas) < len(buf) { 352 w.deltas = make([]graph.Delta, 0, len(buf)) 353 } 354 for _, q := range buf { 355 w.deltas = append(w.deltas, graph.Delta{ 356 Quad: q, Action: graph.Add, 357 }) 358 } 359 err := w.qs.ApplyDeltas(w.deltas, graph.IgnoreOpts{ 360 IgnoreDup: true, 361 }) 362 w.deltas = w.deltas[:0] 363 if err != nil { 364 return 0, err 365 } 366 return len(buf), nil 367 } 368 369 func (w *quadWriter) Close() error { 370 w.deltas = nil 371 return nil 372 } 373 374 func (qs *QuadStore) ApplyDeltas(deltas []graph.Delta, ignoreOpts graph.IgnoreOpts) error { 375 ctx := context.TODO() 376 ids := make(map[quad.Value]int) 377 378 var validDeltas []graph.Delta 379 if ignoreOpts.IgnoreDup || ignoreOpts.IgnoreMissing { 380 validDeltas = make([]graph.Delta, 0, len(deltas)) 381 } 382 // Pre-check the existence condition. 383 for _, d := range deltas { 384 if d.Action != graph.Add && d.Action != graph.Delete { 385 return &graph.DeltaError{Delta: d, Err: graph.ErrInvalidAction} 386 } 387 valid, err := qs.checkValidQuad(ctx, getKeyForQuad(d.Quad)) 388 if err != nil { 389 return &graph.DeltaError{Delta: d, Err: err} 390 } 391 switch d.Action { 392 case graph.Add: 393 if valid { 394 if ignoreOpts.IgnoreDup { 395 continue 396 } else { 397 return &graph.DeltaError{Delta: d, Err: graph.ErrQuadExists} 398 } 399 } 400 case graph.Delete: 401 if !valid { 402 if ignoreOpts.IgnoreMissing { 403 continue 404 } else { 405 return &graph.DeltaError{Delta: d, Err: graph.ErrQuadNotExist} 406 } 407 } 408 } 409 if validDeltas != nil { 410 validDeltas = append(validDeltas, d) 411 } 412 var dn int 413 if d.Action == graph.Add { 414 dn = 1 415 } else { 416 dn = -1 417 } 418 ids[d.Quad.Subject] += dn 419 ids[d.Quad.Object] += dn 420 ids[d.Quad.Predicate] += dn 421 if d.Quad.Label != nil { 422 ids[d.Quad.Label] += dn 423 } 424 } 425 if validDeltas != nil { 426 deltas = validDeltas 427 } 428 if oids, err := qs.appendLog(ctx, deltas); err != nil { 429 if i := len(oids); i < len(deltas) { 430 return &graph.DeltaError{Delta: deltas[i], Err: err} 431 } 432 return &graph.DeltaError{Err: err} 433 } 434 // make sure to create all nodes before writing any quads 435 // concurrent reads may observe broken quads in other case 436 var gc []nosql.Key 437 for name, dn := range ids { 438 key := qs.nameToKey(name) 439 err := qs.updateNodeBy(ctx, key, name, dn) 440 if err != nil { 441 return err 442 } 443 if dn < 0 { 444 gc = append(gc, key) 445 } 446 } 447 // gc nodes that has negative ref counter 448 if err := qs.cleanupNodes(ctx, gc); err != nil { 449 return err 450 } 451 for _, d := range deltas { 452 err := qs.updateQuad(ctx, d.Quad, d.Action) 453 if err != nil { 454 return &graph.DeltaError{Delta: d, Err: err} 455 } 456 } 457 return nil 458 } 459 460 func toDocumentValue(opt *Traits, v quad.Value) nosql.Document { 461 if v == nil { 462 return nil 463 } 464 var doc nosql.Document 465 encPb := func() { 466 qv := pquads.MakeValue(v) 467 data, err := qv.Marshal() 468 if err != nil { 469 panic(err) 470 } 471 doc[fldValPb] = nosql.Bytes(data) 472 } 473 switch d := v.(type) { 474 case quad.String: 475 doc = nosql.Document{fldValData: nosql.String(d)} 476 case quad.IRI: 477 doc = nosql.Document{fldValData: nosql.String(d), fldIRI: nosql.Bool(true)} 478 case quad.BNode: 479 doc = nosql.Document{fldValData: nosql.String(d), fldBNode: nosql.Bool(true)} 480 case quad.TypedString: 481 doc = nosql.Document{fldValData: nosql.String(d.Value), fldType: nosql.String(d.Type)} 482 case quad.LangString: 483 doc = nosql.Document{fldValData: nosql.String(d.Value), fldLang: nosql.String(d.Lang)} 484 case quad.Int: 485 doc = nosql.Document{fldValInt: nosql.Int(d)} 486 if opt.Number32 { 487 // store sortable string representation for range queries 488 doc[fldValStrInt] = nosql.String(itos(int64(d))) 489 encPb() 490 } 491 case quad.Float: 492 doc = nosql.Document{fldValFloat: nosql.Float(d)} 493 if opt.Number32 { 494 encPb() 495 } 496 case quad.Bool: 497 doc = nosql.Document{fldValBool: nosql.Bool(d)} 498 case quad.Time: 499 doc = nosql.Document{fldValTime: nosql.Time(time.Time(d).UTC())} 500 default: 501 encPb() 502 } 503 return nosql.Document{fldValue: doc} 504 } 505 506 func asInt(v nosql.Value) (nosql.Int, error) { 507 var vi nosql.Int 508 switch v := v.(type) { 509 case nosql.Int: 510 vi = v 511 case nosql.Float: 512 vi = nosql.Int(v) 513 default: 514 return 0, fmt.Errorf("unexpected type for int field: %T", v) 515 } 516 return vi, nil 517 } 518 519 func toQuadValue(opt *Traits, d nosql.Document) (quad.Value, error) { 520 if len(d) == 0 { 521 return nil, nil 522 } 523 var err error 524 // prefer protobuf representation 525 if v, ok := d[fldValPb]; ok { 526 var b []byte 527 switch v := v.(type) { 528 case nosql.String: 529 b, err = base64.StdEncoding.DecodeString(string(v)) 530 case nosql.Bytes: 531 b = []byte(v) 532 default: 533 err = fmt.Errorf("unexpected type for pb field: %T", v) 534 } 535 if err != nil { 536 return nil, err 537 } 538 var p pquads.Value 539 if err := p.Unmarshal(b); err != nil { 540 return nil, fmt.Errorf("couldn't decode value: %v", err) 541 } 542 return p.ToNative(), nil 543 } else if v, ok := d[fldValInt]; ok { 544 if opt.Number32 { 545 // parse from string, so we are confident that we will get exactly the same value 546 if vs, ok := d[fldValStrInt].(nosql.String); ok { 547 iv := quad.Int(stoi(string(vs))) 548 return iv, nil 549 } 550 } 551 vi, err := asInt(v) 552 if err != nil { 553 return nil, err 554 } 555 return quad.Int(vi), nil 556 } else if v, ok := d[fldValFloat]; ok { 557 var vf quad.Float 558 switch v := v.(type) { 559 case nosql.Int: 560 vf = quad.Float(v) 561 case nosql.Float: 562 vf = quad.Float(v) 563 default: 564 return nil, fmt.Errorf("unexpected type for float field: %T", v) 565 } 566 return vf, nil 567 } else if v, ok := d[fldValBool]; ok { 568 var vb quad.Bool 569 switch v := v.(type) { 570 case nosql.Bool: 571 vb = quad.Bool(v) 572 default: 573 return nil, fmt.Errorf("unexpected type for bool field: %T", v) 574 } 575 return vb, nil 576 } else if v, ok := d[fldValTime]; ok { 577 var vt quad.Time 578 switch v := v.(type) { 579 case nosql.Time: 580 vt = quad.Time(v) 581 case nosql.String: 582 var t time.Time 583 if err := t.UnmarshalJSON([]byte(`"` + string(v) + `"`)); err != nil { 584 return nil, err 585 } 586 vt = quad.Time(t) 587 default: 588 return nil, fmt.Errorf("unexpected type for bool field: %T", v) 589 } 590 return vt, nil 591 } 592 vs, ok := d[fldValData].(nosql.String) 593 if !ok { 594 return nil, fmt.Errorf("unknown value format: %T", d[fldValData]) 595 } 596 if len(d) == 1 { 597 return quad.String(vs), nil 598 } 599 if ok, _ := d[fldIRI].(nosql.Bool); ok { 600 return quad.IRI(vs), nil 601 } else if ok, _ := d[fldBNode].(nosql.Bool); ok { 602 return quad.BNode(vs), nil 603 } else if typ, ok := d[fldType].(nosql.String); ok { 604 return quad.TypedString{Value: quad.String(vs), Type: quad.IRI(typ)}, nil 605 } else if typ, ok := d[fldLang].(nosql.String); ok { 606 return quad.LangString{Value: quad.String(vs), Lang: string(typ)}, nil 607 } 608 return nil, fmt.Errorf("unsupported value: %#v", d) 609 } 610 611 func (qs *QuadStore) Quad(val graph.Ref) quad.Quad { 612 h := val.(QuadHash) 613 return quad.Quad{ 614 Subject: qs.NameOf(NodeHash(h.Get(quad.Subject))), 615 Predicate: qs.NameOf(NodeHash(h.Get(quad.Predicate))), 616 Object: qs.NameOf(NodeHash(h.Get(quad.Object))), 617 Label: qs.NameOf(NodeHash(h.Get(quad.Label))), 618 } 619 } 620 621 func (qs *QuadStore) QuadIterator(d quad.Direction, val graph.Ref) graph.Iterator { 622 h, ok := val.(NodeHash) 623 if !ok { 624 return iterator.NewNull() 625 } 626 return NewLinksToIterator(qs, "quads", []Linkage{{Dir: d, Val: h}}) 627 } 628 629 func (qs *QuadStore) QuadIteratorSize(ctx context.Context, d quad.Direction, v graph.Ref) (graph.Size, error) { 630 h, ok := v.(NodeHash) 631 if !ok { 632 return graph.Size{Size: 0, Exact: true}, nil 633 } 634 sz, err := qs.getSize("quads", linkageToFilters([]Linkage{{Dir: d, Val: h}})) 635 if err != nil { 636 return graph.Size{}, err 637 } 638 return graph.Size{ 639 Size: sz, 640 Exact: true, 641 }, nil 642 } 643 644 func (qs *QuadStore) NodesAllIterator() graph.Iterator { 645 return NewIterator(qs, "nodes") 646 } 647 648 func (qs *QuadStore) QuadsAllIterator() graph.Iterator { 649 return NewIterator(qs, "quads") 650 } 651 652 func (qs *QuadStore) hashOf(s quad.Value) NodeHash { 653 return NodeHash(hashOf(s)) 654 } 655 656 func (qs *QuadStore) ValueOf(s quad.Value) graph.Ref { 657 if s == nil { 658 return nil 659 } 660 return qs.hashOf(s) 661 } 662 663 func (qs *QuadStore) NameOf(v graph.Ref) quad.Value { 664 if v == nil { 665 return nil 666 } else if v, ok := v.(graph.PreFetchedValue); ok { 667 return v.NameOf() 668 } 669 hash := v.(NodeHash) 670 if hash == "" { 671 return nil 672 } 673 if val, ok := qs.ids.Get(string(hash)); ok { 674 return val.(quad.Value) 675 } 676 nd, err := qs.db.FindByKey(context.TODO(), colNodes, hash.key()) 677 if err != nil { 678 clog.Errorf("couldn't retrieve node %v: %v", v, err) 679 return nil 680 } 681 dv, _ := nd[fldValue].(nosql.Document) 682 qv, err := toQuadValue(&qs.opt, dv) 683 if err != nil { 684 clog.Errorf("couldn't convert node %v: %v", v, err) 685 return nil 686 } 687 if id, _ := nd[fldHash].(nosql.String); id == nosql.String(hash) && qv != nil { 688 qs.ids.Put(string(hash), qv) 689 } 690 return qv 691 } 692 693 func (qs *QuadStore) Stats(ctx context.Context, exact bool) (graph.Stats, error) { 694 // TODO(barakmich): Make size real; store it in the log, and retrieve it. 695 nodes, err := qs.db.Query(colNodes).Count(ctx) 696 if err != nil { 697 return graph.Stats{}, err 698 } 699 quads, err := qs.db.Query(colQuads).Count(ctx) 700 if err != nil { 701 return graph.Stats{}, err 702 } 703 return graph.Stats{ 704 Nodes: graph.Size{ 705 Size: nodes, 706 Exact: true, 707 }, 708 Quads: graph.Size{ 709 Size: quads, 710 Exact: true, 711 }, 712 }, nil 713 } 714 715 func (qs *QuadStore) Size() int64 { 716 count, err := qs.db.Query(colQuads).Count(context.TODO()) 717 if err != nil { 718 clog.Errorf("%v", err) 719 return 0 720 } 721 return count 722 } 723 724 func (qs *QuadStore) Close() error { 725 return qs.db.Close() 726 } 727 728 func (qs *QuadStore) QuadDirection(in graph.Ref, d quad.Direction) graph.Ref { 729 return NodeHash(in.(QuadHash).Get(d)) 730 } 731 732 func (qs *QuadStore) getSize(col string, constraints []nosql.FieldFilter) (int64, error) { 733 cacheKey := "" 734 for _, c := range constraints { // FIXME 735 cacheKey += fmt.Sprint(c.Path, c.Filter, c.Value) 736 } 737 key := col + cacheKey 738 if val, ok := qs.sizes.Get(key); ok { 739 return val.(int64), nil 740 } 741 q := qs.db.Query(col) 742 if len(constraints) != 0 { 743 q = q.WithFields(constraints...) 744 } 745 size, err := q.Count(context.TODO()) 746 if err != nil { 747 clog.Errorf("error getting size for iterator: %v", err) 748 return -1, err 749 } 750 qs.sizes.Put(key, int64(size)) 751 return int64(size), nil 752 }