Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
098e41f
feat: safe reads, reliable jobs and live feedback
machado144 Sep 17, 2026
290da0a
feat: tuning recommendations and counts snapshots
machado144 Sep 17, 2026
1c5eaa6
feat: measure and compare relationship shapes
machado144 Sep 17, 2026
ebc3b0a
feat: analyze and compare relationships in the web UI
machado144 Sep 17, 2026
7b60c88
feat: warn about shared servers and refuse replica targets
machado144 Sep 17, 2026
6c2b3bc
feat: seed foreign keys with measured relationship shapes
machado144 Sep 17, 2026
28855df
docs: document safe reads, tuning, relationships and loadsim
machado144 Sep 17, 2026
e22f780
docs: benchmark relationship shapes and small-instance writers
machado144 Sep 17, 2026
03c8da4
test: describe loadsim evals without numbered references
machado144 Sep 17, 2026
41d53f9
fix: compare datetime keys as MySQL stores them
machado144 Sep 17, 2026
79cb27d
test: prove the full-disk eval filled the disk
machado144 Sep 17, 2026
c4969fc
test: make the estimate fallback eval independent of stats timing
machado144 Sep 17, 2026
db96400
fix: refuse only targets that refuse every write
machado144 Sep 17, 2026
1f83388
fix: keep job streams and borrowed pools intact
machado144 Sep 17, 2026
b808188
style: format the loadsim measure record
machado144 Sep 17, 2026
0910ef0
fix: share a pool's limit between concurrent runs
machado144 Sep 17, 2026
e778325
feat: name the relationships a schema cannot shape
machado144 Sep 17, 2026
6239a11
test: hold every measured shape to applied or reported
machado144 Sep 17, 2026
06fa548
fix: deal shapes in rounds when the parent pool is a sample
machado144 Sep 17, 2026
778b59d
test: pin shaped seeding above the key-pool limit
machado144 Sep 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/loadsim-measure.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Loadsim measure

# Throughput and memory per Cloud SQL-like profile and writer count, run by
# hand. Shared runners are noisy: the numbers are for comparison, never a gate.
on:
workflow_dispatch:
inputs:
profiles:
description: Profiles to measure (comma-separated)
default: cloudsql-micro,cloudsql-2vcpu

permissions:
contents: read

jobs:
measure:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
cache: true

- name: Measure
env:
SEEDSTORM_LOADSIM_MEASURE: ${{ github.workspace }}/loadsim-measure.json
SEEDSTORM_LOADSIM_PROFILES: ${{ inputs.profiles || 'cloudsql-micro,cloudsql-2vcpu' }}
run: cd integration && go test -v -tags "integration loadsim" -count=1 -run TestLoadsim_MeasureWriters ./... -timeout 3000s

