Skip to content

Commit 398f8b0

Browse files
committed
feat: exposed _index for better generator reliability; added on conflict support
1 parent 44b395b commit 398f8b0

4 files changed

Lines changed: 108 additions & 26 deletions

File tree

regresql/fixture.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ type (
1616
}
1717

1818
GenerateSpec struct {
19-
Table string `yaml:"table" json:"table"`
20-
Count int `yaml:"count" json:"count"`
21-
Columns map[string]GeneratorSpec `yaml:"columns" json:"columns"`
19+
Table string `yaml:"table" json:"table"`
20+
Count int `yaml:"count" json:"count"`
21+
Columns map[string]GeneratorSpec `yaml:"columns" json:"columns"`
22+
OnConflict string `yaml:"on_conflict,omitempty" json:"on_conflict,omitempty"`
2223
}
2324

2425
GeneratorSpec struct {

regresql/fixture_manager.go

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -402,26 +402,22 @@ func (fm *FixtureManager) generateTableData(genSpec GenerateSpec) error {
402402
count = genSpec.Count - i
403403
}
404404

405-
if err := fm.generateAndInsertBatch(genSpec, tableInfo, count); err != nil {
405+
if err := fm.generateAndInsertBatch(genSpec, tableInfo, count, i); err != nil {
406406
return fmt.Errorf("failed to generate batch at row %d: %w", i, err)
407407
}
408408
}
409409

410410
return nil
411411
}
412412

