Table of Contents

Generate

foundry generate produces test data with Alchemy, YndigoBlue's test-data generation API. You describe the tables you want and the fields that populate their columns in a JSON payload, and Alchemy returns data that follows it, deterministically when the payload sets a seed.

foundry generate dataset [options]   # one or more related tables, as a .csvx package
foundry generate table [options]     # a single table, as a bare .csv file

A dataset pairs with foundry import schema, which builds the schema the package describes and loads every table into it. A single table pairs with foundry import table, which loads it into a table that already exists.

To check a payload and see what it would cost before generating anything, add --estimate.

Options

Both subcommands take the same options:

Option Short Description
--endpoint <URL> -e Base URL of the Alchemy API, e.g. https://alchemy.yndigoblue.com. Falls back to the ALCHEMY_ENDPOINT environment variable
--api-key <KEY> -k Alchemy API key, sent as X-Api-Key. Falls back to the ALCHEMY_API_KEY environment variable
--payload <FILE> -p Path to the JSON request payload (required)
--out <FILE> -o Path of the file to write (required, unless --estimate)
--estimate -E Check and price the payload without generating anything or being billed. See Estimating Cost
Tip

Set ALCHEMY_API_KEY in your environment rather than passing --api-key, so the key never ends up in a run script or your shell history.

Dataset

foundry generate dataset sends the payload to Alchemy's POST /v1/generate-dataset and writes the .csvx package it returns: a ZIP holding a manifest.json, a Velocity schema file describing every generated table (columns, primary keys, and the foreign keys between tables), and one CSV per table.

foundry generate dataset \
  --endpoint https://alchemy.yndigoblue.com \
  --payload dataset.json \
  --out dataset.csvx

foundry import schema --datasource connection.json --file dataset.csvx

Table

foundry generate table sends the payload to Alchemy's POST /v1/generate-table and writes the one table it describes as a bare CSV file, header row included, with no manifest or schema.

foundry generate table \
  --endpoint https://alchemy.yndigoblue.com \
  --payload customer.json \
  --out customer.csv

foundry import table --datasource connection.json --schema dbo --table customer --file customer.csv

Writing a Payload

Every Foundry project's schemas/ folder includes a JSON Schema for each payload shape, so an editor such as VS Code offers completion for every field type and its settings, and flags a mistake as you type rather than when the request is refused:

Schema Payload for
schemas/alchemy-dataset.schema.json foundry generate dataset
schemas/alchemy-table.schema.json foundry generate table

Point the payload at its schema with $schema:

{
  "$schema": "schemas/alchemy-dataset.schema.json",
  "seed": 42,
  "schemaName": "shop",
  "tables": [
    {
      "name": "customer",
      "rowCount": 100,
      "primaryKey": ["id"],
      "fields": [
        { "type": "Number", "mode": "Sequential", "start": 1,
          "map": { "value": { "column": "id", "dataType": "Integer" } } },
        { "type": "Person", "countries": [{ "country": "us" }],
          "map": {
            "firstName": { "column": "first_name", "dataType": "VarChar" },
            "lastName": { "column": "last_name", "dataType": "VarChar" } } }
      ]
    },
    {
      "name": "orders",
      "rowCount": 500,
      "primaryKey": ["order_id"],
      "fields": [
        { "type": "Number", "mode": "Sequential", "start": 1000,
          "map": { "value": { "column": "order_id", "dataType": "Long" } } },
        { "type": "Reference", "targetTable": "customer", "targetColumn": "id",
          "map": { "value": { "column": "customer_id", "dataType": "Integer" } } }
      ]
    }
  ]
}

A table payload is the same shape as one ordinary entry in tables, with seed, schemaVersion, and csvFormat alongside it at the top level. A link table ("type": "Link") copies its rows from another table in the same request, so it's only available in a dataset payload:

{
  "$schema": "schemas/alchemy-table.schema.json",
  "seed": 42,
  "name": "customer",
  "rowCount": 100,
  "fields": [
    { "type": "Number", "mode": "Sequential", "start": 1,
      "map": { "value": { "column": "id", "dataType": "Integer" } } }
  ]
}

Foundry sends the payload file to Alchemy exactly as it is. Alchemy accepts the top-level $schema and ignores it, but rejects any other property it doesn't recognize, so a misspelled setting fails the request rather than being silently dropped.

See Alchemy's own documentation for every field type and its settings.

Note

A .csvx package records the csvFormat its tables were written in, so foundry import schema reads it with no extra options. A bare CSV from foundry generate table carries no such record: if its payload sets csvFormat, pass the matching options to foundry import table (--delimiter, --encoding, and so on).

Output

On success Foundry reports how many bytes of data were generated and, when Alchemy reports one, what the call cost:

Dataset written to dataset.csvx (.csvx)
368 bytes generated, cost 0.0001 USD

When Alchemy refuses a request, nothing is written and Foundry prints the reason Alchemy gave, including where in the payload the problem is:

Error: Alchemy returned 400 Bad Request: Request body could not be read. Unknown property 'bogus'
at $.fields[0].bogus. ...

Estimating Cost

Add --estimate to either subcommand to send the payload to Alchemy's matching estimate endpoint instead. It runs the same checks a generation request runs before it generates anything, then reports what the request would cost and how much of each per-call limit it uses. No data is generated, nothing is written, and the call isn't billed, so --out isn't needed:

foundry generate dataset --endpoint https://alchemy.yndigoblue.com --payload dataset.json --estimate
dataset.json passed Alchemy's checks.
Estimated cost: 0.0042 USD
Balance: 12.50 USD (covers the estimate)
┌────────────────────────┬───────────┬─────────────┬────────┐
│ Limit                  │ Requested │     Maximum │   Used │
├────────────────────────┼───────────┼─────────────┼────────┤
│ Rows                   │       110 │     100,000 │  0.1 % │
│ Columns (widest table) │         8 │          25 │ 32.0 % │
│ Data bytes (estimated) │    12,380 │ 268,435,456 │  0.0 % │
│ Calculation steps      │         0 │  50,000,000 │  0.0 % │
└────────────────────────┴───────────┴─────────────┴────────┘

A payload that fails a check, or asks for more than a limit allows, is refused with the same error a generation request would get. --estimate also fails when your balance doesn't cover the estimate, so a run script stops before it reaches the generation step. Without an API key, which only a Development instance of Alchemy accepts, the payload is still checked but not priced.

Note

An estimate isn't a guaranteed price, and it doesn't reserve anything. A generation request is charged at the price in effect when it runs. Some checks need the generated data, such as a primaryKey column turning out to contain a repeated value, so a payload that passes an estimate can still be refused when it's generated.

In a Run Script

Both subcommands are available as generate-dataset and generate-table steps in a run script, with "estimate": true for an estimate, so a project can check its payload, generate its test data, and load it in one foundry run:

{
  "$schema": "schemas/run-script.schema.json",
  "datasource": "connection.json",
  "steps": [
    {
      "name": "Check the payload and the balance",
      "generate-dataset": {
        "endpoint": "https://alchemy.yndigoblue.com",
        "payload": "dataset.json",
        "estimate": true
      }
    },
    {
      "name": "Generate test data",
      "generate-dataset": {
        "endpoint": "https://alchemy.yndigoblue.com",
        "payload": "dataset.json",
        "out": "dataset.csvx"
      }
    },
    {
      "name": "Load it",
      "import-schema": { "file": "dataset.csvx", "drop": true }
    }
  ]
}

With ALCHEMY_API_KEY set in the environment, the script needs no key of its own.