github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/workload/movr/movr.go (about) 1 // Copyright 2019 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package movr 12 13 import ( 14 gosql "database/sql" 15 "fmt" 16 "math" 17 "strings" 18 "sync" 19 "time" 20 21 "github.com/cockroachdb/cockroach/pkg/util/uuid" 22 "github.com/cockroachdb/cockroach/pkg/workload" 23 "github.com/cockroachdb/cockroach/pkg/workload/faker" 24 "github.com/cockroachdb/errors" 25 "github.com/spf13/pflag" 26 "golang.org/x/exp/rand" 27 ) 28 29 // Indexes into the slice returned by `Tables`. 30 const ( 31 TablesUsersIdx = 0 32 TablesVehiclesIdx = 1 33 TablesRidesIdx = 2 34 TablesVehicleLocationHistoriesIdx = 3 35 TablesPromoCodesIdx = 4 36 TablesUserPromoCodesIdx = 5 37 ) 38 39 const movrUsersSchema = `( 40 id UUID NOT NULL, 41 city VARCHAR NOT NULL, 42 name VARCHAR NULL, 43 address VARCHAR NULL, 44 credit_card VARCHAR NULL, 45 PRIMARY KEY (city ASC, id ASC) 46 )` 47 48 // Indexes into the rows in movrUsers. 49 const ( 50 usersIDIdx = 0 51 usersCityIdx = 1 52 ) 53 54 const movrVehiclesSchema = `( 55 id UUID NOT NULL, 56 city VARCHAR NOT NULL, 57 type VARCHAR NULL, 58 owner_id UUID NULL, 59 creation_time TIMESTAMP NULL, 60 status VARCHAR NULL, 61 current_location VARCHAR NULL, 62 ext JSONB NULL, 63 PRIMARY KEY (city ASC, id ASC), 64 INDEX vehicles_auto_index_fk_city_ref_users (city ASC, owner_id ASC) 65 )` 66 67 // Indexes into the rows in movrVehicles. 68 const ( 69 vehiclesIDIdx = 0 70 vehiclesCityIdx = 1 71 ) 72 73 const movrRidesSchema = `( 74 id UUID NOT NULL, 75 city VARCHAR NOT NULL, 76 vehicle_city VARCHAR NULL, 77 rider_id UUID NULL, 78 vehicle_id UUID NULL, 79 start_address VARCHAR NULL, 80 end_address VARCHAR NULL, 81 start_time TIMESTAMP NULL, 82 end_time TIMESTAMP NULL, 83 revenue DECIMAL(10,2) NULL, 84 PRIMARY KEY (city ASC, id ASC), 85 INDEX rides_auto_index_fk_city_ref_users (city ASC, rider_id ASC), 86 INDEX rides_auto_index_fk_vehicle_city_ref_vehicles (vehicle_city ASC, vehicle_id ASC), 87 CONSTRAINT check_vehicle_city_city CHECK (vehicle_city = city) 88 )` 89 90 // Indexes into the rows in movrRides. 91 const ( 92 ridesIDIdx = 0 93 ridesCityIdx = 1 94 ) 95 96 const movrVehicleLocationHistoriesSchema = `( 97 city VARCHAR NOT NULL, 98 ride_id UUID NOT NULL, 99 "timestamp" TIMESTAMP NOT NULL, 100 lat FLOAT8 NULL, 101 long FLOAT8 NULL, 102 PRIMARY KEY (city ASC, ride_id ASC, "timestamp" ASC) 103 )` 104 const movrPromoCodesSchema = `( 105 code VARCHAR NOT NULL, 106 description VARCHAR NULL, 107 creation_time TIMESTAMP NULL, 108 expiration_time TIMESTAMP NULL, 109 rules JSONB NULL, 110 PRIMARY KEY (code ASC) 111 )` 112 const movrUserPromoCodesSchema = `( 113 city VARCHAR NOT NULL, 114 user_id UUID NOT NULL, 115 code VARCHAR NOT NULL, 116 "timestamp" TIMESTAMP NULL, 117 usage_count INT NULL, 118 PRIMARY KEY (city ASC, user_id ASC, code ASC) 119 )` 120 121 const ( 122 timestampFormat = "2006-01-02 15:04:05.999999-07:00" 123 ) 124 125 var cities = []struct { 126 city string 127 locality string 128 }{ 129 {city: "new york", locality: "us_east"}, 130 {city: "boston", locality: "us_east"}, 131 {city: "washington dc", locality: "us_east"}, 132 {city: "seattle", locality: "us_west"}, 133 {city: "san francisco", locality: "us_west"}, 134 {city: "los angeles", locality: "us_west"}, 135 {city: "amsterdam", locality: "eu_west"}, 136 {city: "paris", locality: "eu_west"}, 137 {city: "rome", locality: "eu_west"}, 138 } 139 140 type movr struct { 141 flags workload.Flags 142 connFlags *workload.ConnFlags 143 144 seed uint64 145 users, vehicles, rides, histories cityDistributor 146 numPromoCodes int 147 ranges int 148 149 creationTime time.Time 150 151 fakerOnce sync.Once 152 faker faker.Faker 153 } 154 155 func init() { 156 workload.Register(movrMeta) 157 } 158 159 var movrMeta = workload.Meta{ 160 Name: `movr`, 161 Description: `MovR is a fictional vehicle sharing company`, 162 Version: `1.0.0`, 163 PublicFacing: true, 164 New: func() workload.Generator { 165 g := &movr{} 166 g.flags.FlagSet = pflag.NewFlagSet(`movr`, pflag.ContinueOnError) 167 g.flags.Uint64Var(&g.seed, `seed`, 1, `Key hash seed.`) 168 g.flags.IntVar(&g.users.numRows, `num-users`, 50, `Initial number of users.`) 169 g.flags.IntVar(&g.vehicles.numRows, `num-vehicles`, 15, `Initial number of vehicles.`) 170 g.flags.IntVar(&g.rides.numRows, `num-rides`, 500, `Initial number of rides.`) 171 g.flags.IntVar(&g.histories.numRows, `num-histories`, 1000, 172 `Initial number of ride location histories.`) 173 g.flags.IntVar(&g.numPromoCodes, `num-promo-codes`, 1000, `Initial number of promo codes.`) 174 g.flags.IntVar(&g.ranges, `num-ranges`, 9, `Initial number of ranges to break the tables into`) 175 g.connFlags = workload.NewConnFlags(&g.flags) 176 g.creationTime = time.Date(2019, 1, 2, 3, 4, 5, 6, time.UTC) 177 return g 178 }, 179 } 180 181 // Meta implements the Generator interface. 182 func (*movr) Meta() workload.Meta { return movrMeta } 183 184 // Flags implements the Flagser interface. 185 func (g *movr) Flags() workload.Flags { return g.flags } 186 187 // Hooks implements the Hookser interface. 188 func (g *movr) Hooks() workload.Hooks { 189 return workload.Hooks{ 190 Validate: func() error { 191 // Force there to be at least one user/vehicle/ride/history per city. 192 // Otherwise, some cities will be empty, which means we can't construct 193 // the FKs we need. 194 if g.users.numRows < len(cities) { 195 return errors.Errorf(`at least %d users are required`, len(cities)) 196 } 197 if g.vehicles.numRows < len(cities) { 198 return errors.Errorf(`at least %d vehicles are required`, len(cities)) 199 } 200 if g.rides.numRows < len(cities) { 201 return errors.Errorf(`at least %d rides are required`, len(cities)) 202 } 203 if g.histories.numRows < len(cities) { 204 return errors.Errorf(`at least %d histories are required`, len(cities)) 205 } 206 return nil 207 }, 208 PostLoad: func(db *gosql.DB) error { 209 fkStmts := []string{ 210 `ALTER TABLE vehicles ADD FOREIGN KEY ` + 211 `(city, owner_id) REFERENCES users (city, id)`, 212 `ALTER TABLE rides ADD FOREIGN KEY ` + 213 `(city, rider_id) REFERENCES users (city, id)`, 214 `ALTER TABLE rides ADD FOREIGN KEY ` + 215 `(vehicle_city, vehicle_id) REFERENCES vehicles (city, id)`, 216 `ALTER TABLE vehicle_location_histories ADD FOREIGN KEY ` + 217 `(city, ride_id) REFERENCES rides (city, id)`, 218 `ALTER TABLE user_promo_codes ADD FOREIGN KEY ` + 219 `(city, user_id) REFERENCES users (city, id)`, 220 } 221 222 for _, fkStmt := range fkStmts { 223 if _, err := db.Exec(fkStmt); err != nil { 224 // If the statement failed because the fk already exists, 225 // ignore it. Return the error for any other reason. 226 const duplicateFKErr = "columns cannot be used by multiple foreign key constraints" 227 if !strings.Contains(err.Error(), duplicateFKErr) { 228 return err 229 } 230 } 231 } 232 return nil 233 }, 234 // This partitioning step is intended for a 3 region cluster, which have the localities region=us-east1, 235 // region=us-west1, region=europe-west1. 236 Partition: func(db *gosql.DB) error { 237 // Create us-west, us-east and europe-west partitions. 238 q := ` 239 ALTER TABLE users PARTITION BY LIST (city) ( 240 PARTITION us_west VALUES IN ('seattle', 'san francisco', 'los angeles'), 241 PARTITION us_east VALUES IN ('new york', 'boston', 'washington dc'), 242 PARTITION europe_west VALUES IN ('amsterdam', 'paris', 'rome') 243 ); 244 ALTER TABLE vehicles PARTITION BY LIST (city) ( 245 PARTITION us_west VALUES IN ('seattle', 'san francisco', 'los angeles'), 246 PARTITION us_east VALUES IN ('new york', 'boston', 'washington dc'), 247 PARTITION europe_west VALUES IN ('amsterdam', 'paris', 'rome') 248 ); 249 ALTER INDEX vehicles_auto_index_fk_city_ref_users PARTITION BY LIST (city) ( 250 PARTITION us_west VALUES IN ('seattle', 'san francisco', 'los angeles'), 251 PARTITION us_east VALUES IN ('new york', 'boston', 'washington dc'), 252 PARTITION europe_west VALUES IN ('amsterdam', 'paris', 'rome') 253 ); 254 ALTER TABLE rides PARTITION BY LIST (city) ( 255 PARTITION us_west VALUES IN ('seattle', 'san francisco', 'los angeles'), 256 PARTITION us_east VALUES IN ('new york', 'boston', 'washington dc'), 257 PARTITION europe_west VALUES IN ('amsterdam', 'paris', 'rome') 258 ); 259 ALTER INDEX rides_auto_index_fk_city_ref_users PARTITION BY LIST (city) ( 260 PARTITION us_west VALUES IN ('seattle', 'san francisco', 'los angeles'), 261 PARTITION us_east VALUES IN ('new york', 'boston', 'washington dc'), 262 PARTITION europe_west VALUES IN ('amsterdam', 'paris', 'rome') 263 ); 264 ALTER INDEX rides_auto_index_fk_vehicle_city_ref_vehicles PARTITION BY LIST (vehicle_city) ( 265 PARTITION us_west VALUES IN ('seattle', 'san francisco', 'los angeles'), 266 PARTITION us_east VALUES IN ('new york', 'boston', 'washington dc'), 267 PARTITION europe_west VALUES IN ('amsterdam', 'paris', 'rome') 268 ); 269 ALTER TABLE user_promo_codes PARTITION BY LIST (city) ( 270 PARTITION us_west VALUES IN ('seattle', 'san francisco', 'los angeles'), 271 PARTITION us_east VALUES IN ('new york', 'boston', 'washington dc'), 272 PARTITION europe_west VALUES IN ('amsterdam', 'paris', 'rome') 273 ); 274 ALTER TABLE vehicle_location_histories PARTITION BY LIST (city) ( 275 PARTITION us_west VALUES IN ('seattle', 'san francisco', 'los angeles'), 276 PARTITION us_east VALUES IN ('new york', 'boston', 'washington dc'), 277 PARTITION europe_west VALUES IN ('amsterdam', 'paris', 'rome') 278 ); 279 ` 280 if _, err := db.Exec(q); err != nil { 281 return err 282 } 283 284 // Alter the partitions to place replicas in the appropriate zones. 285 q = ` 286 ALTER PARTITION us_west OF INDEX users@* CONFIGURE ZONE USING CONSTRAINTS='["+region=us-west1"]'; 287 ALTER PARTITION us_east OF INDEX users@* CONFIGURE ZONE USING CONSTRAINTS='["+region=us-east1"]'; 288 ALTER PARTITION europe_west OF INDEX users@* CONFIGURE ZONE USING CONSTRAINTS='["+region=europe-west1"]'; 289 290 ALTER PARTITION us_west OF INDEX vehicles@* CONFIGURE ZONE USING CONSTRAINTS='["+region=us-west1"]'; 291 ALTER PARTITION us_east OF INDEX vehicles@* CONFIGURE ZONE USING CONSTRAINTS='["+region=us-east1"]'; 292 ALTER PARTITION europe_west OF INDEX vehicles@* CONFIGURE ZONE USING CONSTRAINTS='["+region=europe-west1"]'; 293 294 ALTER PARTITION us_west OF INDEX rides@* CONFIGURE ZONE USING CONSTRAINTS='["+region=us-west1"]'; 295 ALTER PARTITION us_east OF INDEX rides@* CONFIGURE ZONE USING CONSTRAINTS='["+region=us-east1"]'; 296 ALTER PARTITION europe_west OF INDEX rides@* CONFIGURE ZONE USING CONSTRAINTS='["+region=europe-west1"]'; 297 298 ALTER PARTITION us_west OF INDEX user_promo_codes@* CONFIGURE ZONE USING CONSTRAINTS='["+region=us-west1"]'; 299 ALTER PARTITION us_east OF INDEX user_promo_codes@* CONFIGURE ZONE USING CONSTRAINTS='["+region=us-east1"]'; 300 ALTER PARTITION europe_west OF INDEX user_promo_codes@* CONFIGURE ZONE USING CONSTRAINTS='["+region=europe-west1"]'; 301 302 ALTER PARTITION us_west OF INDEX vehicle_location_histories@* CONFIGURE ZONE USING CONSTRAINTS='["+region=us-west1"]'; 303 ALTER PARTITION us_east OF INDEX vehicle_location_histories@* CONFIGURE ZONE USING CONSTRAINTS='["+region=us-east1"]'; 304 ALTER PARTITION europe_west OF INDEX vehicle_location_histories@* CONFIGURE ZONE USING CONSTRAINTS='["+region=europe-west1"]'; 305 ` 306 if _, err := db.Exec(q); err != nil { 307 return err 308 } 309 310 // Create some duplicate indexes for the promo_codes table. 311 q = ` 312 CREATE INDEX promo_codes_idx_us_west ON promo_codes (code) STORING (description, creation_time, expiration_time, rules); 313 CREATE INDEX promo_codes_idx_europe_west ON promo_codes (code) STORING (description, creation_time, expiration_time, rules); 314 ` 315 if _, err := db.Exec(q); err != nil { 316 return err 317 } 318 319 // Apply configurations to the index for fast reads. 320 q = ` 321 ALTER TABLE promo_codes CONFIGURE ZONE USING num_replicas = 3, 322 constraints = '{"+region=us-east1": 1}', 323 lease_preferences = '[[+region=us-east1]]'; 324 ALTER INDEX promo_codes@promo_codes_idx_us_west CONFIGURE ZONE USING 325 num_replicas = 3, 326 constraints = '{"+region=us-west1": 1}', 327 lease_preferences = '[[+region=us-west1]]'; 328 ALTER INDEX promo_codes@promo_codes_idx_europe_west CONFIGURE ZONE USING 329 num_replicas = 3, 330 constraints = '{"+region=europe-west1": 1}', 331 lease_preferences = '[[+region=europe-west1]]'; 332 ` 333 if _, err := db.Exec(q); err != nil { 334 return err 335 } 336 return nil 337 }, 338 } 339 } 340 341 // Tables implements the Generator interface. 342 func (g *movr) Tables() []workload.Table { 343 g.fakerOnce.Do(func() { 344 g.faker = faker.NewFaker() 345 }) 346 tables := make([]workload.Table, 6) 347 tables[TablesUsersIdx] = workload.Table{ 348 Name: `users`, 349 Schema: movrUsersSchema, 350 InitialRows: workload.Tuples( 351 g.users.numRows, 352 g.movrUsersInitialRow, 353 ), 354 Splits: workload.Tuples( 355 g.ranges-1, 356 func(splitIdx int) []interface{} { 357 row := g.movrUsersInitialRow((splitIdx + 1) * (g.users.numRows / g.ranges)) 358 // The split tuples returned must be valid primary key columns. 359 return []interface{}{row[usersCityIdx], row[usersIDIdx]} 360 }, 361 ), 362 } 363 tables[TablesVehiclesIdx] = workload.Table{ 364 Name: `vehicles`, 365 Schema: movrVehiclesSchema, 366 InitialRows: workload.Tuples( 367 g.vehicles.numRows, 368 g.movrVehiclesInitialRow, 369 ), 370 Splits: workload.Tuples( 371 g.ranges-1, 372 func(splitIdx int) []interface{} { 373 row := g.movrVehiclesInitialRow((splitIdx + 1) * (g.vehicles.numRows / g.ranges)) 374 // The split tuples returned must be valid primary key columns. 375 return []interface{}{row[vehiclesCityIdx], row[vehiclesIDIdx]} 376 }, 377 ), 378 } 379 tables[TablesRidesIdx] = workload.Table{ 380 Name: `rides`, 381 Schema: movrRidesSchema, 382 InitialRows: workload.Tuples( 383 g.rides.numRows, 384 g.movrRidesInitialRow, 385 ), 386 Splits: workload.Tuples( 387 g.ranges-1, 388 func(splitIdx int) []interface{} { 389 row := g.movrRidesInitialRow((splitIdx + 1) * (g.rides.numRows / g.ranges)) 390 // The split tuples returned must be valid primary key columns. 391 return []interface{}{row[ridesCityIdx], row[ridesIDIdx]} 392 }, 393 ), 394 } 395 tables[TablesVehicleLocationHistoriesIdx] = workload.Table{ 396 Name: `vehicle_location_histories`, 397 Schema: movrVehicleLocationHistoriesSchema, 398 InitialRows: workload.Tuples( 399 g.histories.numRows, 400 g.movrVehicleLocationHistoriesInitialRow, 401 ), 402 } 403 tables[TablesPromoCodesIdx] = workload.Table{ 404 Name: `promo_codes`, 405 Schema: movrPromoCodesSchema, 406 InitialRows: workload.Tuples( 407 g.numPromoCodes, 408 g.movrPromoCodesInitialRow, 409 ), 410 } 411 tables[TablesUserPromoCodesIdx] = workload.Table{ 412 Name: `user_promo_codes`, 413 Schema: movrUserPromoCodesSchema, 414 InitialRows: workload.Tuples( 415 0, 416 func(_ int) []interface{} { panic(`unimplemented`) }, 417 ), 418 } 419 return tables 420 } 421 422 // cityDistributor deterministically maps each of numRows to a city. It also 423 // maps a city back to a range of rows. This allows the generator functions 424 // below to select random rows from the same city in another table. numRows is 425 // required to be at least `len(cities)`. 426 type cityDistributor struct { 427 numRows int 428 } 429 430 func (d cityDistributor) cityForRow(rowIdx int) int { 431 if d.numRows < len(cities) { 432 panic(errors.Errorf(`a minimum of %d rows are required got %d`, len(cities), d.numRows)) 433 } 434 numPerCity := float64(d.numRows) / float64(len(cities)) 435 cityIdx := int(float64(rowIdx) / numPerCity) 436 return cityIdx 437 } 438 439 func (d cityDistributor) rowsForCity(cityIdx int) (min, max int) { 440 if d.numRows < len(cities) { 441 panic(errors.Errorf(`a minimum of %d rows are required got %d`, len(cities), d.numRows)) 442 } 443 numPerCity := float64(d.numRows) / float64(len(cities)) 444 min = int(math.Ceil(float64(cityIdx) * numPerCity)) 445 max = int(math.Ceil(float64(cityIdx+1) * numPerCity)) 446 if min >= d.numRows { 447 min = d.numRows 448 } 449 if max >= d.numRows { 450 max = d.numRows 451 } 452 return min, max 453 } 454 455 func (d cityDistributor) randRowInCity(rng *rand.Rand, cityIdx int) int { 456 min, max := d.rowsForCity(cityIdx) 457 return min + rng.Intn(max-min) 458 } 459 460 func (g *movr) movrUsersInitialRow(rowIdx int) []interface{} { 461 rng := rand.New(rand.NewSource(g.seed + uint64(rowIdx))) 462 cityIdx := g.users.cityForRow(rowIdx) 463 city := cities[cityIdx] 464 465 // Make evenly-spaced UUIDs sorted in the same order as the rows. 466 var id uuid.UUID 467 id.DeterministicV4(uint64(rowIdx), uint64(g.users.numRows)) 468 469 return []interface{}{ 470 id.String(), // id 471 city.city, // city 472 g.faker.Name(rng), // name 473 g.faker.StreetAddress(rng), // address 474 randCreditCard(rng), // credit_card 475 } 476 } 477 478 func (g *movr) movrVehiclesInitialRow(rowIdx int) []interface{} { 479 rng := rand.New(rand.NewSource(g.seed + uint64(rowIdx))) 480 cityIdx := g.vehicles.cityForRow(rowIdx) 481 city := cities[cityIdx] 482 483 // Make evenly-spaced UUIDs sorted in the same order as the rows. 484 var id uuid.UUID 485 id.DeterministicV4(uint64(rowIdx), uint64(g.vehicles.numRows)) 486 487 vehicleType := randVehicleType(rng) 488 ownerRowIdx := g.users.randRowInCity(rng, cityIdx) 489 ownerID := g.movrUsersInitialRow(ownerRowIdx)[0] 490 491 return []interface{}{ 492 id.String(), // id 493 city.city, // city 494 vehicleType, // type 495 ownerID, // owner_id 496 g.creationTime.Format(timestampFormat), // creation_time 497 randVehicleStatus(rng), // status 498 g.faker.StreetAddress(rng), // current_location 499 randVehicleMetadata(rng, vehicleType), // ext 500 } 501 } 502 503 func (g *movr) movrRidesInitialRow(rowIdx int) []interface{} { 504 rng := rand.New(rand.NewSource(g.seed + uint64(rowIdx))) 505 cityIdx := g.rides.cityForRow(rowIdx) 506 city := cities[cityIdx] 507 508 // Make evenly-spaced UUIDs sorted in the same order as the rows. 509 var id uuid.UUID 510 id.DeterministicV4(uint64(rowIdx), uint64(g.rides.numRows)) 511 512 riderRowIdx := g.users.randRowInCity(rng, cityIdx) 513 riderID := g.movrUsersInitialRow(riderRowIdx)[0] 514 vehicleRowIdx := g.vehicles.randRowInCity(rng, cityIdx) 515 vehicleID := g.movrVehiclesInitialRow(vehicleRowIdx)[0] 516 startTime := g.creationTime.Add(-time.Duration(rng.Intn(30)) * 24 * time.Hour) 517 endTime := startTime.Add(time.Duration(rng.Intn(60)) * time.Hour) 518 519 return []interface{}{ 520 id.String(), // id 521 city.city, // city 522 city.city, // vehicle_city 523 riderID, // rider_id 524 vehicleID, // vehicle_id 525 g.faker.StreetAddress(rng), // start_address 526 g.faker.StreetAddress(rng), // end_address 527 startTime.Format(timestampFormat), // start_time 528 endTime.Format(timestampFormat), // end_time 529 rng.Intn(100), // revenue 530 } 531 } 532 533 func (g *movr) movrVehicleLocationHistoriesInitialRow(rowIdx int) []interface{} { 534 rng := rand.New(rand.NewSource(g.seed + uint64(rowIdx))) 535 cityIdx := g.histories.cityForRow(rowIdx) 536 city := cities[cityIdx] 537 538 rideRowIdx := g.rides.randRowInCity(rng, cityIdx) 539 rideID := g.movrRidesInitialRow(rideRowIdx)[0] 540 time := g.creationTime.Add(time.Duration(rowIdx) * time.Millisecond) 541 lat, long := randLatLong(rng) 542 543 return []interface{}{ 544 city.city, // city 545 rideID, // ride_id, 546 time.Format(timestampFormat), // timestamp 547 lat, // lat 548 long, // long 549 } 550 } 551 552 func (g *movr) movrPromoCodesInitialRow(rowIdx int) []interface{} { 553 rng := rand.New(rand.NewSource(g.seed + uint64(rowIdx))) 554 code := strings.ToLower(strings.Join(g.faker.Words(rng, 3), `_`)) 555 code = fmt.Sprintf("%d_%s", rowIdx, code) 556 description := g.faker.Paragraph(rng) 557 expirationTime := g.creationTime.Add(time.Duration(rng.Intn(30)) * 24 * time.Hour) 558 // TODO(dan): This is nil in the reference impl, is that intentional? 559 creationTime := expirationTime.Add(-time.Duration(rng.Intn(30)) * 24 * time.Hour) 560 const rulesJSON = `{"type": "percent_discount", "value": "10%"}` 561 562 return []interface{}{ 563 code, // code 564 description, // description 565 creationTime, // creation_time 566 expirationTime, // expiration_time 567 rulesJSON, // rules 568 } 569 }