2019-12-16 15:09:59 +00:00
// Package cockroach implements the cockroach store
package cockroach
2019-11-01 14:13:21 +00:00
import (
"database/sql"
"fmt"
2020-01-29 00:47:41 +08:00
"net/url"
2020-03-01 22:09:06 +00:00
"regexp"
2019-12-16 15:09:59 +00:00
"strings"
2019-11-01 14:13:21 +00:00
"time"
"github.com/lib/pq"
2020-01-30 14:39:00 +03:00
"github.com/micro/go-micro/v2/store"
2019-12-16 14:38:51 +00:00
"github.com/pkg/errors"
2019-11-01 14:13:21 +00:00
)
// DefaultNamespace is the namespace that the sql store
// will use if no namespace is provided.
2019-12-16 12:13:18 +00:00
var (
DefaultNamespace = "micro"
DefaultPrefix = "micro"
)
2019-11-01 14:13:21 +00:00
type sqlStore struct {
db * sql . DB
2019-12-16 12:13:18 +00:00
database string
table string
2019-12-16 14:38:51 +00:00
2020-03-17 16:15:23 +00:00
list * sql . Stmt
readOne * sql . Stmt
readMany * sql . Stmt
readOffset * sql . Stmt
write * sql . Stmt
delete * sql . Stmt
2020-03-12 13:41:30 +00:00
2019-12-16 14:38:51 +00:00
options store . Options
2019-11-01 14:13:21 +00:00
}
2020-01-08 12:11:31 +00:00
func ( s * sqlStore ) Init ( opts ... store . Option ) error {
for _ , o := range opts {
o ( & s . options )
}
// reconfigure
return s . configure ( )
}
2019-11-01 14:13:21 +00:00
// List all the known records
2020-03-12 13:41:30 +00:00
func ( s * sqlStore ) List ( opts ... store . ListOption ) ( [ ] string , error ) {
rows , err := s . list . Query ( )
var keys [ ] string
2019-11-01 14:13:21 +00:00
var timehelper pq . NullTime
if err != nil {
if err == sql . ErrNoRows {
2020-03-12 13:41:30 +00:00
return keys , nil
2019-11-01 14:13:21 +00:00
}
return nil , err
}
defer rows . Close ( )
for rows . Next ( ) {
record := & store . Record { }
if err := rows . Scan ( & record . Key , & record . Value , & timehelper ) ; err != nil {
2020-03-12 13:41:30 +00:00
return keys , err
2019-11-01 14:13:21 +00:00
}
if timehelper . Valid {
if timehelper . Time . Before ( time . Now ( ) ) {
// record has expired
go s . Delete ( record . Key )
} else {
record . Expiry = time . Until ( timehelper . Time )
2020-03-12 13:41:30 +00:00
keys = append ( keys , record . Key )
2019-11-01 14:13:21 +00:00
}
} else {
2020-03-12 13:41:30 +00:00
keys = append ( keys , record . Key )
2019-11-01 14:13:21 +00:00
}
}
rowErr := rows . Close ( )
if rowErr != nil {
// transaction rollback or something
2020-03-12 13:41:30 +00:00
return keys , rowErr
2019-11-01 14:13:21 +00:00
}
if err := rows . Err ( ) ; err != nil {
2020-03-12 13:41:30 +00:00
return keys , err
2019-11-01 14:13:21 +00:00
}
2020-03-12 13:41:30 +00:00
return keys , nil
2019-11-01 14:13:21 +00:00
}
2020-03-12 13:41:30 +00:00
// Read a single key
2020-01-08 22:23:14 +00:00
func ( s * sqlStore ) Read ( key string , opts ... store . ReadOption ) ( [ ] * store . Record , error ) {
var options store . ReadOptions
for _ , o := range opts {
o ( & options )
}
2020-03-17 16:15:23 +00:00
if options . Prefix || options . Suffix {
return s . read ( key , options )
}
2020-01-08 22:23:14 +00:00
2019-11-01 14:13:21 +00:00
var records [ ] * store . Record
var timehelper pq . NullTime
2020-01-08 22:23:14 +00:00
2020-03-12 13:41:30 +00:00
row := s . readOne . QueryRow ( key )
2020-01-08 22:23:14 +00:00
record := & store . Record { }
if err := row . Scan ( & record . Key , & record . Value , & timehelper ) ; err != nil {
if err == sql . ErrNoRows {
return records , store . ErrNotFound
2019-11-01 14:13:21 +00:00
}
2020-01-08 22:23:14 +00:00
return records , err
}
if timehelper . Valid {
if timehelper . Time . Before ( time . Now ( ) ) {
// record has expired
go s . Delete ( key )
return records , store . ErrNotFound
2019-11-01 14:13:21 +00:00
}
2020-01-08 22:23:14 +00:00
record . Expiry = time . Until ( timehelper . Time )
records = append ( records , record )
} else {
records = append ( records , record )
2019-11-01 14:13:21 +00:00
}
2020-01-08 22:23:14 +00:00
2019-11-01 14:13:21 +00:00
return records , nil
}
2020-03-17 16:15:23 +00:00
// Read Many records
func ( s * sqlStore ) read ( key string , options store . ReadOptions ) ( [ ] * store . Record , error ) {
pattern := "%"
if options . Prefix {
pattern = key + pattern
}
if options . Suffix {
pattern = pattern + key
}
var rows * sql . Rows
var err error
if options . Limit != 0 {
rows , err = s . readOffset . Query ( pattern , options . Limit , options . Offset )
} else {
rows , err = s . readMany . Query ( pattern )
}
if err != nil {
if err == sql . ErrNoRows {
return [ ] * store . Record { } , nil
}
return [ ] * store . Record { } , errors . Wrap ( err , "sqlStore.read failed" )
}
defer rows . Close ( )
var records [ ] * store . Record
var timehelper pq . NullTime
for rows . Next ( ) {
record := & store . Record { }
if err := rows . Scan ( & record . Key , & record . Value , & timehelper ) ; err != nil {
return records , err
}
if timehelper . Valid {
if timehelper . Time . Before ( time . Now ( ) ) {
// record has expired
go s . Delete ( record . Key )
} else {
record . Expiry = time . Until ( timehelper . Time )
records = append ( records , record )
}
} else {
records = append ( records , record )
}
}
rowErr := rows . Close ( )
if rowErr != nil {
// transaction rollback or something
return records , rowErr
}
if err := rows . Err ( ) ; err != nil {
return records , err
}
return records , nil
}
2019-11-01 14:13:21 +00:00
// Write records
2020-03-12 13:41:30 +00:00
func ( s * sqlStore ) Write ( r * store . Record , opts ... store . WriteOption ) error {
var err error
2020-01-08 22:23:14 +00:00
if r . Expiry != 0 {
2020-03-12 13:41:30 +00:00
_ , err = s . write . Exec ( r . Key , r . Value , time . Now ( ) . Add ( r . Expiry ) )
2020-01-08 22:23:14 +00:00
} else {
2020-03-12 13:41:30 +00:00
_ , err = s . write . Exec ( r . Key , r . Value , nil )
2020-01-08 22:23:14 +00:00
}
if err != nil {
return errors . Wrap ( err , "Couldn't insert record " + r . Key )
2019-11-01 14:13:21 +00:00
}
return nil
}
// Delete records with keys
2020-03-12 13:41:30 +00:00
func ( s * sqlStore ) Delete ( key string , opts ... store . DeleteOption ) error {
result , err := s . delete . Exec ( key )
2020-01-08 22:23:14 +00:00
if err != nil {
return err
2019-11-01 14:13:21 +00:00
}
2020-01-08 22:23:14 +00:00
_ , err = result . RowsAffected ( )
if err != nil {
return err
}
2019-11-01 14:13:21 +00:00
return nil
}
2020-01-08 12:11:31 +00:00
func ( s * sqlStore ) initDB ( ) error {
2019-12-16 17:11:13 +00:00
// Create the namespace's database
_ , err := s . db . Exec ( fmt . Sprintf ( "CREATE DATABASE IF NOT EXISTS %s ;" , s . database ) )
2019-11-01 14:13:21 +00:00
if err != nil {
2020-01-08 12:11:31 +00:00
return err
2019-11-01 14:13:21 +00:00
}
2019-12-16 14:38:51 +00:00
2019-12-16 17:11:13 +00:00
_ , err = s . db . Exec ( fmt . Sprintf ( "SET DATABASE = %s ;" , s . database ) )
2019-11-01 14:13:21 +00:00
if err != nil {
2020-01-08 12:11:31 +00:00
return errors . Wrap ( err , "Couldn't set database" )
2019-11-01 14:13:21 +00:00
}
2019-12-16 17:11:13 +00:00
// Create a table for the namespace's prefix
_ , err = s . db . Exec ( fmt . Sprintf ( ` CREATE TABLE IF NOT EXISTS % s
2019-11-01 14:13:21 +00:00
(
2019-12-16 17:11:13 +00:00
key text NOT NULL ,
2019-11-01 14:13:21 +00:00
value bytea ,
expiry timestamp with time zone ,
CONSTRAINT % s_pkey PRIMARY KEY ( key )
2019-12-16 17:11:13 +00:00
) ; ` , s . table , s . table ) )
2019-11-01 14:13:21 +00:00
if err != nil {
2020-01-08 12:11:31 +00:00
return errors . Wrap ( err , "Couldn't create table" )
2019-11-01 14:13:21 +00:00
}
2020-03-17 16:15:23 +00:00
// Create Index
2020-03-24 23:49:09 +00:00
_ , err = s . db . Exec ( fmt . Sprintf ( ` CREATE INDEX IF NOT EXISTS "%s" ON %s.%s USING btree ("key"); ` , "key_index_" + s . table , s . database , s . table ) )
2020-03-17 16:15:23 +00:00
if err != nil {
return err
}
2020-03-12 13:41:30 +00:00
list , err := s . db . Prepare ( fmt . Sprintf ( "SELECT key, value, expiry FROM %s.%s;" , s . database , s . table ) )
if err != nil {
return errors . Wrap ( err , "List statement couldn't be prepared" )
}
2020-03-17 16:15:23 +00:00
if s . list != nil {
s . list . Close ( )
}
2020-03-12 13:41:30 +00:00
s . list = list
readOne , err := s . db . Prepare ( fmt . Sprintf ( "SELECT key, value, expiry FROM %s.%s WHERE key = $1;" , s . database , s . table ) )
if err != nil {
return errors . Wrap ( err , "ReadOne statement couldn't be prepared" )
}
2020-03-17 16:15:23 +00:00
if s . readOne != nil {
s . readOne . Close ( )
}
2020-03-12 13:41:30 +00:00
s . readOne = readOne
2020-03-17 16:15:23 +00:00
readMany , err := s . db . Prepare ( fmt . Sprintf ( "SELECT key, value, expiry FROM %s.%s WHERE key LIKE $1;" , s . database , s . table ) )
if err != nil {
return errors . Wrap ( err , "ReadMany statement couldn't be prepared" )
}
if s . readMany != nil {
s . readMany . Close ( )
}
s . readMany = readMany
readOffset , err := s . db . Prepare ( fmt . Sprintf ( "SELECT key, value, expiry FROM %s.%s WHERE key LIKE $1 ORDER BY key DESC LIMIT $2 OFFSET $3;" , s . database , s . table ) )
if err != nil {
return errors . Wrap ( err , "ReadOffset statement couldn't be prepared" )
}
if s . readOffset != nil {
s . readOffset . Close ( )
}
s . readOffset = readOffset
2020-03-12 13:41:30 +00:00
write , err := s . db . Prepare ( fmt . Sprintf ( ` INSERT INTO % s . % s ( key , value , expiry )
VALUES ( $ 1 , $ 2 : : bytea , $ 3 )
ON CONFLICT ( key )
DO UPDATE
SET value = EXCLUDED . value , expiry = EXCLUDED . expiry ; ` , s . database , s . table ) )
if err != nil {
return errors . Wrap ( err , "Write statement couldn't be prepared" )
}
2020-03-17 16:15:23 +00:00
if s . write != nil {
s . write . Close ( )
}
2020-03-12 13:41:30 +00:00
s . write = write
delete , err := s . db . Prepare ( fmt . Sprintf ( "DELETE FROM %s.%s WHERE key = $1;" , s . database , s . table ) )
if err != nil {
return errors . Wrap ( err , "Delete statement couldn't be prepared" )
}
2020-03-17 16:15:23 +00:00
if s . delete != nil {
s . delete . Close ( )
}
2020-03-12 13:41:30 +00:00
s . delete = delete
2020-01-08 12:11:31 +00:00
return nil
}
2019-11-01 14:13:21 +00:00
2020-01-08 12:11:31 +00:00
func ( s * sqlStore ) configure ( ) error {
2020-03-24 15:37:30 +00:00
if len ( s . options . Nodes ) == 0 {
s . options . Nodes = [ ] string { "localhost:26257" }
2019-12-16 14:38:51 +00:00
}
2019-11-01 14:13:21 +00:00
2020-04-06 16:45:55 +01:00
namespace := s . options . Database
2019-12-16 14:38:51 +00:00
if len ( namespace ) == 0 {
namespace = DefaultNamespace
2019-11-01 14:13:21 +00:00
}
2019-12-16 14:38:51 +00:00
2020-04-06 16:45:55 +01:00
prefix := s . options . Table
2019-12-16 14:38:51 +00:00
if len ( prefix ) == 0 {
prefix = DefaultPrefix
2019-11-01 14:13:21 +00:00
}
2019-12-16 14:38:51 +00:00
2020-03-12 13:41:30 +00:00
// store.namespace must only contain letters, numbers and underscores
2020-03-01 22:09:06 +00:00
reg , err := regexp . Compile ( "[^a-zA-Z0-9]+" )
if err != nil {
return errors . New ( "error compiling regex for namespace" )
2019-11-01 14:13:21 +00:00
}
2020-03-12 13:41:30 +00:00
namespace = reg . ReplaceAllString ( namespace , "_" )
2020-04-01 20:19:21 +01:00
prefix = reg . ReplaceAllString ( prefix , "_" )
2019-12-16 14:38:51 +00:00
2020-03-24 15:37:30 +00:00
source := s . options . Nodes [ 0 ]
2020-01-29 00:47:41 +08:00
// check if it is a standard connection string eg: host=%s port=%d user=%s password=%s dbname=%s sslmode=disable
// if err is nil which means it would be a URL like postgre://xxxx?yy=zz
2020-03-01 22:09:06 +00:00
_ , err = url . Parse ( source )
2020-01-29 00:47:41 +08:00
if err != nil {
if ! strings . Contains ( source , " " ) {
source = fmt . Sprintf ( "host=%s" , source )
}
2019-12-16 15:09:59 +00:00
}
2020-01-08 12:11:31 +00:00
2019-12-16 14:38:51 +00:00
// create source from first node
2019-12-16 15:09:59 +00:00
db , err := sql . Open ( "postgres" , source )
2019-12-16 14:38:51 +00:00
if err != nil {
2020-01-08 12:11:31 +00:00
return err
2019-11-01 14:13:21 +00:00
}
2019-12-16 14:38:51 +00:00
if err := db . Ping ( ) ; err != nil {
2020-01-08 12:11:31 +00:00
return err
2019-11-01 14:13:21 +00:00
}
2019-12-16 14:38:51 +00:00
2020-01-08 12:11:31 +00:00
if s . db != nil {
s . db . Close ( )
2019-11-01 14:13:21 +00:00
}
2020-01-08 12:11:31 +00:00
// save the values
s . db = db
s . database = namespace
s . table = prefix
// initialise the database
return s . initDB ( )
}
2020-01-10 19:13:55 +00:00
func ( s * sqlStore ) String ( ) string {
return "cockroach"
}
2020-03-12 13:41:30 +00:00
func ( s * sqlStore ) Options ( ) store . Options {
return s . options
}
// NewStore returns a new micro Store backed by sql
2020-01-08 12:11:31 +00:00
func NewStore ( opts ... store . Option ) store . Store {
var options store . Options
for _ , o := range opts {
o ( & options )
}
// new store
s := new ( sqlStore )
2020-01-10 19:13:55 +00:00
// set the options
s . options = options
2020-01-08 12:11:31 +00:00
2020-03-24 17:16:38 +00:00
// best-effort configure the store
s . configure ( )
2020-01-08 12:11:31 +00:00
// return store
2019-12-16 14:38:51 +00:00
return s
2019-11-01 14:13:21 +00:00
}