Franciszek Stachura, Sep 12, 2026 at 23:06:
> Generates random data for development or performance testing.
>
> Signed-off-by: Franciszek Stachura <[email protected]>
> ---
> For some reason, keeping a long-running connection with large bulk
> inserts causes Postgres to use a lot of memory, until the connection is
> closed. The only solution I found is making the connections
> short-lived with SetConnMaxLifetime
> (I believe that it should be used anyway).
> I don't see any good way of manually closing connections in Bun.
>
> cmd/pw/db/generate.go | 285 ++++++++++++++++++++++++++++++++++++++++++
> cmd/pw/db/main.go | 9 +-
> 2 files changed, 290 insertions(+), 4 deletions(-)
> create mode 100644 cmd/pw/db/generate.go
>
> diff --git a/cmd/pw/db/generate.go b/cmd/pw/db/generate.go
> new file mode 100644
> index 0000000..ca1f18e
> --- /dev/null
> +++ b/cmd/pw/db/generate.go
> @@ -0,0 +1,285 @@
> +// Patchwork - automated patch tracking system
> +// Copyright (C) The Patchwork Contributors (see CONTRIBUTORS)
> +//
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +
> +package db
> +
> +import (
> + "context"
> + "fmt"
> + "math/rand"
> + "time"
> +
> + "github.com/getpatchwork/patchwork/cmd/pw/pw"
> + "github.com/getpatchwork/patchwork/pkg/db"
> + "github.com/getpatchwork/patchwork/pkg/events"
> +)
> +
> +type GenerateCmd struct {
> + Name string `required:"" help:"Name of the project."`
> + Series int `required:"" help:"Number of series to generate. Each
> series will have up to 20 patches."`
> + Authors int `required:"" help:"Number of authors to generate."`
> +}
> +
> +const BATCH_SIZE = 1000
> +
> +func (c *GenerateCmd) Run(ctx context.Context) error {
> + database := pw.GetDB(ctx)
> +
> + bus := events.Start(ctx, database)
> + defer bus.Shutdown()
> + ctx = db.WithBus(ctx, bus)
> +
> + projectName := c.Name
> +
> + project := db.Project{
> + Name: projectName,
> + Linkname: projectName,
> + Listid: projectName,
> + Listemail: fmt.Sprintf("%s@%s.example.com",
> projectName, projectName),
> + WebURL: "",
> + ScmURL: "",
> + WebScmURL: "",
> + ListArchiveURL: "",
> + SubjectMatch: "",
> + CommitURLFormat: "",
> + ListArchiveURLFormat: "",
> + }
> + _, err := database.NewInsert().Model(&project).Exec(ctx)
> + if err != nil {
> + return err
> + }
> +
> + fmt.Printf("Created project %q (id=%d)\n", project.Name, project.ID)
> +
> + var states []db.State
> + err = database.NewSelect().Model(&states).Scan(ctx)
> + if err != nil {
> + return err
> + }
> +
> + var tags []db.Tag
> + err = database.NewSelect().Model(&tags).Scan(ctx)
> + if err != nil {
> + return err
> + }
> +
> + var authors []db.Person
> + for i := range c.Authors {
> + personName := fmt.Sprintf("person_%s_%d", projectName, i)
> +
> + person := db.Person{
> + Email: fmt.Sprintf("%s@%s.example.com", personName,
> projectName),
> + Name: &personName,
> + }
> + _, err := database.NewInsert().Model(&person).Exec(ctx)
> + if err != nil {
> + return err
> + }
> +
> + authors = append(authors, person)
> + }
> +
> + series_batches := c.Series / BATCH_SIZE
> + last_batch_size := c.Series % BATCH_SIZE
> + if last_batch_size != 0 {
> + series_batches += 1
> + }
> +
> + for sb := range series_batches {
> + var batch_size int
> + if sb == series_batches-1 && last_batch_size != 0 {
> + batch_size = last_batch_size
> + } else {
> + batch_size = BATCH_SIZE
> + }
> +
> + // Create covers
> +
> + var batch_covers []db.Cover
> + for b := range batch_size {
> + batch_covers = append(
> + batch_covers,
> + randCover(project,
> authors[rand.Int()%len(authors)], sb*BATCH_SIZE+b),
> + )
> + }
> +
> + _, err :=
> database.NewInsert().Model(&batch_covers).Returning("id").Exec(ctx)
> + if err != nil {
> + return err
> + }
> +
> + // Create series
> +
> + var batch_series []db.Series
> + for _, cover := range batch_covers {
> + batch_series = append(batch_series, randSeries(project,
> cover))
> + }
> + _, err =
> database.NewInsert().Model(&batch_series).Returning("id").Exec(ctx)
> + if err != nil {
> + return err
> + }
> +
> + // Create patches for each series
> +
> + var batch_patches []db.Patch
> + for s, series := range batch_series {
> + for p := range series.Total {
> + name := fmt.Sprintf("series %d patch %d",
> sb*BATCH_SIZE+s, p)
> + batch_patches = append(
> + batch_patches,
> + randPatch(project, series,
> states[rand.Int()%len(states)], name, p),
> + )
> + }
> + }
> +
> + err =
> database.NewInsert().Model(&batch_patches).Returning("id").Scan(ctx)
> + if err != nil {
> + return err
> + }
> +
> + // Create tags and checks
> +
> + var batch_tags []db.PatchTag
> + var batch_checks []db.Check
> + for _, patch := range batch_patches {
> + for t := range len(tags) {
> + if rand.Int()%(t+2) != t {
> + continue
> + }
> +
> + batch_tags = append(batch_tags, db.PatchTag{
> + PatchID: patch.ID,
> + TagID: tags[t].ID,
> + Count: rand.Int() % 10,
> + })
> + }
> +
> + for range rand.Int() % 10 {
> + batch_checks = append(batch_checks,
> randCheck(patch))
> + }
> + }
> +
> + err =
> database.NewInsert().Model(&batch_tags).Returning("").Scan(ctx)
> + if err != nil {
> + return err
> + }
> +
> + err =
> database.NewInsert().Model(&batch_checks).Returning("").Scan(ctx)
> + if err != nil {
> + return err
> + }
> +
> + fmt.Printf("\rgenerated series %d/%d",
> sb*BATCH_SIZE+batch_size, c.Series)
> + }
> + fmt.Printf("\n")
> +
> + return nil
> +}
> +
> +func randString(len uint32) string {
> + res := make([]byte, len)
> + for i := range len {
> + t := rand.Uint32() % 3
> + if t == 0 {
> + // 0-9
> + res[i] = byte(48 + (rand.Uint32() % 10))
> + } else if t == 1 {
> + // A-Z
> + res[i] = byte(65 + (rand.Uint32() % 25))
> + } else {
> + // a-z
> + res[i] = byte(97 + (rand.Uint32() % 25))
> + }
> + }
> +
> + return string(res)
> +}
cmd/pw/db/generate.go:185:3: ifElseChain: rewrite if-else to switch statement
(gocritic)
if t == 0 {
^
Perhaps you can refactor it that way:
const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
func randString(length uint32) string {
res := make([]byte, length)
for i := range res {
res[i] = charset[rand.Intn(len(charset))]
}
return string(res)
}
> +
> +func randTextBlock(lines uint32, lineLen uint32) string {
> + result := make([]byte, (lineLen+1)*lines)
> + for i := range lines {
> + line := randString(lineLen)
> + copy(result[i*lineLen+1:], []byte(line))
> + result[(i+1)*lineLen] = '\n'
> + }
> + return string(result)
> +}
> +
> +func randColor() int {
> + r := (rand.Int() % 200) + 40
> + g := (rand.Int() % 200) + 40
> + b := (rand.Int() % 200) + 40
> +
> + return (r << 16) | (g << 8) | b
> +}
cmd/pw/db/generate.go:210:6: func randColor is unused (unused)
func randColor() int {
^
> +
> +func randCover(project db.Project, submitter db.Person, num int) db.Cover {
> + name := fmt.Sprintf("cover %d", num)
> + content := randTextBlock((rand.Uint32()%500)+10, 72)
> + headers := randTextBlock((rand.Uint32()%40)+10, 72)
> + msgid := fmt.Sprintf("<%s@%s.example.com>", randString(15),
> project.Name)
> +
> + return db.Cover{
> + Msgid: msgid,
> + Date: time.Now(),
> + Headers: headers,
> + SubmitterID: submitter.ID,
> + Content: &content,
> + ProjectID: project.ID,
> + Name: name,
> + }
> +}
> +
> +func randSeries(project db.Project, cover db.Cover) db.Series {
> + patches := rand.Int() % 20
> +
> + return db.Series{
> + ProjectID: &project.ID,
> + CoverLetterID: &cover.ID,
> + Name: &cover.Name,
> + Date: time.Now(),
> + SubmitterID: cover.SubmitterID,
> + Version: 0,
> + Total: patches,
> +
> + ReceivedTotal: patches,
> + ReceivedAll: true,
> + }
> +}
> +
> +func randPatch(project db.Project, series db.Series, state db.State, name
> string, num int) db.Patch {
> + msgid := fmt.Sprintf("<%s@%s.example.com>", randString(15),
> project.Name)
> + content := randTextBlock((rand.Uint32()%500)+10, 72)
> + headers := randTextBlock((rand.Uint32()%40)+10, 72)
> + diff := ""
> + hash := randString(32)
> +
> + return db.Patch{
> + Msgid: msgid,
> + Date: time.Now(),
> + Headers: headers,
> + SubmitterID: series.SubmitterID,
> + Content: &content,
> + ProjectID: project.ID,
> + Name: name,
> + Diff: &diff,
> + StateID: &state.ID,
> + Archived: (rand.Int() % 2) == 0,
> + Hash: &hash,
> + SeriesID: &series.ID,
> + Number: &num,
> + }
> +}
> +
> +func randCheck(patch db.Patch) db.Check {
> + return db.Check{
> + PatchID: patch.ID,
> + Date: time.Now(),
> + State: db.CheckState(rand.Int() % 4),
> + TargetURL: "",
> + Context: fmt.Sprintf("check_%d", rand.Int()%5),
> + Description: "",
> + }
> +}
> diff --git a/cmd/pw/db/main.go b/cmd/pw/db/main.go
> index 6c12f88..8fed09c 100644
> --- a/cmd/pw/db/main.go
> +++ b/cmd/pw/db/main.go
> @@ -7,8 +7,9 @@ package db
>
> // CLI groups the database management subcommands.
> type CLI struct {
> - Sync SyncCmd `cmd:"" help:"Create or update the database schema."`
> - Status StatusCmd `cmd:"" help:"Show database schema migrations status."`
> - Export ExportCmd `cmd:"" help:"Export database contents as SQL."`
> - Import ImportCmd `cmd:"" name:"import" help:"Import SQL data from
> stdin."`
> + Sync SyncCmd `cmd:"" help:"Create or update the database
> schema."`
> + Status StatusCmd `cmd:"" help:"Show database schema migrations
> status."`
> + Export ExportCmd `cmd:"" help:"Export database contents as SQL."`
> + Import ImportCmd `cmd:"" name:"import" help:"Import SQL data from
> stdin."`
> + Generate GenerateCmd `cmd:"" name:"generate" help:"Generate random
> data."`
> }
--
Robin
# Misuse may cause suffocation.
_______________________________________________
Patchwork mailing list
[email protected]
https://lists.ozlabs.org/listinfo/patchwork