- name: Summary
if: always()
run: |
if [ -f loadsim-measure.json ]; then
{ echo "### Loadsim measure"; echo '```json'; cat loadsim-measure.json; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
fi

- uses: actions/upload-artifact@v4
if: always()
with:
name: loadsim-measure
path: loadsim-measure.json
if-no-files-found: ignore
retention-days: 30
74 changes: 74 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,80 @@ jobs:
- name: Run integration tests
run: cd integration && go test -race -v -tags integration -count=1 ./... -timeout 1500s

loadsim:
runs-on: ubuntu-latest
timeout-minutes: 40
name: loadsim (cloud sql profiles)
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_USER: seedstorm
POSTGRES_PASSWORD: seedstorm
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10

mysql:
image: mysql:8.4
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: seedstorm
MYSQL_PASSWORD: seedstorm
MYSQL_DATABASE: testdb
ports:
- 3306:3306
options: >-
--health-cmd "mysqladmin ping -h localhost -u root -proot"
--health-interval 5s
--health-timeout 5s
--health-retries 10

steps:
- uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
cache: true

# Runner size is not assumed: tests skip profiles that do not fit and say so.
- name: Probe runner
run: |
{
echo "### Runner"
echo '```'
echo "cpus: $(nproc)"
free -m
df -h / /var/lib/docker 2>/dev/null || df -h /
echo "cgroup: $(stat -fc %T /sys/fs/cgroup)"
echo "docker data: $(findmnt -no SOURCE,FSTYPE -T /var/lib/docker || true)"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

- name: Pull database images
run: docker pull -q postgres:17-alpine && docker pull -q mysql:8.4 && docker pull -q alpine

- name: Run resource-limited evals
shell: bash # pipefail: tee must not hide a failure
run: cd integration && go test -v -tags "integration loadsim" -count=1 -run TestLoadsim ./... -timeout 1800s 2>&1 | tee loadsim.log

- name: Summarize skips
if: always()
run: |
{
echo "### Loadsim results"
echo '```'
grep -E -- "^(--- (PASS|FAIL|SKIP)|\s+[a-z_]+_test.go:[0-9]+: (only|skip))" integration/loadsim.log || true
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

e2e:
runs-on: ubuntu-latest
timeout-minutes: 25
Expand Down
61 changes: 50 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ seedstorm/
│ │ ├── compare.go # compare command
│ │ ├── mirror.go # mirror command
│ │ ├── snapshot.go # snapshot command (row counts to JSON/YAML)
│ │ ├── tune.go # tune command, --workers auto
│ │ ├── production.go # --production / --allow-production write guard
│ │ ├── relationships.go # --relationships scan flags, --shape-rows, achieved-shape logs
│ │ ├── progress.go # --workers flag, progress log lines, profile ignore
│ │ ├── profile.go # profile command + shared --profile flag/loading
│ │ ├── endpoints.go # --source-*/--target-* flags and connection opening
Expand All @@ -38,7 +41,12 @@ seedstorm/
│ │ ├── postgres.go # PostgreSQL schema introspection + constraint parsing
│ │ ├── mysql.go # MySQL schema introspection + constraint parsing
│ │ ├── truncate.go # Truncate helper (FK-safe order)
│ │ ├── counts.go # GetTableRowCounts helper (used by gaps)
│ │ ├── counts.go # CountTables / CountTablesWithin: per-table outcomes, unknown never 0
│ │ ├── read_scope.go # ReadOnce: read-only tx, statement/lock timeouts, MySQL KILL QUERY on cancel
│ │ ├── relations.go # DegreeHistogram (CASE buckets), LeadingIndexed, EstimateDegrees
│ │ ├── server_info.go # DetectServer (capacity, replica, server id), ConnectionUsage
│ │ ├── explain.go # Explain: disk full / connection lost errors in plain words
│ │ ├── partitions.go # Postgres partitioned tables (bounds, leaves)
│ │ ├── stats.go # Table sizes, estimated counts, column lists, DB identity
│ │ ├── copy.go # CopyRows: Postgres COPY for a chunk of rows
│ │ ├── sequences.go # SyncSequences: move Postgres sequences past inserted ids
Expand All @@ -56,21 +64,31 @@ seedstorm/
│ │ ├── stream.go # Stream: generation state kept across chunks (NewStream, Generate)
│ │ ├── keys.go # keySet: exact map, then scalable Bloom filter
│ │ ├── pools.go # PK pool reservoir sampling and capping
│ │ ├── shapes.go # Relationship shapes: degree dealer (Fenwick slots), DeriveShapedRows
│ │ ├── references.go # Pools for FKs to non-key columns ("table.column")
│ │ ├── partitions.go # Partition-key generators, CheckSeedable
│ │ └── *_test.go # Unit tests alongside production files
│ ├── graph/
│ │ ├── graph.go # Dependency graph (Build, TopologicalSort, RenderPlan)
│ │ ├── ignore.go # ApplyIgnore: drop ignored tables, refuse empty required parents
│ │ └── graph_test.go # Unit tests
│ ├── rules/ # Seed profile rules: model, templates, resolve/validate/compile
│ ├── profiles/ # Saved profile store (profiles.yaml) + Resolve(file|name)
│ ├── compare/ # Snapshots, Diff, PlanMirror, snapshot files (Encode/ParseSnapshot), renderers (never writes)
│ ├── compare/ # Snapshots, Diff, PlanMirror, snapshot files v1/v2 (Encode/ParseSnapshot), DiffShapes, renderers (never writes)
│ ├── relations/ # Relationship shapes: Scan (index gate, per-edge timeout, cancel keeps finished), Shape
│ ├── tuning/ # Recommend, ClampWriters, DetectHost (cgroup CPU/memory)
│ ├── safego/ # Run/Recover: a panic becomes an error with an id
│ ├── runerr/ # Located errors: side · phase · table
│ ├── faultinject/ # SEEDSTORM_FAULT points (build tag faultinject only)
│ ├── dataio/ # Streaming data documents: writers (yaml/json/sql/csv), ReadTables
│ ├── seeder/ # Seed (strict chunked seed/gaps), Fill (resilient mirror inserts), MirrorJob, Preview,
│ │ # writer.go (FK-gated concurrent writes), meter.go (rate/ETA),
│ │ # seed.go generateTables (parallel generation) + poolReleases
│ │ # seed.go generateTables (parallel generation) + poolReleases + clampToServer,
│ │ # relationships.go (Endpoint.Shapes, CompareShapes, MeasureShapes), servers.go (RelateServers)
│ ├── fsutil/ # WriteFileAtomic for on-disk stores
│ ├── tui/ # Bubble Tea flows (seed, gaps, generate, clone, mirror)
│ ├── web/ # serve: handlers_*.go per area, templates/, static/ (page.js + page.css per page)
│ ├── web/ # serve: handlers_*.go per area, templates/, static/ (page.js + page.css per page),
│ │ # jobs.go (list, reattach, eviction), production.go, preflight.go, shapes.go (session shape cache)
│ ├── ai/ai.go # Gemini enrichment (prompt building, response parsing)
│ ├── schema/schema.go # Schema YAML types and loader
│ ├── build/info.go # Version info injected at build time
Expand All @@ -80,20 +98,23 @@ seedstorm/
│ ├── binary_test.go # Builds the binary; scratch DB helpers per engine
│ ├── *_test.go # Scenario evals: reseed, mirror, seeder, keycloak, compare_web, parallel_seed,
│ │ # wide_schema (150 generated tables), access, snapshot, clone_objects,
│ │ # web_jobs (+helpers), reproducible, seed_scale (memory bounds)
│ │ # web_jobs (+helpers), reproducible, seed_scale (memory bounds), read_scope, production,
│ │ # reliability (faultinject), relationships, shaped_seed, partitions, tune, servers
│ ├── loadsim_*_test.go # Resource-limited evals in Cloud SQL-shaped containers (tags: integration loadsim)
│ ├── fixtures/ # Keycloak schema dumps (real-world 87-table stress schema)
│ ├── schema_postgres.sql # 28-table schema for Postgres integration tests
│ └── schema_mysql.sql # 28-table schema for MySQL integration tests
├── e2e/ # Playwright journeys for `serve` (pnpm; make test-e2e)
│ ├── playwright.config.ts # chromium, 1 worker; global setup builds the binary and starts serve
│ ├── support/ # global.setup/teardown, databases.fixture (ss_e2e_* DBs), wide-schema.fixture,
│ │ # db.helpers (SQL assertions), test.fixture, selectors.ts (every data-testid)
│ └── tests/<area>.spec.ts # workspace, seed, clone, compare, access, profiles, mobile
│ └── tests/<area>.spec.ts # workspace, seed, clone, compare, access, profiles, mobile, memory, production, tuning, snapshot, relationships
├── docs/benchmarks.md # Measured throughput, memory and layout numbers behind the defaults
├── README.md # User-facing documentation (keep in sync with code)
├── Makefile # Build, test, lint, dev-up/down targets
├── compose.yaml # Local MySQL + PostgreSQL via Docker Compose
└── .github/workflows/pr.yml # CI: title, review, structlint, unit tests, lint, integration (-race), e2e
├── docker-compose.yaml # Local MySQL + PostgreSQL via Docker Compose
└── .github/workflows/ # pr.yml: title, review, gauntlet (structlint, unit -race), lint, integration, loadsim, e2e;
# loadsim-measure.yml: manual throughput measurement
```

---
Expand Down Expand Up @@ -205,12 +226,12 @@ After standard row generation, `topUpEnumCoverage` guarantees every enum value (

| Job | What it checks |
|-----|---------------|
| `title` | Conventional Commits format |
| `pr-title` | Conventional Commits format |
| `review` | AI code review via reviewforge (Gemini) |
| `validate` | Directory/file structure via structlint |
| `test` | `go test ./...` + `make build` |
| `gauntlet` | structlint, dupehound, unit tests with `-race` |
| `lint` | `golangci-lint` |
| `integration` | Full suite + scenario evals on Postgres 13/15/17 × MySQL 5.7/8.0/8.4, with the race detector |
| `loadsim` | Resource-limited evals (Cloud SQL-shaped containers); outcomes only, never timings |
| `e2e` | Playwright journeys (`e2e/`) against a built binary on Postgres 15 + MySQL 8.0 |

The integration job in CI uses `-race -timeout 1500s` (the race detector roughly doubles the ~5 minute suite). Use the same locally.
Expand Down Expand Up @@ -279,6 +300,24 @@ The integration job in CI uses `-race -timeout 1500s` (the race detector roughly
- Don't rely on server defaults (PG < 15 `CREATE` on `public`, MySQL 5.7 CHECK/roles).
- Do give scratch databases unique `ss_<area>_*` names.

**Shared state a run borrows**
- Do restore a pool setting a run changes (`SetMaxOpenConns`): the web seeds on the pool its pages query.
- Don't judge a replica by MySQL `read_only`: a user with SUPER writes anyway, only `super_read_only` blocks.
- Do buffer every SSE event past the subscriber's channel: a fast job drops events, the handler refills from `job.Events()`.

**Database truths that break guards**
- Do round datetime keys to the second: MySQL rounds fractional seconds on insert, so adjacent values collide.
- Don't read Postgres row statistics right after inserting: they land asynchronously, a partial count is a valid estimate.

**Reads and failures**
- Do route new reads through `db.ReadOnce` (read-only, lock timeout, real cancel); never report an unknown count as 0.
- Do wrap goroutines in `safego.Run` and errors in `runerr.At`/`OnSide` so failures name side, phase and table.
- Don't trust the MySQL driver to stop a cancelled query: it keeps running without `KILL QUERY`.

**Relationship shapes**
- Do keep the unshaped path free of extra random draws (`--seed` hashes must not change).
- Do return dealt slots when a row is retried or dropped; never loop to fit a shape, adjust and warn.

**Web UI**
- Do check 320–1920 widths with the interaction active (search, dialogs, runs).
- Do test graph changes on `wideSchemaDDL(150)`, not only small schemas.
Expand Down
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ test-integration: dev-up
done
cd integration && go test -race -v -tags integration -count=1 ./... -timeout 1500s

.PHONY: test-loadsim
# Resource-limited evals: throwaway database containers shaped like managed
# Cloud SQL instances (integration/loadsim_*_test.go). Needs Docker; skips when
# the machine has too little free memory. Pass ARGS to filter, e.g. ARGS=-run=TestLoadsim_Read
test-loadsim:
cd integration && go test -race -v -tags "integration loadsim" -count=1 -run TestLoadsim $(ARGS) ./... -timeout 1800s

.PHONY: test-e2e
# Playwright journeys against a freshly built `seedstorm serve` and the compose
# databases (make dev-up). Pass ARGS to filter, e.g. ARGS=tests/compare.spec.ts
Expand Down
Loading
Loading