123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- package db
- import (
- "context"
- "fmt"
- "github.com/gshopify/service-wrapper/config"
- "github.com/gshopify/service-wrapper/db"
- "github.com/gshopify/service-wrapper/model"
- "github.com/jellydator/ttlcache/v3"
- "github.com/mailru/dbr"
- _ "github.com/mailru/go-clickhouse"
- "gshopper.com/gshopify/products/graphql/generated"
- "gshopper.com/gshopify/products/relation"
- "net/url"
- "strings"
- "sync"
- )
- type clickhouse struct {
- ctx context.Context
- config *db.Config
- session *dbr.Session
- cache *ttlcache.Cache[string, any]
- }
- func New(ctx context.Context, forceDebug bool) (Database, error) {
- r := &clickhouse{
- ctx: ctx,
- config: db.New(),
- cache: ttlcache.New[string, any](
- ttlcache.WithTTL[string, any](cacheTimeout),
- ttlcache.WithCapacity[string, any](cacheCapacity),
- ),
- }
- if err := config.Instance().Load(ctx, r.config); err != nil {
- return nil, err
- }
- if forceDebug {
- r.config.Params.Debug = true
- }
- //goland:noinspection HttpUrlsUsage
- source, err := url.Parse(fmt.Sprintf("http://%s:%s@%s:%d/%s",
- url.QueryEscape(r.config.Username),
- url.QueryEscape(r.config.Password),
- r.config.Host,
- r.config.Port,
- r.config.Database))
- if err != nil {
- return nil, err
- }
- kv := make(url.Values)
- kv.Set("timeout", fmt.Sprintf("%ds", r.config.Params.Timeout))
- kv.Set("read_timeout", fmt.Sprintf("%ds", r.config.Params.ReadTimeout))
- kv.Set("write_timeout", fmt.Sprintf("%ds", r.config.Params.WriteTimeout))
- kv.Set("debug", fmt.Sprintf("%v", r.config.Params.Debug))
- source.RawQuery = kv.Encode()
- con, err := dbr.Open("clickhouse", source.String(), nil)
- if err != nil {
- return nil, fmt.Errorf("could not establish Clickhouse session: %v", err)
- }
- r.session = con.NewSessionContext(ctx, nil)
- if err = r.session.Ping(); err != nil {
- return nil, err
- }
- go r.cache.Start()
- return r, nil
- }
- func (db *clickhouse) ProductCollections(ln model.LanguageCode, id string) ([]*generated.Collection, error) {
- var (
- collections []*generated.Collection
- key = productCollectionKey("product.id=?", id)
- l = ttlcache.LoaderFunc[string, any](
- func(ttl *ttlcache.Cache[string, any], _ string) *ttlcache.Item[string, any] {
- var o []relation.Collection
- rows, err := db.session.SelectBySql("SELECT "+
- ln.SqlFieldSelection("title")+", "+ln.SqlFieldSelection("description")+", `id`, `handle`, `thumbnail`, "+
- "`created_at`, `updated_at`, `deleted_at` "+
- "FROM `product_collection` "+
- "ARRAY JOIN (SELECT `collection_ids` FROM `product` WHERE `id` = ?) AS cid "+
- "WHERE `id` = cid "+
- "ORDER BY `created_at` ASC;", key.Args()...).
- Load(&o)
- if rows < 1 || err != nil {
- return nil
- }
- return ttl.Set(key.String(), o, key.TTL())
- })
- )
- p := db.cache.Get(key.String(), ttlcache.WithLoader[string, any](l))
- if p == nil {
- return nil, fmt.Errorf("not found")
- }
- for _, row := range p.Value().([]relation.Collection) {
- collections = append(collections, row.As())
- }
- return collections, nil
- }
- func (db *clickhouse) Product(ln model.LanguageCode, handle *string, id *string) (*generated.Product, error) {
- var (
- clause = strings.Builder{}
- vars = []any{relation.ProductStatusPublished}
- )
- clause.WriteString("status=?")
- if id != nil {
- clause.WriteString(" AND id=?")
- vars = append(vars, *id)
- }
- if handle != nil {
- clause.WriteString(" AND handle=?")
- vars = append(vars, *handle)
- }
- var (
- key = productKey(clause.String(), vars...)
- l = ttlcache.LoaderFunc[string, any](
- func(ttl *ttlcache.Cache[string, any], _ string) *ttlcache.Item[string, any] {
- o := relation.Product{}
- rows, err := db.session.
- Select(productSelection(ln)...).
- From(key.Table()).
- Where(key.Clause(), key.Args()...).
- OrderBy("created_at").
- Limit(1).
- Load(&o)
- if rows < 1 || err != nil {
- return nil
- }
- return ttl.Set(key.String(), o, key.TTL())
- })
- )
- p := db.cache.Get(key.String(), ttlcache.WithLoader[string, any](l))
- if p == nil {
- return nil, fmt.Errorf("not found")
- }
- product := p.Value().(relation.Product)
- return product.As(), nil
- }
- func (db *clickhouse) ProductOptions(ln model.LanguageCode, id string) ([]*generated.ProductOption, error) {
- var options []*generated.ProductOption
- var o []relation.ProductOption
- _, err := db.session.
- Select(
- "id",
- "product_id",
- "created_at", "updated_at", "deleted_at",
- ln.SqlFieldSelection("name"),
- ln.SqlArraySelection("values")).
- From("product_option").
- Where("product_id=?", id).
- OrderBy("created_at").
- Load(&o)
- if err != nil {
- return nil, err
- }
- for _, v := range o {
- options = append(options, v.As())
- }
- return options, nil
- }
- func (db *clickhouse) CollectionProducts(ln model.LanguageCode, id string) ([]*generated.Product, error) {
- var (
- products []*generated.Product
- key = productKey("has(collection_ids, ?)", id)
- l = ttlcache.LoaderFunc[string, any](
- func(ttl *ttlcache.Cache[string, any], _ string) *ttlcache.Item[string, any] {
- var o []relation.Product
- rows, err := db.session.
- Select(productSelection(ln)...).
- From(key.Table()).
- Where(key.Clause(), key.Args()...).
- OrderBy("created_at").
- Load(&o)
- if rows < 1 || err != nil {
- return nil
- }
- return ttl.Set(key.String(), o, key.TTL())
- },
- )
- )
- p := db.cache.Get(key.String(), ttlcache.WithLoader[string, any](l))
- if p == nil {
- return nil, fmt.Errorf("not found")
- }
- for _, row := range p.Value().([]relation.Product) {
- products = append(products, row.As())
- }
- return products, nil
- }
- func (db *clickhouse) Ping() error {
- return db.session.Ping()
- }
- func (db *clickhouse) Close() error {
- var wg sync.WaitGroup
- wg.Add(2)
- go func() {
- defer wg.Done()
- if db.cache != nil {
- db.cache.DeleteAll()
- db.cache.Stop()
- }
- }()
- go func() {
- defer wg.Done()
- if db.session != nil {
- _ = db.session.Close()
- }
- }()
- return nil
- }
|