Configuration#
Workloads are defined across one or more .edg config files. edg-lang offers syntax highlighting and autocompletion. See the edg-lang page for a full syntax guide.
# Variables available in all expressions.
let name = value
# User-defined expression functions.
expr name = expression
# Named auto-incrementing sequences.
seq name(start: 1, step: 1)
# Static datasets available to ref_* functions.
ref name [{key: val}, {key: val}]
# Reusable arg templates for queries.
object name {
field = expr
}
# Named query templates for reducing boilerplate.
template name(options)
# Background queries on a fixed schedule.
workers { name(rate: 1/10s) `SQL` }
# Schema creation queries.
up { query_name `SQL` }
# Data population queries.
seed { query_name `SQL` (args) }
# Staged workload execution.
stages {
name(workers: N, duration: D [, ramp_duration: R] [, qps: Q])
}
# Weighted transaction mix (simpler than stages).
weights { name = N }
# Per-worker initialisation queries.
init { query_name `SQL` }
# Workload queries.
run { query_name `SQL` (args) }
# Data cleanup queries.
deseed { query_name `SQL` }
# Schema teardown queries.
down { query_name `SQL` }
# Post-run assertions.
expect { metric_name < threshold }Globals#
The globals section defines top-level variables available in all expressions:
let warehouses = 1
let districts = 10
let customers = 30000
let items = 100000These can be referenced directly in arg expressions, including in arithmetic:
query_name `SELECT ... WHERE district_id = $1 AND warehouse_id = $2` (
customers / districts, # evaluates to 3000
warehouses * 10 # evaluates to 10
)Expression-valued globals#
Global values can be expressions, including references to other globals and environment variables. String values are compiled as expressions; if compilation fails (e.g. a plain string like "new york"), the value is kept as a literal.
Globals are evaluated in the order they appear in config, so later globals can reference earlier ones:
let warehouses = 1
let districts = warehouses * 10
let customers = districts * 3000Globals from environment variables#
Use env() to read a required environment variable, or env_nil() for an optional one. Combine env_nil() with coalesce() to provide a default:
# Required (fails at startup if DB_BATCH_SIZE is not set).
let batch_size = env('DB_BATCH_SIZE')
# Optional (falls back to 10000 if CUSTOMERS is not set).
let customers = int(coalesce(env_nil('CUSTOMERS'), 10000))
env()returns a string. Wrap withint()orfloat()if arithmetic is needed downstream.
Objects#
The objects section defines reusable named arg templates. Each key is an object name, and the value is a map of field names to expressions. Queries reference an object using the object field instead of duplicating args. Fields are bound to query parameters in declaration order ($1, $2, …):
object customer {
email = gen('email')
name = gen('name')
created_at = timestamp('2020-01-01T00:00:00Z', '2024-01-01T00:00:00Z')
}See the Objects page for full documentation, including field(), obj(), sub-objects, aggregate functions, and captured sub-items.
Templates#
The templates section defines named query templates that reduce boilerplate. A template is a partial query definition; any query can reference it with template: and inherit its fields. Fields set on the query override the template.
template batch_insert(count: batch_size, size: 1000)
seed {
seed_users(template: batch_insert, object: user)
`INSERT INTO users __columns__ __values__`
seed_orders(template: batch_insert, object: order)
`INSERT INTO orders __columns__ __values__`
}Template fields work as defaults - any field set on the query wins. Referencing an unknown template name is a validation error. Templates can be used in any section (seed, run, workers, transactions, etc.).
Reference#
The reference section defines static datasets that are loaded into the environment at startup, making them available to ref_rand, ref_same, ref_perm, and ref_diff functions without needing an init query or database connection. Each key is a dataset name, and the value is a list of row objects:
ref regions [
{name: "us", cities: ["a", "b", "c"]}
{name: "eu", cities: ["d", "e", "f"]}
{name: "ap", cities: ["g", "h", "i"]}
]Reference datasets work exactly like datasets populated by init queries - use ref_rand, ref_same, ref_perm, ref_diff, and the distribution-shaped variants in any arg expression. This is useful when your lookup data is small and known ahead of time, avoiding the need for a database round-trip. See Reference data functions for the full list.
Loading from CSV files#
Instead of inlining reference data in config, you can load it from CSV files at startup using the --csv-file and --csv-directory flags. Each CSV file becomes a reference dataset named after its filename (minus the extension). The header row defines the field names.
edg seed \
--config workload.edg \
--csv-file data/regions.csv \
--csv-file data/translations.csv \
--url $DATABASE_URLOr load all CSV files from a directory (non-recursive):
edg seed \
--config workload.edg \
--csv-directory data/ \
--url $DATABASE_URLA file named regions.csv with this content:
code,name,currency
us,United States,USD
gb,United Kingdom,GBP
de,Germany,EURis equivalent to this inline reference block:
ref regions [
{code: "us", name: "United States", currency: "USD"}
{code: "gb", name: "United Kingdom", currency: "GBP"}
{code: "de", name: "Germany", currency: "EUR"}
]Both flags are repeatable and work with all commands (up, seed, run, stage, repl, etc.). If a CSV filename collides with an existing reference: dataset name, edg returns an error.
See examples/csv/ for a complete working example.
Seq#
The seq section defines named auto-incrementing sequences that are shared across all workers. Unlike seq(start, step) which maintains a separate counter per worker, seq_global("name") returns globally unique values using atomic counters.
seq order_id(start: 1, step: 1)
seq line_item_id(start: 1000, step: 5)
seq sku_code(length: 3)| Field | Description |
|---|---|
name | Sequence identifier, referenced in seq_global("name") or seq_alpha_global("name") calls. Must be unique. |
start | Initial value for numeric sequences. |
step | Increment between consecutive numeric values. |
length | Character length for alpha sequences (e.g. 3 produces aaa, aab, …, zzz). Mutually exclusive with start/step. |
Each entry must use either start/step (numeric) or length (alpha), not both.
Use seq_global("name") in any arg expression:
seed {
seed_orders(count: 1000)
`INSERT INTO orders (id, name) VALUES ($1, $2)` (
seq_global("order_id"),
gen('firstname')
)
}Sequences work in all sections (seed, run, workers) and produce gap-free, globally unique values across concurrent workers. The same sequence can be used in both seed and run; the counter continues from where seeding left off.
To reference a random value that has already been generated by a sequence, use uniform.seq("name") or a distribution-shaped variant (seq_zipf, seq_norm, etc.). These compute valid values algebraically from the sequence’s start, step, and current counter - no values stored in memory. Useful for foreign key references where the referenced column was populated by seq_global. See Sequence functions for the full list.
See examples/global_sequences/ for a complete working example, and the Distributions page for histograms showing how each distribution shapes value selection.
Stages#
The stages section defines a sequence of workload phases, each with its own worker count and duration. Explicitly passing -w or -d on the command line overrides the stages section, falling back to single-stage mode with the provided values.
stages {
ramp(workers: 1, duration: 10s)
steady(workers: 10, duration: 30s, ramp_duration: 5s)
cooldown(workers: 2, duration: 10s)
}Each stage runs sequentially. When a stage completes (its duration expires), the next stage starts immediately with a new set of workers. The init section runs once before the first stage, and its results are shared across all stages.
| Field | Description |
|---|---|
name | Stage identifier, logged when the stage starts. |
workers | Number of concurrent workers for this stage. Defaults to 1 if omitted. |
duration | How long this stage runs (e.g. 10s, 5m, 1h). |
ramp_duration | Optional duration over which to linearly ramp up to full capacity. For QPS stages, all workers start immediately and QPS increases linearly. For worker-only stages, workers are spawned incrementally. Must be less than duration. |
qps | Optional queries-per-second rate limit shared across all workers. Accepts a static integer (e.g. 1000) or ramp(start, end, duration) to linearly increase QPS over time. |
run_weights | Optional per-stage weight overrides. Falls back to top-level run_weights when omitted. |
Per-stage run weights#
Each stage can optionally define its own run_weights to change the workload mix for that phase. When a stage omits run_weights, the top-level run_weights apply. When neither exists, all run items execute sequentially.
stages {
ramp(workers: 1, duration: 10s, weights: {check_balance: 90, credit_account: 5, make_transfer: 5})
steady(workers: 10, duration: 30s)
# steady falls back to top-level weights
}
weights {
check_balance = 50
credit_account = 5
make_transfer = 45
}When ramp_duration is set, the stage linearly ramps up to full capacity over that period instead of starting at full blast. For stages with qps, all workers start immediately and QPS increases linearly. For stages without qps, workers are spawned incrementally over the ramp period.
This is useful for simulating ramp-up patterns, sustained load tests, or multi-phase benchmarks without running separate commands.
See examples/stages/ for basic staged execution and examples/stages_run_weights/ for per-stage weight overrides.
Includes / Imports#
edg-lang supports splitting configs into reusable fragments:
includefor flat file merging, andimport/pubfor namespaced sharing across.edgfiles.
Includes (flat merging)#
Use include to merge another file’s contents directly (without namespacing or visibility restrictions):
include 'shared/globals.edg'
include 'shared/schema.edg'
seed {
populate_users(count: count, size: batch_size)
`INSERT INTO users (email) __values__` (gen('email'))
}Imports (namespaced sharing)#
In edg-lang, use import / pub to share declarations with namespacing. The filename (minus .edg) becomes the namespace:
import 'top.edg'
import 'objects/customer.edg' as cust
seed {
populate_customer(count: top.customers, size: top.batch_size, object: cust.customer)
`INSERT INTO customer (email) __values__ __columns__`
}Use as to override the default namespace:
import 'shared/long_filename.edg' as cfg
# now use cfg.customers, cfg.batch_size, etc.The pub keyword works with: let, object, ref, expr, seq, and template. Only pub-marked declarations are visible to importing files. Imported sections (up, seed, down, init, deseed, run, workers, weights) are also merged, with query names namespaced under the import alias.
Rules#
- Includes and imports must appear before all other declarations.
- Circular includes/imports are detected and produce an error.
- Paths are resolved relative to the file containing the directive; nested includes are supported.
See examples/imports/ for a complete working example.
CSV data (edg-lang only)#
Use csv to declare CSV file dependencies directly in your .edg config. Each CSV file becomes a reference dataset (like ref), named after its filename stem:
csv 'data/regions.csv'
csv 'data/translations.csv'
seed {
populate_customer(type: exec_batch, count: 10, size: 10)
`INSERT INTO customer (region, currency, greeting) __values__` (
ref_same('regions').code,
ref_same('regions').currency,
ref('translations').greeting
)
}You can also point to a directory to load all .csv files inside it:
csv 'data/'CSV files must have a header row and at least one data row. The header columns become the field names accessible via dot notation (e.g., .code, .currency).
The LSP recognises csv directives, so you get:
- Go-to-definition on dataset names (e.g., ctrl-click
'regions'inref_same('regions')opens the CSV file) - Path autocompletion when typing the CSV path
See examples/csv/ for a complete working example.