seed:- name:populate_userstype:exec_batchcount:1000size:100args:- gen('email')query:|- INSERT INTO users (email)
__values__
For Oracle, use the parameterized form __values__(table(cols)) to generate INSERT ALL ... SELECT 1 FROM DUAL.
The following expressions are used with the driver-specific batch expansion patterns (unnest, JSON_TABLE, etc.):
Expression
Description
batch(customers / batch_size)
Drives batched execution: the parent query runs N times with $1 = 0..N-1
gen_batch(customers, batch_size, 'email')
Generates unique emails via gofakeit, split into batches
string_to_array('$1', __sep__)
Splits a batch-expanded placeholder back into rows using the driver-aware separator
When using driver-specific batch expansion, always use __sep__ instead of a literal comma delimiter. Generated values (names, addresses, etc.) can contain commas, which would silently split a single value into multiple rows and corrupt your data. The __values__ approach avoids this issue entirely.
Independent full name in locale order (東 = 佐藤太郎, 西 = Hans Müller). First and last are picked independently of first_name/last_name args.
arg('first_name') + " " + arg('last_name')
Composed full name from previously generated args. Use this when first_name and last_name are separate args and the full name must match. For eastern-order locales, use arg('last_name') + arg('first_name') (no space).
gen_locale('city', 'fr_FR')
French city name (e.g. Paris, Lyon)
gen_locale('street', 'es_ES')
Spanish street name (e.g. Gran Vía)
gen_locale('phone', 'ko_KR')
Korean phone number (e.g. 010-1234-5678)
gen_locale('zip', 'ja_JP')
Japanese postal code (e.g. 123-4567)
gen_locale('address', 'de_DE')
Full German address with street number, city, and zip
mask('john@example.com')
Deterministic 16-char hex token (e.g. a3f8c1d9e2b74f06). Same input -> same output within a session
mask(arg('email'), 8)
8-char hex token of a previously generated email (e.g. a3f8c1d9)
When seeding a child table (e.g. account) from every row in a parent table (e.g. customer), use ref_each or ref_cursor instead of manual LIMIT/OFFSET pagination.
ref_each - load all parent rows, iterate in batches#
ref_each('SQL') runs the SQL query, loads all returned rows into memory, then executes the parent query once per row. Combined with __values__, rows are collapsed into multi-row INSERTs controlled by size.
Best for small-to-medium parent tables (under ~1M rows) where loading all IDs into memory is acceptable.
seed {
populate_customer(type: exec_batch, count: customers, size: batch_size, workers: 4)
`INSERT INTO customer (email)
__values__` (gen('email'))
populate_account(size: batch_size, workers: 4)
`INSERT INTO account (balance, customer_id)
__values__` (
initial_balance,
ref_each('SELECT id FROM customer').id
)
}
seed:- name:populate_customertype:exec_batchcount:customerssize:batch_sizeworkers:4args:- gen('email')query:|- INSERT INTO customer (email)
__values__- name:populate_accountsize:batch_sizeworkers:4args:- initial_balance- ref_each('SELECT id FROM customer').idquery:|- INSERT INTO account (balance, customer_id)
__values__
ref_cursor('SQL', page_size, 'cursor_column') pages through the parent table using keyset pagination (WHERE cursor_col > $last_value ORDER BY cursor_col LIMIT page_size). Only one page of rows is in memory at a time.
Best for large parent tables (1M+ rows) where loading all IDs into memory is impractical.
seed {
populate_customer(type: exec_batch, count: customers, size: batch_size, workers: 4)
`INSERT INTO customer (email)
__values__` (gen('email'))
populate_account(size: batch_size, workers: 4)
`INSERT INTO account (balance, customer_id)
__values__` (
initial_balance,
ref_cursor('SELECT id FROM customer ORDER BY id', batch_size, 'id')
)
}
seed:- name:populate_customertype:exec_batchcount:customerssize:batch_sizeworkers:4args:- gen('email')query:|- INSERT INTO customer (email)
__values__- name:populate_accountsize:batch_sizeworkers:4args:- initial_balance- ref_cursor('SELECT id FROM customer ORDER BY id', batch_size, 'id')query:|- INSERT INTO account (balance, customer_id)
__values__
In batch queries, embed() calls are deferred - placeholders are inserted during arg evaluation, then all pending texts are resolved together at the end of each batch:
populate_product(count: 100, size: 50)
`INSERT INTO product (name, description, embedding)
SELECT n, d, e::VECTOR
FROM unnest(ARRAY[$1], ARRAY[$2], ARRAY[$3]) AS t(n, d, e)` (
ref_each(product_catalog).name,
ref_each(product_catalog).description,
embed(ref_each(product_catalog).name, ref_each(product_catalog).description)
)
- name:populate_producttype:exec_batchcount:100size:50args:- ref_each(product_catalog).name- ref_each(product_catalog).description- embed(ref_each(product_catalog).name, ref_each(product_catalog).description)query:|- INSERT INTO product (name, description, embedding)
SELECT n, d, e::VECTOR
FROM unnest(ARRAY[$1], ARRAY[$2], ARRAY[$3]) AS t(n, d, e)
With count: 100 and size: 50, there are 2 batches of 50. Each batch collects 50 texts, then resolves them in a single API call - 2 API calls instead of 100.
Use --embed-max-batch to cap texts per API call. For example, --embed-max-batch 30 on a 50-row batch produces 2 API calls (30+20) per batch, or 4 total (30+20+30+20).