-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 232c238
Showing
26 changed files
with
896 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
name: lint | ||
on: | ||
push: | ||
branches: | ||
- main | ||
pull_request: | ||
|
||
permissions: | ||
contents: read | ||
|
||
jobs: | ||
golangci: | ||
name: lint | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: actions/setup-go@v5 | ||
with: | ||
go-version: stable | ||
- name: golangci-lint | ||
uses: golangci/golangci-lint-action@v6 | ||
with: | ||
version: v1.59.1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
name: test | ||
on: | ||
push: | ||
branches: | ||
- main | ||
pull_request: | ||
|
||
permissions: | ||
contents: read | ||
|
||
jobs: | ||
test: | ||
name: test | ||
runs-on: ubuntu-latest | ||
services: | ||
postgres: | ||
image: postgres:15.3 | ||
env: | ||
POSTGRES_DB: ripoff-test-db | ||
POSTGRES_USER: ripoff | ||
POSTGRES_PASSWORD: ripoff | ||
options: >- | ||
--health-cmd pg_isready | ||
--health-interval 10s | ||
--health-timeout 5s | ||
--health-retries 5 | ||
ports: | ||
- 5432:5432 | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: actions/setup-go@v5 | ||
with: | ||
go-version: stable | ||
- name: Run tests | ||
run: go test . | ||
env: | ||
RIPOFF_TEST_DATABASE_URL: postgres://ripoff:ripoff@localhost/ripoff-test-db |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
.vscode | ||
tmp |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
Copyright 2024 Samuel Mortenson | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
# ripoff - generate fake data from templated yaml files | ||
|
||
ripoff is a command line tool that generates fake data from yaml files (ripoffs) and inserts that data into PostgreSQL. | ||
|
||
Some features of ripoff are: | ||
|
||
- Model your fake data in one or more yaml files, god's favorite file format | ||
- Provide templated yaml files in cases where a row in one table requires many other rows, or you want loops | ||
- Ability to resolve dependencies (foreign keys) between generated rows, always running queries "in order" | ||
- Due to deterministic random generation, re-running ripoff will perform upserts on all generated rows | ||
|
||
# Installation | ||
|
||
1. Run `go install github.com/mortenson/ripoff/cmd/ripoff@latest` | ||
2. Set the `DATABASE_URL` env variable to your local PostgreSQL database | ||
3. Run `ripoff <directory to your yaml files>` | ||
|
||
When writing your initial ripoffs, you may want to use the `-s` ("soft") flag which does not commit the generated transaction. | ||
|
||
# File format | ||
|
||
ripoffs define rows to be inserted into your database. Any number of ripoffs can be included in a single directory. | ||
|
||
## Basic example | ||
|
||
```yaml | ||
# A map of rows to upsert, where keys are in the format <table_name>:<valueFunc>(<identifier or seed>), and values are maps of column names to values. | ||
rows: | ||
# A "users" table row identified with a UUID generated with the seed "fooBar" | ||
users:uuid(fooBar): | ||
# Using the map key here implicitly informs ripoff that "id" is the primary key of the table | ||
id: users:uuid(fooBar) | ||
email: [email protected] | ||
avatars:uuid(fooBarAvatar): | ||
id: avatars:uuid(fooBarAvatar) | ||
# ripoff will see this and insert the "users:uuid(fooBar)" row before this row | ||
user_id: users:uuid(fooBar) | ||
``` | ||
For more (sometimes wildly complex) examples, see `./testdata`. | ||
|
||
## More on valueFuncs and row keys | ||
|
||
valueFuncs allow you to generate random data that's seeded with a static string. This ensures that repeat runs of ripoff are deterministic, which enables upserts (consistent primary keys). If they appear anywhere in a | ||
|
||
ripoff provides: | ||
|
||
- `uuid(seedString)` - generates a UUIDv4 | ||
- `int(seedString)` - generates an integer (note: might be awkward on auto incrementing tables) | ||
- `literal(someId)` - returns "someId" exactly. useful if you want to hard code UUIDs/ints | ||
|
||
and also all functions from [gofakeit](https://github.com/brianvoe/gofakeit?tab=readme-ov-file#functions) that have no arguments and return a string (called in camelcase, ex: `email(seedString)`). | ||
|
||
## Using templates | ||
|
||
ripoff files can be used as templates to create multiple rows at once. | ||
|
||
Yaml files that start with `template_` will be treated as Go templates. Here's a template that creates a user and an avatar: | ||
|
||
```yaml | ||
rows: | ||
# "rowId" is the id/key of the row that rendered this template. All other variables are arbitrarily provided. | ||
{{ .rowId }}: | ||
id: {{ .rowId }} | ||
email: {{ .email }} | ||
avatar_id: avatars:uuid({{ .rowId }}) | ||
avatars:uuid({{ .rowId }}): | ||
id: avatars:uuid({{ .rowId }}) | ||
url: {{ .avatarUrl }} | ||
``` | ||
|
||
which you would use from a "normal" ripoff like: | ||
|
||
```yaml | ||
rows: | ||
# The row id/key will be passed to the template in the "rowId" variable. | ||
# This is useful if you'd like to reference "users:uuid(fooBar)" in a foreign key elsewhere. | ||
users:uuid(fooBar): | ||
# The template filename. | ||
template: template_user.yml | ||
# All other variables are arbitrary. | ||
email: [email protected] | ||
avatarUrl: image.png | ||
avatarGrayscale: false | ||
``` | ||
|
||
## Explicitly defining primary keys | ||
|
||
ripoff will try to determine the primary key for your row by matching the row ID with a single column (see "Basic example" above). However if you use composite keys, or your primary key is a foreign key to another table (see ./testdata/dependencies), this may not be possible. In these cases you can manually define primary keys using `~conflict: column_1, column_2, ...`. | ||
|
||
# Security | ||
|
||
This project explicitly allows SQL injection due to the way queries are constructed. Do not run `ripoff` on directories you do not trust. | ||
|
||
# Why this exists | ||
|
||
Fake data generators generally come in two flavors: | ||
|
||
1. Model your fake data in the same language/DSL/ORM that your application uses | ||
2. Fuzz your database schema by spewing completely randomized data at it | ||
|
||
I find generating fake data to be a completely separate use case from "normal" ORM usage, and truly randomized fake data is awkward to use locally. | ||
|
||
So ripoff is my approach to fake (but not excessively random) data generation. Because it's not aware of your application or schema, it's closer to writing templated SQL than learning some crazy high level DSL. There are awkward bits (everything is a string) but it's holding up OK for | ||
|
||
# FAQ | ||
|
||
## Why use Go templates and valueFuncs? | ||
|
||
It's kind of weird that `template_*` files use Go templates, but there's also valueFuncs like `uuid(someSeed)`. | ||
|
||
This is done for two reasons - first, Go templates are ugly and result in invalid yaml, so no reason to force you to write them unless you need to. Second, ripoff builds its dependency graph based on the row ids/keys, not the actual generated random value. So you can think of the query building pipeline as: | ||
|
||
1. Load all ripoff files | ||
2. For each row in each file, check if the row uses a template | ||
3. If it does, process the template and append the templated rows into the total rows | ||
4. If not, just append that row | ||
5. Now we have a "total ripoff" (har har) file which contains all rows. I think it's cool at this point that the templating is "done" | ||
6. For each row, check if any column references another row and build a directed acyclic graph, then sort that graph | ||
7. Run queries for each row, in order of least to greatest dependencies |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"flag" | ||
"fmt" | ||
"log/slog" | ||
"os" | ||
"path" | ||
|
||
"github.com/jackc/pgx/v5" | ||
|
||
"github.com/mortenson/ripoff" | ||
) | ||
|
||
func errAttr(err error) slog.Attr { | ||
return slog.Any("error", err) | ||
} | ||
|
||
func main() { | ||
verbosePtr := flag.Bool("v", false, "enable verbose output") | ||
softPtr := flag.Bool("s", false, "do not commit generated queries") | ||
flag.Parse() | ||
|
||
if *verbosePtr { | ||
slog.SetLogLoggerLevel(slog.LevelDebug) | ||
} | ||
|
||
dburl := os.Getenv("DATABASE_URL") | ||
if dburl == "" { | ||
slog.Error("DATABASE_URL env variable is required") | ||
os.Exit(1) | ||
} | ||
|
||
if len(flag.Args()) != 1 { | ||
slog.Error("Path to YAML files required") | ||
os.Exit(1) | ||
} | ||
rootDirectory := path.Clean(flag.Arg(0)) | ||
totalRipoff, err := ripoff.RipoffFromDirectory(rootDirectory) | ||
if err != nil { | ||
slog.Error("Could not load ripoff", errAttr(err)) | ||
os.Exit(1) | ||
} | ||
|
||
ctx := context.Background() | ||
conn, err := pgx.Connect(ctx, dburl) | ||
if err != nil { | ||
slog.Error("Could not connect to database", errAttr(err)) | ||
os.Exit(1) | ||
} | ||
defer conn.Close(ctx) | ||
|
||
tx, err := conn.Begin(ctx) | ||
if err != nil { | ||
slog.Error("Could not create transaction", errAttr(err)) | ||
os.Exit(1) | ||
} | ||
defer func() { | ||
err = tx.Rollback(ctx) | ||
if err != nil { | ||
slog.Error("Could not rollback transaction", errAttr(err)) | ||
os.Exit(1) | ||
} | ||
}() | ||
|
||
err = ripoff.RunRipoff(ctx, tx, totalRipoff) | ||
if err != nil { | ||
slog.Error("Could not run ripoff", errAttr(err)) | ||
os.Exit(1) | ||
} | ||
|
||
if *softPtr { | ||
slog.Info("Not committing transaction due to -s flag") | ||
} else { | ||
err = tx.Commit(ctx) | ||
if err != nil { | ||
slog.Error("Could not commit transaction", errAttr(err)) | ||
os.Exit(1) | ||
} | ||
} | ||
|
||
slog.Info(fmt.Sprintf("Ripoff complete, %d rows processed", len(totalRipoff.Rows))) | ||
} |
Oops, something went wrong.