github.com/annwntech/go-micro/v2@v2.9.5/store/cockroach/cockroach.go (about) 1 // Package cockroach implements the cockroach store 2 package cockroach 3 4 import ( 5 "database/sql" 6 "fmt" 7 "net/url" 8 "regexp" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/lib/pq" 14 "github.com/annwntech/go-micro/v2/logger" 15 "github.com/annwntech/go-micro/v2/store" 16 "github.com/pkg/errors" 17 ) 18 19 // DefaultDatabase is the namespace that the sql store 20 // will use if no namespace is provided. 21 var ( 22 DefaultDatabase = "micro" 23 DefaultTable = "micro" 24 ) 25 26 var ( 27 re = regexp.MustCompile("[^a-zA-Z0-9]+") 28 29 statements = map[string]string{ 30 "list": "SELECT key, value, metadata, expiry FROM %s.%s;", 31 "read": "SELECT key, value, metadata, expiry FROM %s.%s WHERE key = $1;", 32 "readMany": "SELECT key, value, metadata, expiry FROM %s.%s WHERE key LIKE $1;", 33 "readOffset": "SELECT key, value, metadata, expiry FROM %s.%s WHERE key LIKE $1 ORDER BY key DESC LIMIT $2 OFFSET $3;", 34 "write": "INSERT INTO %s.%s(key, value, metadata, expiry) VALUES ($1, $2::bytea, $3, $4) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, metadata = EXCLUDED.metadata, expiry = EXCLUDED.expiry;", 35 "delete": "DELETE FROM %s.%s WHERE key = $1;", 36 } 37 ) 38 39 type sqlStore struct { 40 options store.Options 41 db *sql.DB 42 43 sync.RWMutex 44 // known databases 45 databases map[string]bool 46 } 47 48 func (s *sqlStore) getDB(database, table string) (string, string) { 49 if len(database) == 0 { 50 if len(s.options.Database) > 0 { 51 database = s.options.Database 52 } else { 53 database = DefaultDatabase 54 } 55 } 56 57 if len(table) == 0 { 58 if len(s.options.Table) > 0 { 59 table = s.options.Table 60 } else { 61 database = DefaultTable 62 } 63 } 64 65 // store.namespace must only contain letters, numbers and underscores 66 database = re.ReplaceAllString(database, "_") 67 table = re.ReplaceAllString(table, "_") 68 69 return database, table 70 } 71 72 func (s *sqlStore) createDB(database, table string) error { 73 database, table = s.getDB(database, table) 74 75 s.Lock() 76 defer s.Unlock() 77 78 if _, ok := s.databases[database+":"+table]; ok { 79 return nil 80 } 81 82 if err := s.initDB(database, table); err != nil { 83 return err 84 } 85 86 s.databases[database+":"+table] = true 87 return nil 88 } 89 90 func (s *sqlStore) initDB(database, table string) error { 91 if s.db == nil { 92 return errors.New("Database connection not initialised") 93 } 94 95 // Create the namespace's database 96 _, err := s.db.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s;", database)) 97 if err != nil { 98 return err 99 } 100 101 _, err = s.db.Exec(fmt.Sprintf("SET DATABASE = %s;", database)) 102 if err != nil { 103 return errors.Wrap(err, "Couldn't set database") 104 } 105 106 // Create a table for the namespace's prefix 107 _, err = s.db.Exec(fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s 108 ( 109 key text NOT NULL, 110 value bytea, 111 metadata JSONB, 112 expiry timestamp with time zone, 113 CONSTRAINT %s_pkey PRIMARY KEY (key) 114 );`, table, table)) 115 if err != nil { 116 return errors.Wrap(err, "Couldn't create table") 117 } 118 119 // Create Index 120 _, err = s.db.Exec(fmt.Sprintf(`CREATE INDEX IF NOT EXISTS "%s" ON %s.%s USING btree ("key");`, "key_index_"+table, database, table)) 121 if err != nil { 122 return err 123 } 124 125 // Create Metadata Index 126 _, err = s.db.Exec(fmt.Sprintf(`CREATE INDEX IF NOT EXISTS "%s" ON %s.%s USING GIN ("metadata");`, "metadata_index_"+table, database, table)) 127 if err != nil { 128 return err 129 } 130 131 return nil 132 } 133 134 func (s *sqlStore) configure() error { 135 if len(s.options.Nodes) == 0 { 136 s.options.Nodes = []string{"postgresql://root@localhost:26257?sslmode=disable"} 137 } 138 139 source := s.options.Nodes[0] 140 // check if it is a standard connection string eg: host=%s port=%d user=%s password=%s dbname=%s sslmode=disable 141 // if err is nil which means it would be a URL like postgre://xxxx?yy=zz 142 _, err := url.Parse(source) 143 if err != nil { 144 if !strings.Contains(source, " ") { 145 source = fmt.Sprintf("host=%s", source) 146 } 147 } 148 149 // create source from first node 150 db, err := sql.Open("postgres", source) 151 if err != nil { 152 return err 153 } 154 155 if err := db.Ping(); err != nil { 156 return err 157 } 158 159 if s.db != nil { 160 s.db.Close() 161 } 162 163 // save the values 164 s.db = db 165 166 // get DB 167 database, table := s.getDB(s.options.Database, s.options.Table) 168 169 // initialise the database 170 return s.initDB(database, table) 171 } 172 173 func (s *sqlStore) prepare(database, table, query string) (*sql.Stmt, error) { 174 st, ok := statements[query] 175 if !ok { 176 return nil, errors.New("unsupported statement") 177 } 178 179 // get DB 180 database, table = s.getDB(database, table) 181 182 q := fmt.Sprintf(st, database, table) 183 stmt, err := s.db.Prepare(q) 184 if err != nil { 185 return nil, err 186 } 187 return stmt, nil 188 } 189 190 func (s *sqlStore) Close() error { 191 if s.db != nil { 192 return s.db.Close() 193 } 194 return nil 195 } 196 197 func (s *sqlStore) Init(opts ...store.Option) error { 198 for _, o := range opts { 199 o(&s.options) 200 } 201 // reconfigure 202 return s.configure() 203 } 204 205 // List all the known records 206 func (s *sqlStore) List(opts ...store.ListOption) ([]string, error) { 207 var options store.ListOptions 208 for _, o := range opts { 209 o(&options) 210 } 211 212 // create the db if not exists 213 if err := s.createDB(options.Database, options.Table); err != nil { 214 return nil, err 215 } 216 217 st, err := s.prepare(options.Database, options.Table, "list") 218 if err != nil { 219 return nil, err 220 } 221 defer st.Close() 222 223 rows, err := st.Query() 224 if err != nil { 225 if err == sql.ErrNoRows { 226 return nil, nil 227 } 228 return nil, err 229 } 230 defer rows.Close() 231 232 var keys []string 233 var timehelper pq.NullTime 234 235 for rows.Next() { 236 record := &store.Record{} 237 metadata := make(Metadata) 238 239 if err := rows.Scan(&record.Key, &record.Value, &metadata, &timehelper); err != nil { 240 return keys, err 241 } 242 243 // set the metadata 244 record.Metadata = toMetadata(&metadata) 245 246 if timehelper.Valid { 247 if timehelper.Time.Before(time.Now()) { 248 // record has expired 249 go s.Delete(record.Key) 250 } else { 251 record.Expiry = time.Until(timehelper.Time) 252 keys = append(keys, record.Key) 253 } 254 } else { 255 keys = append(keys, record.Key) 256 } 257 258 } 259 rowErr := rows.Close() 260 if rowErr != nil { 261 // transaction rollback or something 262 return keys, rowErr 263 } 264 if err := rows.Err(); err != nil { 265 return keys, err 266 } 267 return keys, nil 268 } 269 270 // Read a single key 271 func (s *sqlStore) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) { 272 var options store.ReadOptions 273 for _, o := range opts { 274 o(&options) 275 } 276 277 // create the db if not exists 278 if err := s.createDB(options.Database, options.Table); err != nil { 279 return nil, err 280 } 281 282 if options.Prefix || options.Suffix { 283 return s.read(key, options) 284 } 285 286 var records []*store.Record 287 var timehelper pq.NullTime 288 289 st, err := s.prepare(options.Database, options.Table, "read") 290 if err != nil { 291 return nil, err 292 } 293 defer st.Close() 294 295 row := st.QueryRow(key) 296 record := &store.Record{} 297 metadata := make(Metadata) 298 299 if err := row.Scan(&record.Key, &record.Value, &metadata, &timehelper); err != nil { 300 if err == sql.ErrNoRows { 301 return records, store.ErrNotFound 302 } 303 return records, err 304 } 305 306 // set the metadata 307 record.Metadata = toMetadata(&metadata) 308 309 if timehelper.Valid { 310 if timehelper.Time.Before(time.Now()) { 311 // record has expired 312 go s.Delete(key) 313 return records, store.ErrNotFound 314 } 315 record.Expiry = time.Until(timehelper.Time) 316 records = append(records, record) 317 } else { 318 records = append(records, record) 319 } 320 321 return records, nil 322 } 323 324 // Read Many records 325 func (s *sqlStore) read(key string, options store.ReadOptions) ([]*store.Record, error) { 326 pattern := "%" 327 if options.Prefix { 328 pattern = key + pattern 329 } 330 if options.Suffix { 331 pattern = pattern + key 332 } 333 334 var rows *sql.Rows 335 var err error 336 337 if options.Limit != 0 { 338 st, err := s.prepare(options.Database, options.Table, "readOffset") 339 if err != nil { 340 return nil, err 341 } 342 defer st.Close() 343 344 rows, err = st.Query(pattern, options.Limit, options.Offset) 345 } else { 346 st, err := s.prepare(options.Database, options.Table, "readMany") 347 if err != nil { 348 return nil, err 349 } 350 defer st.Close() 351 352 rows, err = st.Query(pattern) 353 } 354 if err != nil { 355 if err == sql.ErrNoRows { 356 return []*store.Record{}, nil 357 } 358 return []*store.Record{}, errors.Wrap(err, "sqlStore.read failed") 359 } 360 361 defer rows.Close() 362 363 var records []*store.Record 364 var timehelper pq.NullTime 365 366 for rows.Next() { 367 record := &store.Record{} 368 metadata := make(Metadata) 369 370 if err := rows.Scan(&record.Key, &record.Value, &metadata, &timehelper); err != nil { 371 return records, err 372 } 373 374 // set the metadata 375 record.Metadata = toMetadata(&metadata) 376 377 if timehelper.Valid { 378 if timehelper.Time.Before(time.Now()) { 379 // record has expired 380 go s.Delete(record.Key) 381 } else { 382 record.Expiry = time.Until(timehelper.Time) 383 records = append(records, record) 384 } 385 } else { 386 records = append(records, record) 387 } 388 } 389 rowErr := rows.Close() 390 if rowErr != nil { 391 // transaction rollback or something 392 return records, rowErr 393 } 394 if err := rows.Err(); err != nil { 395 return records, err 396 } 397 398 return records, nil 399 } 400 401 // Write records 402 func (s *sqlStore) Write(r *store.Record, opts ...store.WriteOption) error { 403 var options store.WriteOptions 404 for _, o := range opts { 405 o(&options) 406 } 407 408 // create the db if not exists 409 if err := s.createDB(options.Database, options.Table); err != nil { 410 return err 411 } 412 413 st, err := s.prepare(options.Database, options.Table, "write") 414 if err != nil { 415 return err 416 } 417 defer st.Close() 418 419 metadata := make(Metadata) 420 for k, v := range r.Metadata { 421 metadata[k] = v 422 } 423 424 if r.Expiry != 0 { 425 _, err = st.Exec(r.Key, r.Value, metadata, time.Now().Add(r.Expiry)) 426 } else { 427 _, err = st.Exec(r.Key, r.Value, metadata, nil) 428 } 429 430 if err != nil { 431 return errors.Wrap(err, "Couldn't insert record "+r.Key) 432 } 433 434 return nil 435 } 436 437 // Delete records with keys 438 func (s *sqlStore) Delete(key string, opts ...store.DeleteOption) error { 439 var options store.DeleteOptions 440 for _, o := range opts { 441 o(&options) 442 } 443 444 // create the db if not exists 445 if err := s.createDB(options.Database, options.Table); err != nil { 446 return err 447 } 448 449 st, err := s.prepare(options.Database, options.Table, "delete") 450 if err != nil { 451 return err 452 } 453 defer st.Close() 454 455 result, err := st.Exec(key) 456 if err != nil { 457 return err 458 } 459 460 _, err = result.RowsAffected() 461 if err != nil { 462 return err 463 } 464 465 return nil 466 } 467 468 func (s *sqlStore) Options() store.Options { 469 return s.options 470 } 471 472 func (s *sqlStore) String() string { 473 return "cockroach" 474 } 475 476 // NewStore returns a new micro Store backed by sql 477 func NewStore(opts ...store.Option) store.Store { 478 options := store.Options{ 479 Database: DefaultDatabase, 480 Table: DefaultTable, 481 } 482 483 for _, o := range opts { 484 o(&options) 485 } 486 487 // new store 488 s := new(sqlStore) 489 // set the options 490 s.options = options 491 // mark known databases 492 s.databases = make(map[string]bool) 493 // best-effort configure the store 494 if err := s.configure(); err != nil { 495 if logger.V(logger.ErrorLevel, logger.DefaultLogger) { 496 logger.Error("Error configuring store ", err) 497 } 498 } 499 500 // return store 501 return s 502 }