package mysql import ( "database/sql" "fmt" "time" "unicode" log "go-micro.dev/v4/logger" "go-micro.dev/v4/store" "github.com/pkg/errors" ) var ( // DefaultDatabase is the database that the sql store will use if no database is provided. DefaultDatabase = "micro" // DefaultTable is the table that the sql store will use if no table is provided. DefaultTable = "micro" ) type sqlStore struct { db *sql.DB database string table string options store.Options readPrepare, writePrepare, deletePrepare *sql.Stmt } func (s *sqlStore) Init(opts ...store.Option) error { for _, o := range opts { o(&s.options) } // reconfigure return s.configure() } func (s *sqlStore) Options() store.Options { return s.options } func (s *sqlStore) Close() error { return s.db.Close() } // List all the known records func (s *sqlStore) List(opts ...store.ListOption) ([]string, error) { rows, err := s.db.Query(fmt.Sprintf("SELECT `key`, value, expiry FROM %s.%s;", s.database, s.table)) if err != nil { if err == sql.ErrNoRows { return nil, nil } return nil, err } defer rows.Close() var records []string var cachedTime time.Time for rows.Next() { record := &store.Record{} if err := rows.Scan(&record.Key, &record.Value, &cachedTime); err != nil { return nil, err } if cachedTime.Before(time.Now()) { // record has expired go s.Delete(record.Key) } else { records = append(records, record.Key) } } rowErr := rows.Close() if rowErr != nil { // transaction rollback or something return records, rowErr } if err := rows.Err(); err != nil { return nil, err } return records, nil } // Read all records with keys func (s *sqlStore) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) { var options store.ReadOptions for _, o := range opts { o(&options) } // TODO: make use of options.Prefix using WHERE key LIKE = ? var records []*store.Record row := s.readPrepare.QueryRow(key) record := &store.Record{} var cachedTime time.Time if err := row.Scan(&record.Key, &record.Value, &cachedTime); err != nil { if err == sql.ErrNoRows { return records, store.ErrNotFound } return records, err } if cachedTime.Before(time.Now()) { // record has expired go s.Delete(key) return records, store.ErrNotFound } record.Expiry = time.Until(cachedTime) records = append(records, record) return records, nil } // Write records func (s *sqlStore) Write(r *store.Record, opts ...store.WriteOption) error { timeCached := time.Now().Add(r.Expiry) _, err := s.writePrepare.Exec(r.Key, r.Value, timeCached, r.Value, timeCached) if err != nil { return errors.Wrap(err, "Couldn't insert record "+r.Key) } return nil } // Delete records with keys func (s *sqlStore) Delete(key string, opts ...store.DeleteOption) error { result, err := s.deletePrepare.Exec(key) if err != nil { return err } _, err = result.RowsAffected() if err != nil { return err } return nil } func (s *sqlStore) initDB() error { // Create the namespace's database _, err := s.db.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s ;", s.database)) if err != nil { return err } _, err = s.db.Exec(fmt.Sprintf("USE %s ;", s.database)) if err != nil { return errors.Wrap(err, "Couldn't use database") } // Create a table for the namespace's prefix createSQL := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (`key` varchar(255) primary key, value blob null, expiry timestamp not null);", s.table) _, err = s.db.Exec(createSQL) if err != nil { return errors.Wrap(err, "Couldn't create table") } // prepare s.readPrepare, _ = s.db.Prepare(fmt.Sprintf("SELECT `key`, value, expiry FROM %s.%s WHERE `key` = ?;", s.database, s.table)) s.writePrepare, _ = s.db.Prepare(fmt.Sprintf("INSERT INTO %s.%s (`key`, value, expiry) VALUES(?, ?, ?) ON DUPLICATE KEY UPDATE `value`= ?, `expiry` = ?", s.database, s.table)) s.deletePrepare, _ = s.db.Prepare(fmt.Sprintf("DELETE FROM %s.%s WHERE `key` = ?;", s.database, s.table)) return nil } func (s *sqlStore) configure() error { nodes := s.options.Nodes if len(nodes) == 0 { nodes = []string{"localhost:3306"} } database := s.options.Database if len(database) == 0 { database = DefaultDatabase } table := s.options.Table if len(table) == 0 { table = DefaultTable } for _, r := range database { if !unicode.IsLetter(r) { return errors.New("store.namespace must only contain letters") } } source := nodes[0] // create source from first node db, err := sql.Open("mysql", source) if err != nil { return err } if err := db.Ping(); err != nil { return err } if s.db != nil { s.db.Close() } // save the values s.db = db s.database = database s.table = table // initialise the database return s.initDB() } func (s *sqlStore) String() string { return "mysql" } // New returns a new micro Store backed by sql func NewStore(opts ...store.Option) store.Store { var options store.Options for _, o := range opts { o(&options) } // new store s := new(sqlStore) // set the options s.options = options // configure the store if err := s.configure(); err != nil { log.Fatal(err) } // return store return s }