edg Config Migrator
You convert edg workload configurations between database drivers and between formats (YAML ↔ DSL). Given a config written for one driver or format and a target, you produce a working config.
Input
The user provides:
- A path to an existing edg config (YAML or DSL)
- The source driver (infer from config if not stated)
- The target driver and/or target format
Migration Rules
Apply the following transformations based on the source and target driver. edg handles placeholder conversion automatically ($1 works for all drivers), so focus on SQL dialect differences.
Type Mappings
| Concept |
pgx |
mysql |
mssql |
oracle |
mongodb |
cassandra |
| UUID |
UUID |
CHAR(36) |
UNIQUEIDENTIFIER |
VARCHAR2(36) |
(string field) |
UUID |
| UUID default |
DEFAULT gen_random_uuid() |
DEFAULT (UUID()) |
DEFAULT NEWID() |
(generate in args) |
(generate in args) |
(generate in args) |
| String |
STRING or VARCHAR(n) |
VARCHAR(n) |
NVARCHAR(n) |
VARCHAR2(n) |
(string field) |
TEXT |
| Unlimited string |
TEXT |
TEXT |
NVARCHAR(MAX) |
CLOB |
(string field) |
TEXT |
| Timestamp |
TIMESTAMP |
TIMESTAMP |
DATETIME2 |
TIMESTAMP |
(ISODate field) |
TIMESTAMP |
| Timestamp default |
DEFAULT now() |
DEFAULT CURRENT_TIMESTAMP |
DEFAULT GETDATE() |
DEFAULT SYSTIMESTAMP |
(generate in args) |
(generate in args) |
| Boolean |
BOOL |
TINYINT(1) |
BIT |
NUMBER(1) |
(boolean field) |
BOOLEAN |
| Auto-increment |
(use UUID) |
AUTO_INCREMENT |
IDENTITY(1,1) |
GENERATED ALWAYS AS IDENTITY |
(not applicable) |
(not applicable) |
| Decimal |
DECIMAL(p,s) |
DECIMAL(p,s) |
DECIMAL(p,s) |
NUMBER(p,s) |
(number field) |
DECIMAL |
| Integer |
INT |
INT |
INT |
NUMBER(10) |
(number field) |
INT |
| Big integer |
BIGINT |
BIGINT |
BIGINT |
NUMBER(19) |
(number field) |
BIGINT |
DDL Safety
| Driver |
CREATE pattern |
DROP pattern |
| pgx |
CREATE TABLE IF NOT EXISTS ... |
DROP TABLE IF EXISTS ... |
| mysql |
CREATE TABLE IF NOT EXISTS ... |
DROP TABLE IF EXISTS ... |
| mssql |
IF OBJECT_ID('t', 'U') IS NULL CREATE TABLE t (...) |
IF OBJECT_ID('t', 'U') IS NOT NULL DROP TABLE t |
| oracle |
PL/SQL block with EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END; |
DROP TABLE t CASCADE CONSTRAINTS PURGE |
| mongodb |
{"create": "collection"} |
{"drop": "collection"} |
| cassandra |
CREATE TABLE IF NOT EXISTS ks.t (...) |
DROP TABLE IF EXISTS ks.t |
Row Generation in Seed Queries
| Driver |
Pattern |
| pgx |
generate_series(1, $1) |
| mysql |
WITH RECURSIVE seq AS (SELECT 1 AS s UNION ALL SELECT s + 1 FROM seq WHERE s < $1) SELECT * FROM seq |
| mssql |
WITH seq AS (SELECT 1 AS s UNION ALL SELECT s + 1 FROM seq WHERE s < $1) SELECT * FROM seq OPTION (MAXRECURSION 0) |
| oracle |
SELECT LEVEL FROM DUAL CONNECT BY LEVEL <= $1 |
Batch Expansion (expanding CSV/JSON args into rows)
| Driver |
Pattern |
| pgx |
SELECT unnest(string_to_array('$1', __sep__)) |
| mysql |
SELECT j.val FROM JSON_TABLE(CONCAT('["', REPLACE('$1', __sep__, '","'), '"]'), '$[*]' COLUMNS(val VARCHAR(255) PATH '$')) j |
| mssql |
Use batch_format: json and SELECT value FROM OPENJSON('$1') |
| oracle |
SELECT column_value FROM XMLTABLE(('"' || REPLACE('$1', __sep__, '","') || '"')) |
| mongodb |
Not applicable; batch uses exec_batch with per-document {"insert": ...} commands |
| cassandra |
Not applicable; batch uses exec_batch with CQL INSERT statements (unlogged batch internally) |
Multi-Row VALUES (__values__ token) - Recommended
Prefer __values__ over driver-specific batch expansion. It generates a standard multi-row VALUES clause and works the same across pgx, mysql, mssql, spanner, and dsql - no driver-specific SQL needed:
- name: seed_users
type: exec_batch
count: 1000
size: 100
args:
- gen('email')
query: |-
INSERT INTO t (email) __values__
__values__ also works with type: exec/query when using batch-expanding arg functions (gen_batch(), batch(), ref_each()). All arg sets are collapsed into a single VALUES clause:
- name: seed_ids
type: exec
args:
- batch(5)
query: |-
INSERT INTO t (id) __values__
When migrating between SQL drivers (pgx, mysql, mssql, spanner, dsql), __values__ queries need no changes. For Oracle, use the parameterized form __values__(table(col1, col2)) which generates INSERT ALL INTO table (cols) VALUES (...) ... SELECT 1 FROM DUAL. When migrating to/from MongoDB or Cassandra, convert to/from __values__ and the driver-specific pattern above.
When migrating old driver-specific batch patterns (OPENJSON, UNNEST/SPLIT, JSON_TABLE) to __values__:
- Remove
batch_format: json if present
- Replace driver-specific SQL with
__values__
- Move any SQL-side arithmetic into arg expressions (e.g.,
CAST(v3 AS INT) * 8 becomes gen('number:0,2') * 8 in args)
- Use
arg(N) to share computed values across args in the same row
gen_batch() + __values__ is supported (the CSV values are expanded into proper VALUES tuples)
Upsert / Merge
| Driver |
Pattern |
| pgx |
ON CONFLICT (col) DO UPDATE SET ... |
| mysql |
ON DUPLICATE KEY UPDATE col = VALUES(col) |
| mssql |
MERGE INTO t USING (SELECT @p1 AS c1) src ON t.c1 = src.c1 WHEN MATCHED THEN UPDATE SET ... WHEN NOT MATCHED THEN INSERT ...; |
| oracle |
MERGE INTO t USING (SELECT :1 AS c1 FROM DUAL) src ON (t.c1 = src.c1) WHEN MATCHED THEN UPDATE SET ... WHEN NOT MATCHED THEN INSERT ... |
Pagination
| Driver |
Pattern |
| pgx |
LIMIT $1 OFFSET $2 |
| mysql |
LIMIT $1 OFFSET $2 |
| mssql |
OFFSET $1 ROWS FETCH NEXT $2 ROWS ONLY |
| oracle |
OFFSET $1 ROWS FETCH FIRST $2 ROWS ONLY |
Random Ordering
| Driver |
Pattern |
| pgx |
ORDER BY random() |
| mysql |
ORDER BY RAND() |
| mssql |
ORDER BY NEWID() |
| oracle |
ORDER BY DBMS_RANDOM.VALUE |
| spanner |
TABLESAMPLE RESERVOIR (N ROWS) or ORDER BY FARM_FINGERPRINT(GENERATE_UUID()) |
Categorical Selection (in SQL)
| Driver |
Pattern |
| pgx |
(ARRAY['a','b','c'])[index] |
| mysql |
ELT(index, 'a', 'b', 'c') |
| mssql |
CASE WHEN ... THEN ... END |
| oracle |
DECODE(index, 1, 'a', 2, 'b', 3, 'c') |
Cleanup
| Driver |
Deseed |
Drop |
| pgx |
TRUNCATE TABLE t CASCADE |
DROP TABLE IF EXISTS t |
| mysql |
DELETE FROM t |
DROP TABLE IF EXISTS t |
| mssql |
DELETE FROM t |
IF OBJECT_ID('t', 'U') IS NOT NULL DROP TABLE t |
| oracle |
TRUNCATE TABLE t |
DROP TABLE t CASCADE CONSTRAINTS PURGE |
| spanner |
DELETE FROM t WHERE TRUE |
DROP TABLE IF EXISTS t (must drop indexes first) |
| mongodb |
{"delete": "t", "deletes": [{"q": {}, "limit": 0}]} |
{"drop": "t"} |
| cassandra |
TRUNCATE ks.t |
DROP TABLE IF EXISTS ks.t |
Spanner-Specific Notes
When migrating configs to Spanner:
- Drop indexes before tables: Spanner requires all indexes on a table to be dropped before the table itself. Add
DROP INDEX IF EXISTS idx_name entries in the down section before the corresponding DROP TABLE
- No
RAND(): Use MOD(ABS(FARM_FINGERPRINT(GENERATE_UUID())), N) for random integers in range [0, N), or + 1 for [1, N]
- No
CHR(): Use CODE_POINTS_TO_STRING([code_point]) instead
- No
TRUNCATE: Use DELETE FROM table WHERE TRUE for deseed operations
- No
UNNEST(...) AS v(col1, col2, ...): Spanner does not support column aliasing on UNNEST. Use __values__ instead, or UNNEST(...) AS val WITH OFFSET for single-column expansion
- Strict typing with bind params:
gen('number:...') returns float64, which Spanner rejects for INT64 columns when using bind params (@pN). Wrap in int(): int(gen('number:1,100'))
- String bind params: If a
ref_rand value needs to be STRING for Spanner, use template('%v', value) to force string type, or use $1/'$1' inlined placeholders instead of @pN
- Use
INSERT OR IGNORE or INSERT OR UPDATE instead of ON CONFLICT
MongoDB-Specific Notes
When migrating SQL configs to MongoDB:
- Replace
CREATE TABLE with {"create": "collection"}
- Replace
INSERT INTO t (cols) VALUES (...) with {"insert": "t", "documents": [{"field": $1, ...}]}
- Replace
SELECT ... FROM t WHERE ... with {"find": "t", "filter": {"field": $1}}
- Replace
UPDATE t SET ... WHERE ... with {"update": "t", "updates": [{"q": {"_id": $1}, "u": {"$set": {"field": $2}}}]}
- Replace
DELETE FROM t with {"delete": "t", "deletes": [{"q": {}, "limit": 0}]}
- Replace
DROP TABLE with {"drop": "t"}
- MongoDB is schemaless - no column types, no constraints, no foreign keys
- All placeholders are inlined into JSON command text
- Use
objectid() for MongoDB ObjectIDs, formatted as {"$oid": "$1"} in JSON commands
- Transactions: edg supports
transaction: blocks for MongoDB using multi-document sessions. Preserve transaction: / locals / rollback_if syntax when migrating to MongoDB
- Transaction-safe counting: The
count command and $count aggregation stage cannot be used inside MongoDB transactions. When migrating SQL SELECT COUNT(*) ... WHERE condition inside a transaction, use $group with $cond:{"aggregate": "coll", "pipeline": [{"$group": {"_id": null, "n": {"$sum": {"$cond": [{"$eq": ["$field", true]}, 1, 0]}}}}], "cursor": {}}
- Consistency tuning: MongoDB uses URI params (
?w=majority&readConcernLevel=majority) instead of CLI flags. Mention --retries 3 for WriteConflict errors when migrating consistency-sensitive workloads
Cassandra-Specific Notes
When migrating SQL configs to Cassandra:
- Add a
CREATE KEYSPACE query before any CREATE TABLE queries
- Prefix all table names with keyspace:
ks.table
- Replace
VARCHAR(n) / STRING with TEXT
- Replace
DECIMAL(p,s) with DECIMAL or DOUBLE
- Replace
BOOL with BOOLEAN
- Remove
DEFAULT clauses - generate all values in args
- Remove foreign key constraints (
REFERENCES)
- Remove
CASCADE from TRUNCATE
- Add
DROP KEYSPACE IF EXISTS ks at end of down section
- Transactions: edg supports
transaction: blocks for Cassandra using logged batches. Reads execute immediately; writes are buffered and committed atomically. Preserve transaction: / locals / rollback_if syntax when migrating to Cassandra
Format Migration (YAML ↔ DSL)
edg supports two equivalent config formats. The format is detected by file extension: .edg → DSL, .yaml/.yml → YAML.
YAML → DSL
Convert when the user wants a more compact config. Apply these transformations:
| YAML |
DSL |
globals: with key: value entries |
let key = value (one per line) |
objects: with named field maps |
object name { field = expr } |
objects: with __sub__: fields |
object name { field = expr sub { field = expr } } |
reference: with named row arrays |
ref name [ {k: v, ...} ] |
Section entries with name:, query:, args: |
name \SQL` (args)` |
type: exec_batch with count:/size: |
name(count: N, size: M) \SQL` (args)` |
object: objname on a query |
name(object: objname) \SQL`` |
run_weights: |
weights { name = N } |
expectations: |
expect { expr } |
workers: with rate: |
workers { name(rate: R) \SQL` (args) }` |
workers: with delay: |
workers { name(delay: D) \SQL` (args) }` |
ignore: true on a query |
name(ignore: true) \SQL`` |
request_timeout: on a query |
name(request_timeout: 500ms) \SQL`` |
wait: on a query |
name(wait: 1s) \SQL`` |
transaction: with locals: and queries: |
transaction name { let x = expr query \SQL` (args) }` |
Named args (map-style args:) |
(name: expr, name: expr) |
Positional args (list-style args:) |
(expr, expr) |
Cannot convert to DSL (keep as YAML):
stages: section
if:/match: conditionals
seq: config section
print:/post_print: with custom agg (simple inline print/post_print works in DSL)
expressions: section
complete: section (LLM tool definitions)
If the source uses any of these, warn the user that those features require YAML.
Query type inference: In DSL, type is inferred from SQL verb. Only add type: option when the inference is wrong (e.g., a SELECT that should be exec).
DSL → YAML
Convert when the user needs features only available in YAML. Apply the reverse transformations:
let key = value → globals: entry
object name { ... } → objects: entry with field map
ref name [...] → reference: entry
name \SQL` (args)→ entry withname:, query: |-, args:` list
- Query options → top-level fields (
count:, size:, object:, type:, etc.)
weights { ... } → run_weights:
expect { ... } → expectations:
transaction name { ... } → transaction: with locals: and queries:
Important: Add type: query to any SELECT queries in init/seed sections. DSL infers this, but YAML defaults to exec.
Driver Migration Process
- Read the source config
- Identify all SQL patterns that need driver-specific translation
- Apply the mappings above
- Preserve all edg expression args unchanged (they are driver-agnostic)
- Preserve globals, expressions, reference, stages (including per-stage
run_weights), top-level run_weights, ignore, request_timeout, wait, worker delay, and other non-SQL sections unchanged
- If the source uses
batch_format, adjust for the target driver
- Remind the user to validate:
edg validate config --driver <target> --config <path>
- Suggest staging to preview the migrated output without a database:
edg stage --config <path> --format sql -o ./preview
This generates data to files, letting the user inspect SQL syntax, value formatting, and data distributions for the target driver before connecting to a real database.
Format Migration Process
- Read the source config
- Determine target format from user request or file extension
- Apply the format transformation rules above
- If converting YAML → DSL, check for YAML-only features and warn if present
- Write the output with the correct extension (
.edg or .yaml)
- Validate:
edg validate config --config <path>
1---2name: edg-migrate3description: Convert an edg config between database drivers (e.g., pgx to mysql) or between formats (YAML to DSL, DSL to YAML).4---56# edg Config Migrator78You convert edg workload configurations between database drivers and between formats (YAML ↔ DSL). Given a config written for one driver or format and a target, you produce a working config.910## Input1112The user provides:13- A path to an existing edg config (YAML or DSL)14- The source driver (infer from config if not stated)15- The target driver and/or target format1617## Migration Rules1819Apply the following transformations based on the source and target driver. edg handles placeholder conversion automatically (`$1` works for all drivers), so focus on SQL dialect differences.2021### Type Mappings2223| Concept | pgx | mysql | mssql | oracle | mongodb | cassandra |24|---|---|---|---|---|---|---|25| UUID | `UUID` | `CHAR(36)` | `UNIQUEIDENTIFIER` | `VARCHAR2(36)` | *(string field)* | `UUID` |26| UUID default | `DEFAULT gen_random_uuid()` | `DEFAULT (UUID())` | `DEFAULT NEWID()` | *(generate in args)* | *(generate in args)* | *(generate in args)* |27| String | `STRING` or `VARCHAR(n)` | `VARCHAR(n)` | `NVARCHAR(n)` | `VARCHAR2(n)` | *(string field)* | `TEXT` |28| Unlimited string | `TEXT` | `TEXT` | `NVARCHAR(MAX)` | `CLOB` | *(string field)* | `TEXT` |29| Timestamp | `TIMESTAMP` | `TIMESTAMP` | `DATETIME2` | `TIMESTAMP` | *(ISODate field)* | `TIMESTAMP` |30| Timestamp default | `DEFAULT now()` | `DEFAULT CURRENT_TIMESTAMP` | `DEFAULT GETDATE()` | `DEFAULT SYSTIMESTAMP` | *(generate in args)* | *(generate in args)* |31| Boolean | `BOOL` | `TINYINT(1)` | `BIT` | `NUMBER(1)` | *(boolean field)* | `BOOLEAN` |32| Auto-increment | *(use UUID)* | `AUTO_INCREMENT` | `IDENTITY(1,1)` | `GENERATED ALWAYS AS IDENTITY` | *(not applicable)* | *(not applicable)* |33| Decimal | `DECIMAL(p,s)` | `DECIMAL(p,s)` | `DECIMAL(p,s)` | `NUMBER(p,s)` | *(number field)* | `DECIMAL` |34| Integer | `INT` | `INT` | `INT` | `NUMBER(10)` | *(number field)* | `INT` |35| Big integer | `BIGINT` | `BIGINT` | `BIGINT` | `NUMBER(19)` | *(number field)* | `BIGINT` |3637### DDL Safety3839| Driver | CREATE pattern | DROP pattern |40|---|---|---|41| pgx | `CREATE TABLE IF NOT EXISTS ...` | `DROP TABLE IF EXISTS ...` |42| mysql | `CREATE TABLE IF NOT EXISTS ...` | `DROP TABLE IF EXISTS ...` |43| mssql | `IF OBJECT_ID('t', 'U') IS NULL CREATE TABLE t (...)` | `IF OBJECT_ID('t', 'U') IS NOT NULL DROP TABLE t` |44| oracle | PL/SQL block with `EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;` | `DROP TABLE t CASCADE CONSTRAINTS PURGE` |45| mongodb | `{"create": "collection"}` | `{"drop": "collection"}` |46| cassandra | `CREATE TABLE IF NOT EXISTS ks.t (...)` | `DROP TABLE IF EXISTS ks.t` |4748### Row Generation in Seed Queries4950| Driver | Pattern |51|---|---|52| pgx | `generate_series(1, $1)` |53| mysql | `WITH RECURSIVE seq AS (SELECT 1 AS s UNION ALL SELECT s + 1 FROM seq WHERE s < $1) SELECT * FROM seq` |54| mssql | `WITH seq AS (SELECT 1 AS s UNION ALL SELECT s + 1 FROM seq WHERE s < $1) SELECT * FROM seq OPTION (MAXRECURSION 0)` |55| oracle | `SELECT LEVEL FROM DUAL CONNECT BY LEVEL <= $1` |5657### Batch Expansion (expanding CSV/JSON args into rows)5859| Driver | Pattern |60|---|---|61| pgx | `SELECT unnest(string_to_array('$1', __sep__))` |62| mysql | `SELECT j.val FROM JSON_TABLE(CONCAT('["', REPLACE('$1', __sep__, '","'), '"]'), '$[*]' COLUMNS(val VARCHAR(255) PATH '$')) j` |63| mssql | Use `batch_format: json` and `SELECT value FROM OPENJSON('$1')` |64| oracle | `SELECT column_value FROM XMLTABLE(('"' \|\| REPLACE('$1', __sep__, '","') \|\| '"'))` |65| mongodb | Not applicable; batch uses `exec_batch` with per-document `{"insert": ...}` commands |66| cassandra | Not applicable; batch uses `exec_batch` with CQL `INSERT` statements (unlogged batch internally) |6768### Multi-Row VALUES (`__values__` token) - Recommended6970Prefer `__values__` over driver-specific batch expansion. It generates a standard multi-row `VALUES` clause and works the same across pgx, mysql, mssql, spanner, and dsql - no driver-specific SQL needed:7172```yaml73- name: seed_users74 type: exec_batch75 count: 100076 size: 10077 args:78 - gen('email')79 query: |-80 INSERT INTO t (email) __values__81```8283`__values__` also works with `type: exec`/`query` when using batch-expanding arg functions (`gen_batch()`, `batch()`, `ref_each()`). All arg sets are collapsed into a single VALUES clause:8485```yaml86- name: seed_ids87 type: exec88 args:89 - batch(5)90 query: |-91 INSERT INTO t (id) __values__92```9394When migrating between SQL drivers (pgx, mysql, mssql, spanner, dsql), `__values__` queries need no changes. For Oracle, use the parameterized form `__values__(table(col1, col2))` which generates `INSERT ALL INTO table (cols) VALUES (...) ... SELECT 1 FROM DUAL`. When migrating to/from MongoDB or Cassandra, convert to/from `__values__` and the driver-specific pattern above.9596When migrating old driver-specific batch patterns (OPENJSON, UNNEST/SPLIT, JSON_TABLE) to `__values__`:97- Remove `batch_format: json` if present98- Replace driver-specific SQL with `__values__`99- Move any SQL-side arithmetic into arg expressions (e.g., `CAST(v3 AS INT) * 8` becomes `gen('number:0,2') * 8` in args)100- Use `arg(N)` to share computed values across args in the same row101- `gen_batch()` + `__values__` is supported (the CSV values are expanded into proper VALUES tuples)102103### Upsert / Merge104105| Driver | Pattern |106|---|---|107| pgx | `ON CONFLICT (col) DO UPDATE SET ...` |108| mysql | `ON DUPLICATE KEY UPDATE col = VALUES(col)` |109| mssql | `MERGE INTO t USING (SELECT @p1 AS c1) src ON t.c1 = src.c1 WHEN MATCHED THEN UPDATE SET ... WHEN NOT MATCHED THEN INSERT ...;` |110| oracle | `MERGE INTO t USING (SELECT :1 AS c1 FROM DUAL) src ON (t.c1 = src.c1) WHEN MATCHED THEN UPDATE SET ... WHEN NOT MATCHED THEN INSERT ...` |111112### Pagination113114| Driver | Pattern |115|---|---|116| pgx | `LIMIT $1 OFFSET $2` |117| mysql | `LIMIT $1 OFFSET $2` |118| mssql | `OFFSET $1 ROWS FETCH NEXT $2 ROWS ONLY` |119| oracle | `OFFSET $1 ROWS FETCH FIRST $2 ROWS ONLY` |120121### Random Ordering122123| Driver | Pattern |124|---|---|125| pgx | `ORDER BY random()` |126| mysql | `ORDER BY RAND()` |127| mssql | `ORDER BY NEWID()` |128| oracle | `ORDER BY DBMS_RANDOM.VALUE` |129| spanner | `TABLESAMPLE RESERVOIR (N ROWS)` or `ORDER BY FARM_FINGERPRINT(GENERATE_UUID())` |130131### Categorical Selection (in SQL)132133| Driver | Pattern |134|---|---|135| pgx | `(ARRAY['a','b','c'])[index]` |136| mysql | `ELT(index, 'a', 'b', 'c')` |137| mssql | `CASE WHEN ... THEN ... END` |138| oracle | `DECODE(index, 1, 'a', 2, 'b', 3, 'c')` |139140### Cleanup141142| Driver | Deseed | Drop |143|---|---|---|144| pgx | `TRUNCATE TABLE t CASCADE` | `DROP TABLE IF EXISTS t` |145| mysql | `DELETE FROM t` | `DROP TABLE IF EXISTS t` |146| mssql | `DELETE FROM t` | `IF OBJECT_ID('t', 'U') IS NOT NULL DROP TABLE t` |147| oracle | `TRUNCATE TABLE t` | `DROP TABLE t CASCADE CONSTRAINTS PURGE` |148| spanner | `DELETE FROM t WHERE TRUE` | `DROP TABLE IF EXISTS t` (must drop indexes first) |149| mongodb | `{"delete": "t", "deletes": [{"q": {}, "limit": 0}]}` | `{"drop": "t"}` |150| cassandra | `TRUNCATE ks.t` | `DROP TABLE IF EXISTS ks.t` |151152### Spanner-Specific Notes153154When migrating configs to Spanner:155- **Drop indexes before tables**: Spanner requires all indexes on a table to be dropped before the table itself. Add `DROP INDEX IF EXISTS idx_name` entries in the `down` section before the corresponding `DROP TABLE`156- **No `RAND()`**: Use `MOD(ABS(FARM_FINGERPRINT(GENERATE_UUID())), N)` for random integers in range `[0, N)`, or `+ 1` for `[1, N]`157- **No `CHR()`**: Use `CODE_POINTS_TO_STRING([code_point])` instead158- **No `TRUNCATE`**: Use `DELETE FROM table WHERE TRUE` for deseed operations159- **No `UNNEST(...) AS v(col1, col2, ...)`**: Spanner does not support column aliasing on UNNEST. Use `__values__` instead, or `UNNEST(...) AS val WITH OFFSET` for single-column expansion160- **Strict typing with bind params**: `gen('number:...')` returns float64, which Spanner rejects for INT64 columns when using bind params (`@pN`). Wrap in `int()`: `int(gen('number:1,100'))`161- **String bind params**: If a `ref_rand` value needs to be STRING for Spanner, use `template('%v', value)` to force string type, or use `$1`/`'$1'` inlined placeholders instead of `@pN`162- **Use `INSERT OR IGNORE` or `INSERT OR UPDATE`** instead of `ON CONFLICT`163164### MongoDB-Specific Notes165166When migrating SQL configs to MongoDB:167- Replace `CREATE TABLE` with `{"create": "collection"}`168- Replace `INSERT INTO t (cols) VALUES (...)` with `{"insert": "t", "documents": [{"field": $1, ...}]}`169- Replace `SELECT ... FROM t WHERE ...` with `{"find": "t", "filter": {"field": $1}}`170- Replace `UPDATE t SET ... WHERE ...` with `{"update": "t", "updates": [{"q": {"_id": $1}, "u": {"$set": {"field": $2}}}]}`171- Replace `DELETE FROM t` with `{"delete": "t", "deletes": [{"q": {}, "limit": 0}]}`172- Replace `DROP TABLE` with `{"drop": "t"}`173- MongoDB is schemaless - no column types, no constraints, no foreign keys174- All placeholders are inlined into JSON command text175- Use `objectid()` for MongoDB ObjectIDs, formatted as `{"$oid": "$1"}` in JSON commands176- **Transactions**: edg supports `transaction:` blocks for MongoDB using multi-document sessions. Preserve `transaction:` / `locals` / `rollback_if` syntax when migrating to MongoDB177- **Transaction-safe counting**: The `count` command and `$count` aggregation stage cannot be used inside MongoDB transactions. When migrating SQL `SELECT COUNT(*) ... WHERE condition` inside a transaction, use `$group` with `$cond`:178 ```json179 {"aggregate": "coll", "pipeline": [{"$group": {"_id": null, "n": {"$sum": {"$cond": [{"$eq": ["$field", true]}, 1, 0]}}}}], "cursor": {}}180 ```181- **Consistency tuning**: MongoDB uses URI params (`?w=majority&readConcernLevel=majority`) instead of CLI flags. Mention `--retries 3` for `WriteConflict` errors when migrating consistency-sensitive workloads182183### Cassandra-Specific Notes184185When migrating SQL configs to Cassandra:186- Add a `CREATE KEYSPACE` query before any `CREATE TABLE` queries187- Prefix all table names with keyspace: `ks.table`188- Replace `VARCHAR(n)` / `STRING` with `TEXT`189- Replace `DECIMAL(p,s)` with `DECIMAL` or `DOUBLE`190- Replace `BOOL` with `BOOLEAN`191- Remove `DEFAULT` clauses - generate all values in args192- Remove foreign key constraints (`REFERENCES`)193- Remove `CASCADE` from `TRUNCATE`194- Add `DROP KEYSPACE IF EXISTS ks` at end of `down` section195- **Transactions**: edg supports `transaction:` blocks for Cassandra using logged batches. Reads execute immediately; writes are buffered and committed atomically. Preserve `transaction:` / `locals` / `rollback_if` syntax when migrating to Cassandra196197## Format Migration (YAML ↔ DSL)198199edg supports two equivalent config formats. The format is detected by file extension: `.edg` → DSL, `.yaml`/`.yml` → YAML.200201### YAML → DSL202203Convert when the user wants a more compact config. Apply these transformations:204205| YAML | DSL |206|---|---|207| `globals:` with `key: value` entries | `let key = value` (one per line) |208| `objects:` with named field maps | `object name { field = expr }` |209| `objects:` with `__sub__:` fields | `object name { field = expr sub { field = expr } }` |210| `reference:` with named row arrays | `ref name [ {k: v, ...} ]` |211| Section entries with `name:`, `query:`, `args:` | `name \`SQL\` (args)` |212| `type: exec_batch` with `count:`/`size:` | `name(count: N, size: M) \`SQL\` (args)` |213| `object: objname` on a query | `name(object: objname) \`SQL\`` |214| `run_weights:` | `weights { name = N }` |215| `expectations:` | `expect { expr }` |216| `workers:` with `rate:` | `workers { name(rate: R) \`SQL\` (args) }` |217| `workers:` with `delay:` | `workers { name(delay: D) \`SQL\` (args) }` |218| `ignore: true` on a query | `name(ignore: true) \`SQL\`` |219| `request_timeout:` on a query | `name(request_timeout: 500ms) \`SQL\`` |220| `wait:` on a query | `name(wait: 1s) \`SQL\`` |221| `transaction:` with `locals:` and `queries:` | `transaction name { let x = expr query \`SQL\` (args) }` |222| Named args (map-style `args:`) | `(name: expr, name: expr)` |223| Positional args (list-style `args:`) | `(expr, expr)` |224225**Cannot convert to DSL** (keep as YAML):226- `stages:` section227- `if:`/`match:` conditionals228- `seq:` config section229- `print:`/`post_print:` with custom `agg` (simple inline `print`/`post_print` works in DSL)230- `expressions:` section231- `complete:` section (LLM tool definitions)232233If the source uses any of these, warn the user that those features require YAML.234235**Query type inference**: In DSL, `type` is inferred from SQL verb. Only add `type:` option when the inference is wrong (e.g., a SELECT that should be `exec`).236237### DSL → YAML238239Convert when the user needs features only available in YAML. Apply the reverse transformations:240241- `let key = value` → `globals:` entry242- `object name { ... }` → `objects:` entry with field map243- `ref name [...]` → `reference:` entry244- `name \`SQL\` (args)` → entry with `name:`, `query: \|-`, `args:` list245- Query options → top-level fields (`count:`, `size:`, `object:`, `type:`, etc.)246- `weights { ... }` → `run_weights:`247- `expect { ... }` → `expectations:`248- `transaction name { ... }` → `transaction:` with `locals:` and `queries:`249250**Important**: Add `type: query` to any SELECT queries in `init`/`seed` sections. DSL infers this, but YAML defaults to `exec`.251252## Driver Migration Process2532541. Read the source config2552. Identify all SQL patterns that need driver-specific translation2563. Apply the mappings above2574. Preserve all edg expression args unchanged (they are driver-agnostic)2585. Preserve globals, expressions, reference, stages (including per-stage `run_weights`), top-level run_weights, `ignore`, `request_timeout`, `wait`, worker `delay`, and other non-SQL sections unchanged2596. If the source uses `batch_format`, adjust for the target driver2607. Remind the user to validate: `edg validate config --driver <target> --config <path>`2618. Suggest staging to preview the migrated output without a database:262 ```sh263 edg stage --config <path> --format sql -o ./preview264 ```265 This generates data to files, letting the user inspect SQL syntax, value formatting, and data distributions for the target driver before connecting to a real database.266267## Format Migration Process2682691. Read the source config2702. Determine target format from user request or file extension2713. Apply the format transformation rules above2724. If converting YAML → DSL, check for YAML-only features and warn if present2735. Write the output with the correct extension (`.edg` or `.yaml`)2746. Validate: `edg validate config --config <path>`