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  }