Scaffold PRO#

The edg scaffold command is an interactive wizard that generates a complete workload config. It walks you through driver selection, table definitions, and column types, then outputs to stdout.

edg scaffold > workload.edg

Walkthrough#

The wizard prompts for three things:

  1. Database driver - select from pgx, mysql, mssql, oracle, mongodb, cassandra, sqlite, redis, spanner, or dsql.
  2. Table names - comma-separated list of tables to generate (e.g. users, orders, products).
  3. Seed row counts - comma-separated count per table (e.g. 10000, 5000, 1000). Tables without a count default to 10000.
  4. Columns per table - for each table, enter name:type pairs comma-separated (e.g. id:uuid, email:text, age:int). Leave blank for defaults (id:uuid, name:text).

Supported column types#

TypeSQL Type (pgx)Expression
uuidUUIDuuid_v4()
textTEXTgen('name')
intINTuniform.int(1, 10000)
floatDOUBLE PRECISIONuniform.float(0.0, 100.0, 2)
boolBOOLEANbool()
timestampTIMESTAMPTZtimestamp('2024-01-01T00:00:00Z', '2025-01-01T00:00:00Z')

SQL types are driver-aware. For example, uuid becomes CHAR(36) on MySQL and UNIQUEIDENTIFIER on MSSQL.

Generated Config#

The output includes all standard sections:

SectionPurpose
globalsPer-table row count and batch size
objectsColumn expressions for each table (SQL drivers only)
upCREATE TABLE statements (or MongoDB create commands, or PING on Redis)
seedBatch insert queries using __columns__ and __values__, or positional args on drivers with no objects section
initSELECT queries to fetch seeded data for ref_* access
runPoint read queries using ref
deseedTRUNCATE statements (DELETE FROM on SQLite)
downDROP TABLE statements

Example#

edg scaffold > workload.edg
# Select: pgx
# Tables: users, orders
# Row counts: 5000, 10000
# users columns: id:uuid, email:text
# orders columns: id:uuid, total:float

Produces:

let users_rows = 5000
let orders_rows = 10000
let batch_size = 1000

object users {
  id = uuid_v4()
  email = gen('name')
}

object orders {
  id = uuid_v4()
  total = uniform.int(0.0, 100.0)
}

up {
  create_users `CREATE TABLE IF NOT EXISTS users (
    id UUID,
    email TEXT
  )`
  create_orders `CREATE TABLE IF NOT EXISTS orders (
    id UUID,
    total DOUBLE PRECISION
  )`
}

seed {
  seed_users(type: exec_batch, count: users_rows, size: batch_size, object: users)
    `INSERT INTO users __columns__ __values__`
  seed_orders(type: exec_batch, count: orders_rows, size: batch_size, object: orders)
    `INSERT INTO orders __columns__ __values__`
}

init {
  fetch_users `SELECT * FROM users LIMIT 1000`
  fetch_orders `SELECT * FROM orders LIMIT 1000`
}

run {
  read_users
    `SELECT * FROM users WHERE id = $1` (ref('fetch_users').id)
  read_orders
    `SELECT * FROM orders WHERE id = $1` (ref('fetch_orders').id)
}

deseed {
  clean_users `TRUNCATE TABLE users`
  clean_orders `TRUNCATE TABLE orders`
}

down {
  drop_users `DROP TABLE IF EXISTS users`
  drop_orders `DROP TABLE IF EXISTS orders`
}

MongoDB#

When the mongodb driver is selected, the scaffold generates JSON commands instead of SQL and omits the objects section (MongoDB doesn’t use __columns__/__values__). With no object to draw from, the seed query carries its own positional args:

up {
  create_orders `{"create": "orders"}`
}

seed {
  seed_orders(type: exec_batch, count: orders_rows, size: batch_size)
    `{"insert": "orders", "documents": [{"id": $1, "total": $2}]}` (
    uuid_v4(),
    uniform.float(0.0, 100.0, 2)
  )
}

SQLite#

The sqlite driver scaffolds ordinary SQL with SQLite’s type affinities (TEXT for uuid, REAL for float, INTEGER for int), and uses DELETE FROM in deseed because SQLite has no TRUNCATE:

up {
  create_orders(type: exec) `CREATE TABLE IF NOT EXISTS orders (
      id TEXT,
      total REAL
    )`
}

deseed {
  clean_orders(type: exec) `DELETE FROM orders`
}

Redis#

The redis driver scaffolds commands rather than SQL. Each row becomes a hash keyed on the first column, up is reduced to a connectivity check, and deseed/down sweep the keyspace with a SCAN + UNLINK script rather than KEYS, which would block the server:

up {
  create_orders(type: exec) `PING`
}

seed {
  seed_orders(type: exec_batch, count: orders_rows, size: batch_size) `HSET orders:$1 total $2` (
    uuid_v4(),
    uniform.float(0.0, 100.0, 2)
  )
}

run {
  read_orders `HGETALL ${ref('fetch_orders').value}`
}

The init query returns whole keys under a value column, which is why the read is ref('fetch_orders').value rather than a column name.

Always validate generated configs before running them: edg validate config --config workload.edg

Next Steps#

After generating a config:

  1. Review and customise - add foreign key relationships, transactions, run weights, or more complex expressions.
  2. Validate - run edg validate config --config workload.edg to check for errors.
  3. Test - use edg repl --config workload.edg to try expressions interactively.
  4. Run - execute edg up && edg seed && edg run against your database.