Graph Data#

The tree: config section generates hierarchical and graph-structured data declaratively. Instead of writing one seed query per level, you describe the shape with levels and edg expands it into the correct batch inserts with parent references wired up automatically.

Trees#

A tree generates strict parent-child hierarchies (e.g. org charts, category trees, nested comments). Each node at level N references exactly one parent at level N-1. Root nodes (level 0) get NULL parents.

seq emp_id(start: 1, step: 1)

tree org(levels: [1, 5, 15, 50, 200], id_column: id, parent_column: manager_id)
  `INSERT INTO employees (id, name, role, manager_id)
   __values__` (
  id: seq_global('emp_id'),
  name: gen('name'),
  role: const(''),
  manager_id: const(nil)
) {
  level 0 { role: const('CEO') }
  level 1 { role: const('VP') }
  level 2 { role: const('Director') }
  level 3 { role: const('Manager') }
  level 4 { role: set(['Engineer', 'IC'], []) }
}

This produces 271 rows across 5 levels:

LevelCountRole
01CEO
15VP
215Director
350Manager
4200Engineer or IC

Args that edg manages for you still need a placeholder expression, since every arg takes one. const(nil) is the conventional choice. The parent_column arg (manager_id above) is overwritten by edg at each level with the correct parent reference expression, and args overridden by level_args (like role above) are replaced at every level, so whatever you write for them is never evaluated.

How it works#

edg expands the tree config into separate seed queries at load time:

Expanded queryCountmanager_id expression
org_level_01const(nil)
org_level_15ref('org_level_0').id
org_level_215ref('org_level_1').id
org_level_350ref('org_level_2').id
org_level_4200ref('org_level_3').id

Database-generated ids#

The examples above generate ids client-side with seq_global, so edg already knows every id before it writes a row and ref() can chain levels from what it generated.

When the ids come from the database instead - an identity column, a DEFAULT, a trigger - edg has to read them back. Set type: query and add whatever clause your database uses to return written rows:

tree org(levels: [1, 5, 15], id_column: id, parent_column: manager_id, type: query)
  `INSERT INTO employees (name, manager_id)
   __values__
   RETURNING id` (
  name: gen('name'),
  manager_id: const(nil)
)

type: query tells edg to keep the rows each level returns, so ref('org_level_0').id picks from real database ids rather than ones edg made up. Without it, levels discard what comes back and a tree built on database-generated ids has nothing to chain on.

The returning clause is yours to write - edg never reads your SQL to work out whether you included one, because every driver spells it differently (RETURNING in PostgreSQL and CockroachDB, OUTPUT INSERTED in SQL Server, and MySQL has no equivalent). What edg does check is the rows that come back: if a level returns nothing, or returns rows without the id_column in them, it fails at that level and says so, rather than letting the next level fail on an empty ref().

Levels with type: query run on a single worker, since reading rows back rules out the parallel batch path. Trees with client-generated ids are unaffected.

DAGs#

Set dag: true to generate directed acyclic graphs. In DAG mode, a node can reference a parent from any prior level, not just the one directly above. This models dependency graphs, prerequisite chains, and similar structures.

tree deps(levels: [3, 8, 20], id_column: id, parent_column: dependency_id, dag: true)
  `INSERT INTO dependencies (id, name, dependency_id)
   __values__` (
  id: seq_global('dep_id'),
  name: template('pkg-%d', iter()),
  dependency_id: const(nil)
)

In DAG mode, the parent expression picks from all prior levels uniformly:

Expanded querydependency_id expression
deps_level_0const(nil)
deps_level_1ref(set(['deps_level_0'], [])).id
deps_level_2ref(set(['deps_level_0', 'deps_level_1'], [])).id

Per-level argument overrides#

Use level_args to override specific args at each level. Any expression is valid. Args not overridden at a given level use the default from args.

tree org(levels: [1, 5, 100], id_column: id, parent_column: parent_id)
  `INSERT INTO org (id, title, parent_id)
   __values__` (
  id: seq_global('id'),
  title: gen('jobtitle'),
  parent_id: const(nil)
) {
  level 0 { title: const('CEO') }
}

Level 0 gets const('CEO'), levels 1 and 2 fall back to gen('jobtitle').

Templates and batch options#

Use template to inherit shared settings across multiple tree configs. Combine with size and wait to control how expanded queries execute.

tree categories(levels: [1, 20, 200], id_column: id, parent_column: parent_id, template: tree_batch)
  `INSERT INTO categories (id, name, parent_id)
   __values__` (
  id: seq_global('cat_id'),
  name: gen('noun'),
  parent_id: const(nil)
)

tree regions(levels: [1, 10, 50], id_column: id, parent_column: parent_id, template: tree_batch)
  `INSERT INTO regions (id, name, parent_id)
   __values__` (
  id: seq_global('region_id'),
  name: gen('country'),
  parent_id: const(nil)
)

template tree_batch(size: 500, wait: 100ms)

Both trees inherit size: 500 and wait: 100ms from tree_batch. Each expanded level query inserts in batches of 500 rows, pausing 100ms between batches.

Tree levels always run batched, which rules out prepared statements, since the SQL changes with every batch. A tree rejects prepared set on itself, and ignores it on a template so the same template can still be shared with ordinary seed queries that do use it.

Fields set directly on the tree override the template:

tree huge_tree(levels: [1, 100, 10000], id_column: id, parent_column: parent_id, template: tree_batch, size: 5000)
  `INSERT INTO nodes (id, parent_id)
   __values__` (
  id: seq_global('id'),
  parent_id: const(nil)
)

Here size: 5000 overrides the template’s size: 500, but wait is still inherited.

Config reference#

FieldRequiredDescription
nameyesPrefix for expanded query names ({name}_level_{n})
levelsyesList of row counts per level, each an integer or the name of a let global
id_columnyesColumn name used as the node identifier
parent_columnyesColumn name for the parent reference (must appear in args)
dagnotrue for DAG mode (default false for strict tree)
argsyesNamed argument expressions (same as seed queries)
level_argsnoPer-level argument overrides, keyed by level index
queryyesINSERT query with __values__ placeholder
templatenoInherit defaults from a named template
typenoquery to chain on ids the database returns, exec (default) for client-generated ids
sizenoBatch size for expanded queries
batch_formatnoBatch format (json, etc.)
waitnoDelay between expanded queries

Expression-valued levels#

Level counts can reference globals, so you can parameterise the shape:

let total_subcats = 50
let total_items = 500

tree categories(levels: [1, total_subcats, total_items], id_column: id, parent_column: parent_id)
  `INSERT INTO categories (id, parent_id)
   __values__` (
  id: seq_global('cat_id'),
  parent_id: const(nil)
)