github.com/cayleygraph/cayley@v0.7.7/graph/sql/quadstore.go (about) 1 package sql 2 3 import ( 4 "context" 5 "database/sql" 6 "database/sql/driver" 7 "fmt" 8 "strings" 9 "sync" 10 "time" 11 12 "github.com/cayleygraph/cayley/clog" 13 "github.com/cayleygraph/cayley/graph" 14 "github.com/cayleygraph/cayley/graph/iterator" 15 "github.com/cayleygraph/cayley/graph/log" 16 "github.com/cayleygraph/cayley/internal/lru" 17 "github.com/cayleygraph/quad" 18 "github.com/cayleygraph/quad/pquads" 19 ) 20 21 // Type string for generic sql QuadStore. 22 // 23 // Deprecated: use specific types from sub-packages. 24 const QuadStoreType = "sql" 25 26 func init() { 27 // Deprecated QS registration that resolves backend type via "flavor" option. 28 registerQuadStore(QuadStoreType, "") 29 } 30 31 func registerQuadStore(name, typ string) { 32 graph.RegisterQuadStore(name, graph.QuadStoreRegistration{ 33 NewFunc: func(addr string, options graph.Options) (graph.QuadStore, error) { 34 return New(typ, addr, options) 35 }, 36 UpgradeFunc: nil, 37 InitFunc: func(addr string, options graph.Options) error { 38 return Init(typ, addr, options) 39 }, 40 IsPersistent: true, 41 }) 42 } 43 44 var _ Value = StringVal("") 45 46 type StringVal string 47 48 func (v StringVal) SQLValue() interface{} { 49 return escapeNullByte(string(v)) 50 } 51 52 type IntVal int64 53 54 func (v IntVal) SQLValue() interface{} { 55 return int64(v) 56 } 57 58 type FloatVal float64 59 60 func (v FloatVal) SQLValue() interface{} { 61 return float64(v) 62 } 63 64 type BoolVal bool 65 66 func (v BoolVal) SQLValue() interface{} { 67 return bool(v) 68 } 69 70 type TimeVal time.Time 71 72 func (v TimeVal) SQLValue() interface{} { 73 return time.Time(v) 74 } 75 76 type NodeHash struct { 77 graph.ValueHash 78 } 79 80 func (h NodeHash) SQLValue() interface{} { 81 if !h.Valid() { 82 return nil 83 } 84 return []byte(h.ValueHash[:]) 85 } 86 func (h *NodeHash) Scan(src interface{}) error { 87 if src == nil { 88 *h = NodeHash{} 89 return nil 90 } 91 b, ok := src.([]byte) 92 if !ok { 93 return fmt.Errorf("cannot scan %T to NodeHash", src) 94 } 95 if len(b) == 0 { 96 *h = NodeHash{} 97 return nil 98 } else if len(b) != quad.HashSize { 99 return fmt.Errorf("unexpected hash length: %d", len(b)) 100 } 101 copy(h.ValueHash[:], b) 102 return nil 103 } 104 105 func HashOf(s quad.Value) NodeHash { 106 return NodeHash{graph.HashOf(s)} 107 } 108 109 type QuadHashes struct { 110 graph.QuadHash 111 } 112 113 type QuadStore struct { 114 db *sql.DB 115 opt *Optimizer 116 flavor Registration 117 ids *lru.Cache 118 sizes *lru.Cache 119 noSizes bool 120 121 mu sync.RWMutex 122 nodes int64 123 quads int64 124 } 125 126 func connect(addr string, flavor string, opts graph.Options) (*sql.DB, error) { 127 maxOpenConnections, err := opts.IntKey("maxopenconnections", -1) 128 129 if err != nil { 130 return nil, fmt.Errorf("could not retrieve maxopenconnections from options: %v", err) 131 } 132 133 maxIdleConnections, err := opts.IntKey("maxidleconnections", -1) 134 135 if err != nil { 136 return nil, fmt.Errorf("could not retrieve maxIdleConnections from options: %v", err) 137 } 138 139 connMaxLifetime, err := opts.StringKey("connmaxlifetime", "") 140 141 if err != nil { 142 return nil, fmt.Errorf("could not retrieve connmaxlifetime from options: %v", err) 143 } 144 145 var connDuration time.Duration 146 if connMaxLifetime != "" { 147 connDuration, err = time.ParseDuration(connMaxLifetime) 148 149 if err != nil { 150 return nil, fmt.Errorf("couldn't parse connmaxlifetime string: %v", err) 151 } 152 } 153 154 // TODO(barakmich): Parse options for more friendly addr 155 conn, err := sql.Open(flavor, addr) 156 if err != nil { 157 clog.Errorf("Couldn't open database at %s: %#v", addr, err) 158 return nil, err 159 } 160 161 // "Open may just validate its arguments without creating a connection to the database." 162 // "To verify that the data source name is valid, call Ping." 163 // Source: http://golang.org/pkg/database/sql/#Open 164 if err := conn.Ping(); err != nil { 165 clog.Errorf("Couldn't open database at %s: %#v", addr, err) 166 return nil, err 167 } 168 169 if maxOpenConnections != -1 { 170 conn.SetMaxOpenConns(maxOpenConnections) 171 } 172 if maxIdleConnections != -1 { 173 conn.SetMaxIdleConns(maxIdleConnections) 174 } 175 if connDuration != 0 { 176 conn.SetConnMaxLifetime(connDuration) 177 } 178 179 return conn, nil 180 } 181 182 var nodesColumns = []string{ 183 "hash", 184 "value", 185 "value_string", 186 "datatype", 187 "language", 188 "iri", 189 "bnode", 190 "value_int", 191 "value_bool", 192 "value_float", 193 "value_time", 194 } 195 196 var nodeInsertColumns = [][]string{ 197 {"value"}, 198 {"value_string", "iri"}, 199 {"value_string", "bnode"}, 200 {"value_string"}, 201 {"value_string", "datatype"}, 202 {"value_string", "language"}, 203 {"value_int"}, 204 {"value_bool"}, 205 {"value_float"}, 206 {"value_time"}, 207 } 208 209 func typeFromOpts(opts graph.Options) string { 210 flavor, _ := opts.StringKey("flavor", "postgres") 211 return flavor 212 } 213 214 func Init(typ string, addr string, options graph.Options) error { 215 if typ == "" { 216 typ = typeFromOpts(options) 217 } 218 fl, ok := types[typ] 219 if !ok { 220 return fmt.Errorf("unsupported sql database: %s", typ) 221 } 222 conn, err := connect(addr, fl.Driver, options) 223 if err != nil { 224 return err 225 } 226 defer conn.Close() 227 228 nodesSql := fl.nodesTable() 229 quadsSql := fl.quadsTable() 230 indexes := fl.quadIndexes(options) 231 232 if fl.NoSchemaChangesInTx { 233 _, err = conn.Exec(nodesSql) 234 if err != nil { 235 err = fl.Error(err) 236 clog.Errorf("Cannot create nodes table: %v", err) 237 return err 238 } 239 _, err = conn.Exec(quadsSql) 240 if err != nil { 241 err = fl.Error(err) 242 clog.Errorf("Cannot create quad table: %v", err) 243 return err 244 } 245 for _, index := range indexes { 246 if _, err = conn.Exec(index); err != nil { 247 clog.Errorf("Cannot create index: %v", err) 248 return err 249 } 250 } 251 } else { 252 tx, err := conn.Begin() 253 if err != nil { 254 clog.Errorf("Couldn't begin creation transaction: %s", err) 255 return err 256 } 257 258 _, err = tx.Exec(nodesSql) 259 if err != nil { 260 tx.Rollback() 261 err = fl.Error(err) 262 clog.Errorf("Cannot create nodes table: %v", err) 263 return err 264 } 265 _, err = tx.Exec(quadsSql) 266 if err != nil { 267 tx.Rollback() 268 err = fl.Error(err) 269 clog.Errorf("Cannot create quad table: %v", err) 270 return err 271 } 272 for _, index := range indexes { 273 if _, err = tx.Exec(index); err != nil { 274 clog.Errorf("Cannot create index: %v", err) 275 tx.Rollback() 276 return err 277 } 278 } 279 tx.Commit() 280 } 281 return nil 282 } 283 284 func New(typ string, addr string, options graph.Options) (graph.QuadStore, error) { 285 if typ == "" { 286 typ = typeFromOpts(options) 287 } 288 fl, ok := types[typ] 289 if !ok { 290 return nil, fmt.Errorf("unsupported sql database: %s", typ) 291 } 292 conn, err := connect(addr, fl.Driver, options) 293 if err != nil { 294 return nil, err 295 } 296 qs := &QuadStore{ 297 db: conn, 298 opt: NewOptimizer(), 299 flavor: fl, 300 quads: -1, 301 nodes: -1, 302 sizes: lru.New(1024), 303 ids: lru.New(1024), 304 noSizes: true, // Skip size checking by default. 305 } 306 qs.opt.SetRegexpOp(qs.flavor.RegexpOp) 307 if qs.flavor.NoOffsetWithoutLimit { 308 qs.opt.NoOffsetWithoutLimit() 309 } 310 311 if local, err := options.BoolKey("local_optimize", false); err != nil { 312 return nil, err 313 } else if ok && local { 314 qs.noSizes = false 315 } 316 return qs, nil 317 } 318 319 func escapeNullByte(s string) string { 320 return strings.Replace(s, "\u0000", `\x00`, -1) 321 } 322 func unescapeNullByte(s string) string { 323 return strings.Replace(s, `\x00`, "\u0000", -1) 324 } 325 326 type ValueType int 327 328 func (t ValueType) Columns() []string { 329 return nodeInsertColumns[t] 330 } 331 332 func NodeValues(h NodeHash, v quad.Value) (ValueType, []interface{}, error) { 333 var ( 334 nodeKey ValueType 335 values = []interface{}{h.SQLValue(), nil, nil}[:1] 336 ) 337 switch v := v.(type) { 338 case quad.IRI: 339 nodeKey = 1 340 values = append(values, string(v), true) 341 case quad.BNode: 342 nodeKey = 2 343 values = append(values, string(v), true) 344 case quad.String: 345 nodeKey = 3 346 values = append(values, escapeNullByte(string(v))) 347 case quad.TypedString: 348 nodeKey = 4 349 values = append(values, escapeNullByte(string(v.Value)), string(v.Type)) 350 case quad.LangString: 351 nodeKey = 5 352 values = append(values, escapeNullByte(string(v.Value)), v.Lang) 353 case quad.Int: 354 nodeKey = 6 355 values = append(values, int64(v)) 356 case quad.Bool: 357 nodeKey = 7 358 values = append(values, bool(v)) 359 case quad.Float: 360 nodeKey = 8 361 values = append(values, float64(v)) 362 case quad.Time: 363 nodeKey = 9 364 values = append(values, time.Time(v)) 365 default: 366 nodeKey = 0 367 p, err := pquads.MarshalValue(v) 368 if err != nil { 369 clog.Errorf("couldn't marshal value: %v", err) 370 return 0, nil, err 371 } 372 values = append(values, p) 373 } 374 return nodeKey, values, nil 375 } 376 377 func (qs *QuadStore) NewQuadWriter() (quad.WriteCloser, error) { 378 return &quadWriter{qs: qs}, nil 379 } 380 381 type quadWriter struct { 382 qs *QuadStore 383 deltas []graph.Delta 384 } 385 386 func (w *quadWriter) WriteQuad(q quad.Quad) error { 387 _, err := w.WriteQuads([]quad.Quad{q}) 388 return err 389 } 390 391 func (w *quadWriter) WriteQuads(buf []quad.Quad) (int, error) { 392 // TODO(dennwc): write an optimized implementation 393 w.deltas = w.deltas[:0] 394 if cap(w.deltas) < len(buf) { 395 w.deltas = make([]graph.Delta, 0, len(buf)) 396 } 397 for _, q := range buf { 398 w.deltas = append(w.deltas, graph.Delta{ 399 Quad: q, Action: graph.Add, 400 }) 401 } 402 err := w.qs.ApplyDeltas(w.deltas, graph.IgnoreOpts{ 403 IgnoreDup: true, 404 }) 405 w.deltas = w.deltas[:0] 406 if err != nil { 407 return 0, err 408 } 409 return len(buf), nil 410 } 411 412 func (w *quadWriter) Close() error { 413 w.deltas = nil 414 return nil 415 } 416 417 func (qs *QuadStore) ApplyDeltas(in []graph.Delta, opts graph.IgnoreOpts) error { 418 // first calculate values ref deltas 419 deltas := graphlog.SplitDeltas(in) 420 421 tx, err := qs.db.Begin() 422 if err != nil { 423 clog.Errorf("couldn't begin write transaction: %v", err) 424 return err 425 } 426 427 retry := qs.flavor.TxRetry 428 if retry == nil { 429 retry = func(tx *sql.Tx, stmts func() error) error { 430 return stmts() 431 } 432 } 433 p := make([]string, 4) 434 for i := range p { 435 p[i] = qs.flavor.Placeholder(i + 1) 436 } 437 438 err = retry(tx, func() error { 439 err = qs.flavor.RunTx(tx, deltas.IncNode, deltas.QuadAdd, opts) 440 if err != nil { 441 return err 442 } 443 // quad delete is also generic, execute here 444 var ( 445 deleteQuad *sql.Stmt 446 deleteTriple *sql.Stmt 447 ) 448 fixNodes := make(map[graph.ValueHash]int) 449 for _, d := range deltas.QuadDel { 450 dirs := make([]interface{}, 0, len(quad.Directions)) 451 for _, h := range d.Quad.Dirs() { 452 dirs = append(dirs, NodeHash{h}.SQLValue()) 453 } 454 if deleteQuad == nil { 455 deleteQuad, err = tx.Prepare(`DELETE FROM quads WHERE subject_hash=` + p[0] + ` and predicate_hash=` + p[1] + ` and object_hash=` + p[2] + ` and label_hash=` + p[3] + `;`) 456 if err != nil { 457 return err 458 } 459 deleteTriple, err = tx.Prepare(`DELETE FROM quads WHERE subject_hash=` + p[0] + ` and predicate_hash=` + p[1] + ` and object_hash=` + p[2] + ` and label_hash is null;`) 460 if err != nil { 461 return err 462 } 463 } 464 stmt := deleteQuad 465 if i := len(dirs) - 1; dirs[i] == nil { 466 stmt = deleteTriple 467 dirs = dirs[:i] 468 } 469 result, err := stmt.Exec(dirs...) 470 if err != nil { 471 clog.Errorf("couldn't exec DELETE statement: %v", err) 472 return err 473 } 474 affected, err := result.RowsAffected() 475 if err != nil { 476 clog.Errorf("couldn't get DELETE RowsAffected: %v", err) 477 return err 478 } 479 if affected != 1 { 480 if !opts.IgnoreMissing { 481 // TODO: reference to delta 482 return &graph.DeltaError{Err: graph.ErrQuadNotExist} 483 } 484 // revert counters for all directions of this quad 485 for _, dir := range quad.Directions { 486 if h := d.Quad.Get(dir); h.Valid() { 487 fixNodes[h]++ 488 } 489 } 490 } 491 } 492 if len(deltas.DecNode) == 0 { 493 return nil 494 } 495 // node update SQL is generic enough to run it here 496 updateNode, err := tx.Prepare(`UPDATE nodes SET refs = refs + ` + p[0] + ` WHERE hash = ` + p[1] + `;`) 497 if err != nil { 498 return err 499 } 500 for _, n := range deltas.DecNode { 501 n.RefInc += fixNodes[n.Hash] 502 if n.RefInc == 0 { 503 continue 504 } 505 _, err := updateNode.Exec(n.RefInc, NodeHash{n.Hash}.SQLValue()) 506 if err != nil { 507 clog.Errorf("couldn't exec UPDATE statement: %v", err) 508 return err 509 } 510 } 511 // and remove unused nodes at last 512 _, err = tx.Exec(`DELETE FROM nodes WHERE refs <= 0;`) 513 if err != nil { 514 clog.Errorf("couldn't exec DELETE nodes statement: %v", err) 515 return err 516 } 517 return nil 518 }) 519 if err != nil { 520 tx.Rollback() 521 return err 522 } 523 524 qs.mu.Lock() 525 // TODO(barakmich): Sync size with writes. 526 qs.quads = -1 527 qs.nodes = -1 528 qs.mu.Unlock() 529 return tx.Commit() 530 } 531 532 func (qs *QuadStore) Quad(val graph.Ref) quad.Quad { 533 h := val.(QuadHashes) 534 return quad.Quad{ 535 Subject: qs.NameOf(h.Get(quad.Subject)), 536 Predicate: qs.NameOf(h.Get(quad.Predicate)), 537 Object: qs.NameOf(h.Get(quad.Object)), 538 Label: qs.NameOf(h.Get(quad.Label)), 539 } 540 } 541 542 func (qs *QuadStore) QuadIterator(d quad.Direction, val graph.Ref) graph.Iterator { 543 v, ok := val.(Value) 544 if !ok { 545 return iterator.NewNull() 546 } 547 sel := AllQuads("") 548 sel.WhereEq("", dirField(d), v) 549 return qs.NewIterator(sel) 550 } 551 552 func (qs *QuadStore) querySize(ctx context.Context, sel Select) (graph.Size, error) { 553 sel.Fields = []Field{ 554 {Name: "COUNT(*)", Raw: true}, // TODO: proper support for expressions 555 } 556 var sz int64 557 err := qs.QueryRow(ctx, sel).Scan(&sz) 558 if err != nil { 559 return graph.Size{}, err 560 } 561 return graph.Size{ 562 Size: sz, 563 Exact: true, 564 }, nil 565 } 566 567 func (qs *QuadStore) QuadIteratorSize(ctx context.Context, d quad.Direction, val graph.Ref) (graph.Size, error) { 568 v, ok := val.(Value) 569 if !ok { 570 return graph.Size{Size: 0, Exact: true}, nil 571 } 572 sel := AllQuads("") 573 sel.WhereEq("", dirField(d), v) 574 return qs.querySize(ctx, sel) 575 } 576 577 func (qs *QuadStore) NodesAllIterator() graph.Iterator { 578 return qs.NewIterator(AllNodes()) 579 } 580 581 func (qs *QuadStore) QuadsAllIterator() graph.Iterator { 582 return qs.NewIterator(AllQuads("")) 583 } 584 585 func (qs *QuadStore) ValueOf(s quad.Value) graph.Ref { 586 return NodeHash(HashOf(s)) 587 } 588 589 // NullTime represents a time.Time that may be null. NullTime implements the 590 // sql.Scanner interface so it can be used as a scan destination, similar to 591 // sql.NullString. 592 type NullTime struct { 593 Time time.Time 594 Valid bool // Valid is true if Time is not NULL 595 } 596 597 // Scan implements the Scanner interface. 598 func (nt *NullTime) Scan(value interface{}) error { 599 if value == nil { 600 nt.Time, nt.Valid = time.Time{}, false 601 return nil 602 } 603 switch value := value.(type) { 604 case time.Time: 605 nt.Time, nt.Valid = value, true 606 case []byte: 607 t, err := time.Parse("2006-01-02 15:04:05.999999", string(value)) 608 if err != nil { 609 return err 610 } 611 nt.Time, nt.Valid = t, true 612 default: 613 return fmt.Errorf("unsupported time format: %T: %v", value, value) 614 } 615 return nil 616 } 617 618 // Value implements the driver Valuer interface. 619 func (nt NullTime) Value() (driver.Value, error) { 620 if !nt.Valid { 621 return nil, nil 622 } 623 return nt.Time, nil 624 } 625 626 func (qs *QuadStore) NameOf(v graph.Ref) quad.Value { 627 if v == nil { 628 if clog.V(2) { 629 clog.Infof("NameOf was nil") 630 } 631 return nil 632 } else if v, ok := v.(graph.PreFetchedValue); ok { 633 return v.NameOf() 634 } 635 var hash NodeHash 636 switch h := v.(type) { 637 case graph.PreFetchedValue: 638 return h.NameOf() 639 case NodeHash: 640 hash = h 641 case graph.ValueHash: 642 hash = NodeHash{h} 643 default: 644 panic(fmt.Errorf("unexpected token: %T", v)) 645 } 646 if !hash.Valid() { 647 if clog.V(2) { 648 clog.Infof("NameOf was nil") 649 } 650 return nil 651 } 652 if val, ok := qs.ids.Get(hash.String()); ok { 653 return val.(quad.Value) 654 } 655 query := `SELECT 656 value, 657 value_string, 658 datatype, 659 language, 660 iri, 661 bnode, 662 value_int, 663 value_bool, 664 value_float, 665 value_time 666 FROM nodes WHERE hash = ` + qs.flavor.Placeholder(1) + ` LIMIT 1;` 667 c := qs.db.QueryRow(query, hash.SQLValue()) 668 var ( 669 data []byte 670 str sql.NullString 671 typ sql.NullString 672 lang sql.NullString 673 iri sql.NullBool 674 bnode sql.NullBool 675 vint sql.NullInt64 676 vbool sql.NullBool 677 vfloat sql.NullFloat64 678 vtime NullTime 679 ) 680 if err := c.Scan( 681 &data, 682 &str, 683 &typ, 684 &lang, 685 &iri, 686 &bnode, 687 &vint, 688 &vbool, 689 &vfloat, 690 &vtime, 691 ); err != nil { 692 if err != sql.ErrNoRows { 693 clog.Errorf("Couldn't execute value lookup: %v", err) 694 } 695 return nil 696 } 697 var val quad.Value 698 if str.Valid { 699 if iri.Bool { 700 val = quad.IRI(str.String) 701 } else if bnode.Bool { 702 val = quad.BNode(str.String) 703 } else if lang.Valid { 704 val = quad.LangString{ 705 Value: quad.String(unescapeNullByte(str.String)), 706 Lang: lang.String, 707 } 708 } else if typ.Valid { 709 val = quad.TypedString{ 710 Value: quad.String(unescapeNullByte(str.String)), 711 Type: quad.IRI(typ.String), 712 } 713 } else { 714 val = quad.String(unescapeNullByte(str.String)) 715 } 716 } else if vint.Valid { 717 val = quad.Int(vint.Int64) 718 } else if vbool.Valid { 719 val = quad.Bool(vbool.Bool) 720 } else if vfloat.Valid { 721 val = quad.Float(vfloat.Float64) 722 } else if vtime.Valid { 723 val = quad.Time(vtime.Time) 724 } else { 725 qv, err := pquads.UnmarshalValue(data) 726 if err != nil { 727 clog.Errorf("Couldn't unmarshal value: %v", err) 728 return nil 729 } 730 val = qv 731 } 732 if val != nil { 733 qs.ids.Put(hash.String(), val) 734 } 735 return val 736 } 737 738 func (qs *QuadStore) Stats(ctx context.Context, exact bool) (graph.Stats, error) { 739 st := graph.Stats{ 740 Nodes: graph.Size{Exact: true}, 741 Quads: graph.Size{Exact: true}, 742 } 743 qs.mu.RLock() 744 st.Quads.Size = qs.quads 745 st.Nodes.Size = qs.nodes 746 qs.mu.RUnlock() 747 if st.Quads.Size >= 0 { 748 return st, nil 749 } 750 query := func(table string) string { 751 return "SELECT COUNT(*) FROM " + table + ";" 752 } 753 if !exact && qs.flavor.Estimated != nil { 754 query = qs.flavor.Estimated 755 st.Quads.Exact = false 756 st.Nodes.Exact = false 757 } 758 err := qs.db.QueryRow(query("quads")).Scan(&st.Quads.Size) 759 if err != nil { 760 return graph.Stats{}, err 761 } 762 err = qs.db.QueryRow(query("nodes")).Scan(&st.Nodes.Size) 763 if err != nil { 764 return graph.Stats{}, err 765 } 766 if st.Quads.Exact { 767 qs.mu.Lock() 768 qs.quads = st.Quads.Size 769 qs.nodes = st.Nodes.Size 770 qs.mu.Unlock() 771 } 772 return st, nil 773 } 774 775 func (qs *QuadStore) Close() error { 776 return qs.db.Close() 777 } 778 779 func (qs *QuadStore) QuadDirection(in graph.Ref, d quad.Direction) graph.Ref { 780 return NodeHash{in.(QuadHashes).Get(d)} 781 } 782 783 func (qs *QuadStore) sizeForIterator(dir quad.Direction, hash NodeHash) int64 { 784 var err error 785 if qs.noSizes { 786 st, _ := qs.Stats(context.TODO(), false) 787 if dir == quad.Predicate { 788 return (st.Quads.Size / 100) + 1 789 } 790 return (st.Quads.Size / 1000) + 1 791 } 792 if val, ok := qs.sizes.Get(hash.String() + string(dir.Prefix())); ok { 793 return val.(int64) 794 } 795 var size int64 796 if clog.V(4) { 797 clog.Infof("sql: getting size for select %s, %v", dir.String(), hash) 798 } 799 err = qs.db.QueryRow( 800 fmt.Sprintf("SELECT count(*) FROM quads WHERE %s_hash = "+qs.flavor.Placeholder(1)+";", dir.String()), hash.SQLValue()).Scan(&size) 801 if err != nil { 802 clog.Errorf("Error getting size from SQL database: %v", err) 803 return 0 804 } 805 qs.sizes.Put(hash.String()+string(dir.Prefix()), size) 806 return size 807 }