edg#
edg is an expression-based data generator driven by a purpose-built language called edg-lang.
It was built to help you understand and prepare for how your database will behave when production traffic arrives and prevent against logic and performance regressions. Connect to one of the supported databases, create database objects, seed lots of realistic data quickly, and define workloads (transactional or otherwise). Concurrent workers and real-time throughput reporting will take of the rest.
Query arguments are written as expressions compiled at startup, giving you access to global constants, random data generation, reference lookups, and a bunch of random distributions.
Quick start#
Install with the install script.
curl -fsSL https://raw.githubusercontent.com/codingconcepts/edg-releases/main/install.sh | shInstall via Homebrew.
brew install codingconcepts/tap/edgRun all of the config steps for the built-in TPC-C workload (one of 16 built-in workloads).
edg workload tpcc all \
--driver <DRIVER> \
--url <CONNECTION_STRING> \
--workers 100 \
--duration 10mRun a custom workload.
edg all \
--driver <DRIVER> \
--url <CONNECTION_STRING> \
--config <FILE/HTTP PATH TO CONFIG>.edg \
--workers 10 \
--duration 10mWho is edg for?#
If you interact with databases in any way, edg will have a place in your arsenal. Whether you’re a database administrator, a developer, or a product manager, edg:
- Gives you a reproducible tool for testing workloads; even with complex relationships.
- Can compare the impact of migrations from one database to another.
- Can compare the impact of schema changes on database performance.
- Can reconcile ETL pipelines betweend databases for consistency.
- Can stress test your database, with high concurrency to isolate performance bottlenecks.
- Allows you to run complex workloads and assert the performance and end result (e.g. consistency).
Supported databases#
| Database | Driver | URL (example) |
|---|---|---|
| PostgreSQL | pgx | postgres://root@localhost:26257/db?sslmode=disable |
| MySQL | mysql | user:password@tcp(host:port)/db?parseTime=true |
| Cassandra | cassandra | cassandra://user:pass@host1[,host2,host3]:9042/keyspace |
| MongoDB | mongodb | mongodb://localhost:27017/db |
| SQLite | sqlite | file:edg.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)&_txlock=immediate |
| Redis / Valkey | redis | redis://user:pass@localhost:6379/0 |
| Aurora DSQL PRO | dsql | clusterid.dsql.us-east-1.on.aws |
| Spanner PRO | spanner | projects/PROJECT/instances/INSTANCE/databases/DATABASE |
| MSSQL PRO | mssql | sqlserver://user:password@host:port?database=db&encrypt=disable |
| Oracle PRO | oracle | oracle://system:password@localhost:1521/db |
Supported features#
| Feature | pgx | mysql | mongodb | cassandra | sqlite | redis | mssql | oracle | dsql | spanner |
|---|---|---|---|---|---|---|---|---|---|---|
| up / seed / run / deseed / down | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ |
| scaffold | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ |
| sync (run / down / verify) | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ⚠️ 1 | ☑️ | ☑️ | ☑️ | ☑️ |
| jobs (serve / submit / stream) | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ |
| Prometheus metrics | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ |
| Embeddings | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ |
| LLM completions | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ |
| init (schema introspection) | ☑️ | ☑️ | ⚠️ | 🚫 | ☑️ | ⚠️ | ☑️ | ☑️ | ☑️ | ☑️ |
| Batch operations | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ 2 | ☑️ | ☑️ | ☑️ | ☑️ |
| Expectations | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ |
| Plugins (WASM) | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ |
| Prepared statements | ☑️ | ☑️ | ⚠️ | 🚫 | ☑️ | ⚠️ | ☑️ | ☑️ | ☑️ | ☑️ |
| Stages | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ |
| Transactions | ☑️ | ☑️ | ☑️ 3 | ☑️ 4 | ☑️ | ☑️ 5 | ☑️ | ☑️ | ☑️ | ☑️ |
| Workers | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ 6 | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ |
☑️ Supported · 🚫 Not yet supported · ⚠️ Not applicable
SQLite concurrency#
SQLite is embedded and allows a single writer at a time, so a multi-worker write workload will fail with database is locked (5) (SQLITE_BUSY) unless the database is configured for it. edg passes the connection URL to the driver verbatim and doesn’t rewrite pragmas or clamp the pool size, so tuning is yours to do:
# WAL lets readers run alongside the single writer, busy_timeout makes
# writers wait for the lock instead of failing immediately, and txlock
# starts transactions as writers so they never have to upgrade.
edg run \
--driver sqlite \
--url "file:edg.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)&_txlock=immediate" \
--config workload.edgRules of thumb:
journal_mode(WAL)- readers no longer block on the writer. WAL is persisted in the database file, so it only needs setting once.busy_timeout(5000)- a writer that finds the lock held retries for up to 5s rather than erroring straight away. Raise it for write-heavy runs._txlock=immediate- needed for any transaction that reads before it writes. A default (deferred) transaction takes a read lock first, and if another writer commits in the meantime the upgrade fails immediately withdatabase is locked (517), whichbusy_timeoutdoes not retry. Starting asBEGIN IMMEDIATEtakes the write lock up front, so the wait happens wherebusy_timeoutapplies.--pool-size 1- serialises every statement through one connection, which removesSQLITE_BUSYentirely at the cost of concurrency. Useful when you want a deterministic run rather than a throughput number.:memory:gives each connection its own private database, so pair it with--pool-size 1or usefile::memory:?cache=shared.
sync runandsync downwork;sync verifydoesn’t, because it pages through a table in a stable order and RedisSCANguarantees no ordering. ↩︎Implemented using a non-transactional pipeline, so commands are sent in one round trip but applied independently. ↩︎
Implemented using multi-document transactions via client sessions. ↩︎
Implemented using logged batches, which guarantee atomicity but not isolation. ↩︎
Implemented using
MULTI/EXEC. Writes queue until commit; reads run immediately, so a transaction can’t read its own uncommitted writes. ↩︎SQLite allows one writer at a time. See SQLite concurrency. ↩︎