413-
// generateAndInsertBatch generates and inserts a batch of rows
414-
func (fm *FixtureManager) generateAndInsertBatch(genSpec GenerateSpec, tableInfo *TableInfo, count int) error {
415-
// Collect column names
413+
func (fm *FixtureManager) generateAndInsertBatch(genSpec GenerateSpec, tableInfo *TableInfo, count, startIndex int) error {
416414
columns := make([]string, 0, len(genSpec.Columns))
417415
for colName := range genSpec.Columns {
418416
columns = append(columns, colName)
419417
}
420418

421-
// Build INSERT statement
422419
query := fmt.Sprintf("INSERT INTO %s (%s) VALUES ", genSpec.Table, joinColumns(columns))
423420

424-
// Generate rows
425421
var values []any
426422
valuePlaceholders := make([]string, 0, count)
427423

@@ -438,11 +434,11 @@ func (fm *FixtureManager) generateAndInsertBatch(genSpec GenerateSpec, tableInfo
438434
}
439435

440436
params := copyParams(genSpecCol.Params)
441-
params["_index"] = i
437+
params["_index"] = startIndex + i
442438

443439
value, err := gen.Generate(params, colInfo)
444440
if err != nil {
445-
return fmt.Errorf("failed to generate value for column '%s' (row %d): %w", colName, i, err)
441+
return fmt.Errorf("failed to generate value for column '%s' (row %d): %w", colName, startIndex+i, err)
446442
}
447443

448444
values = append(values, value)
@@ -452,8 +448,11 @@ func (fm *FixtureManager) generateAndInsertBatch(genSpec GenerateSpec, tableInfo
452448
valuePlaceholders = append(valuePlaceholders, fmt.Sprintf("(%s)", joinStrings(rowValues, ", ")))
453449
}
454450

455-
// Execute insert
456451
finalQuery := query + joinStrings(valuePlaceholders, ", ")
452+
if genSpec.OnConflict == "skip" {
453+
finalQuery += " ON CONFLICT DO NOTHING"
454+
}
455+
457456
if _, err := fm.tx.Exec(finalQuery, values...); err != nil {
458457
return err
459458
}

regresql/generators_basic.go

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -161,21 +161,37 @@ func NewStringGenerator() *StringGenerator {
161161
func (g *StringGenerator) Generate(params map[string]any, column *ColumnInfo) (any, error) {
162162
length := getParam(params, "length", 10)
163163
charset := getParam(params, "charset", defaultCharset)
164+
unique := getParam(params, "unique", false)
165+
index := getParam(params, "_index", -1)
164166

165167
if length <= 0 {
166168
return nil, fmt.Errorf("length must be positive")
167169
}
168170

169-
// Apply column max length constraint if available
170171
if column.MaxLength != nil && length > *column.MaxLength {
171172
length = *column.MaxLength
172173
}
173174

175+
// Deterministic unique string based on index
176+
if unique && index >= 0 {
177+
base := fmt.Sprintf("%d", index)
178+
if len(base) > length {
179+
return nil, fmt.Errorf("unique string generator: index %d requires %d chars but length is %d", index, len(base), length)
180+
}
181+
if len(base) == length {
182+
return base, nil
183+
}
184+
padding := make([]byte, length-len(base))
185+
for i := range padding {
186+
padding[i] = charset[(index+i)%len(charset)]
187+
}
188+
return string(padding) + base, nil
189+
}
190+
174191
result := make([]byte, length)
175192
for i := range result {
176193
result[i] = charset[rand.Intn(len(charset))]
177194
}
178-
179195
return string(result), nil
180196
}
181197

@@ -227,16 +243,20 @@ func NewEmailGenerator() *EmailGenerator {
227243

228244
func (g *EmailGenerator) Generate(params map[string]any, column *ColumnInfo) (any, error) {
229245
domain := getParam(params, "domain", "")
230-
231-
prefix := emailPrefixes[rand.Intn(len(emailPrefixes))]
232-
suffix := rand.Intn(10000)
246+
index := getParam(params, "_index", -1)
233247

234248
if domain == "" {
235249
domain = emailDomains[rand.Intn(len(emailDomains))]
236250
}
237251

238-
email := fmt.Sprintf("%s%d@%s", prefix, suffix, domain)
239-
return email, nil
252+
// Use index for guaranteed uniqueness when available
253+
if index >= 0 {
254+
return fmt.Sprintf("user%d@%s", index, domain), nil
255+
}
256+
257+
prefix := emailPrefixes[rand.Intn(len(emailPrefixes))]
258+
suffix := rand.Intn(10000)
259+
return fmt.Sprintf("%s%d@%s", prefix, suffix, domain), nil
240260
}
241261

242262
func (g *EmailGenerator) Validate(params map[string]any, column *ColumnInfo) error {
@@ -251,20 +271,42 @@ func NewNameGenerator() *NameGenerator {
251271

252272
func (g *NameGenerator) Generate(params map[string]any, column *ColumnInfo) (any, error) {
253273
nameType := getParam(params, "type", "full")
254-
255-
firstName := firstNames[rand.Intn(len(firstNames))]
256-
lastName := lastNames[rand.Intn(len(lastNames))]
257-
274+
unique := getParam(params, "unique", false)
275+
index := getParam(params, "_index", -1)
276+
277+
var firstName, lastName string
278+
if unique && index >= 0 {
279+
firstName = firstNames[index%len(firstNames)]
280+
lastName = lastNames[index%len(lastNames)]
281+
// Add suffix when names would cycle
282+
cycle := index / (len(firstNames) * len(lastNames))
283+
if cycle > 0 {
284+
lastName = fmt.Sprintf("%s%d", lastName, cycle)
285+
}
286+
} else {
287+
firstName = firstNames[rand.Intn(len(firstNames))]
288+
lastName = lastNames[rand.Intn(len(lastNames))]
289+
}
290+
291+
var result string
258292
switch nameType {
259293
case "first":
260-
return firstName, nil
294+
result = firstName
261295
case "last":
262-
return lastName, nil
296+
result = lastName
263297
case "full":
264-
return fmt.Sprintf("%s %s", firstName, lastName), nil
298+
result = fmt.Sprintf("%s %s", firstName, lastName)
265299
default:
266300
return nil, fmt.Errorf("unsupported name type: %s", nameType)
267301
}
302+
303+
if column.MaxLength != nil && len(result) > *column.MaxLength {
304+
if unique {
305+
return nil, fmt.Errorf("unique name generator: result %q (%d chars) exceeds column max length %d", result, len(result), *column.MaxLength)
306+
}
307+
result = result[:*column.MaxLength]
308+
}
309+
return result, nil
268310
}
269311

270312
func (g *NameGenerator) Validate(params map[string]any, column *ColumnInfo) error {

regresql/schema.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type (
2626
IsNullable bool
2727
IsPrimaryKey bool
2828
IsForeignKey bool
29+
IsUnique bool
2930
ForeignKey *ForeignKeyInfo
3031
Default *string
3132
MaxLength *int
@@ -95,6 +96,17 @@ func IntrospectSchema(db *sql.DB) (*DatabaseSchema, error) {
9596
}
9697
}
9798

99+
// Get unique constraints
100+
uniqueCols, err := getUniqueColumns(db, tableName)
101+
if err != nil {
102+
return nil, fmt.Errorf("failed to get unique constraints for table '%s': %w", tableName, err)
103+
}
104+
for colName := range uniqueCols {
105+
if col, exists := tableInfo.Columns[colName]; exists {
106+
col.IsUnique = true
107+
}
108+
}
109+
98110
schema.tables[tableName] = tableInfo
99111
}
100112

@@ -252,6 +264,34 @@ func getForeignKeys(db *sql.DB, tableName string) ([]*ForeignKeyInfo, error) {
252264
return foreignKeys, rows.Err()
253265
}
254266

267+
func getUniqueColumns(db *sql.DB, tableName string) (map[string]bool, error) {
268+
query := `
269+
SELECT a.attname
270+
FROM pg_index i
271+
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
272+
WHERE i.indrelid = $1::regclass
273+
AND i.indisunique
274+
AND NOT i.indisprimary
275+
AND array_length(i.indkey, 1) = 1
276+
`
277+
278+
rows, err := db.Query(query, tableName)
279+
if err != nil {
280+
return nil, err
281+
}
282+
defer rows.Close()
283+
284+
uniqueCols := make(map[string]bool)
285+
for rows.Next() {
286+
var colName string
287+
if err := rows.Scan(&colName); err != nil {
288+
return nil, err
289+
}
290+
uniqueCols[colName] = true
291+
}
292+
return uniqueCols, rows.Err()
293+
}
294+
255295
// GetTable retrieves table metadata
256296
func (ds *DatabaseSchema) GetTable(name string) (*TableInfo, error) {
257297
table, exists := ds.tables[name]

0 commit comments

Comments
 (0)