Skip to content

Keys — related tables with pk and fk

Relations are two column methods. A pk value is a pure function of its kwargs and the row index; an fk value is a function of its parent's pk definition, the generator seed and the row index. Nothing is stored between generators: the child rebuilds parent keys from the parent's spec.

from rand_engine import DataGenerator

customer_id = {"method": "pk", "kwargs": {"start": 1}}

customers = DataGenerator({
    "customer_id": customer_id,
    "city": {"method": "distincts", "kwargs": {"distincts": ["Rio", "Recife"]}},
}, seed=1).size(1_000).get_df()

orders = DataGenerator({
    "order_id": {"method": "pk", "kwargs": {"start": 1}},
    "customer_id": {"method": "fk", "kwargs": {"parent": customer_id, "parent_size": 1_000}},
}, seed=2).size(2_000).get_df()

assert customers["customer_id"].is_unique
assert orders["customer_id"].isin(customers["customer_id"]).all()

pk

kwarg Default Meaning
style "sequence" "sequence": start + i * step; "permuted": start + a bijection of i over [0, domain)
start 0 first value / offset
step 1 sequence only; never 0
domain — permuted only, required, in [1, 2**62]; row indices must stay below it
key 0 permuted only; another key gives another order
format — renders each value with str.format; exactly one integer field, e.g. "C-{:06d}"

i is the row index. Both styles are unique by construction, and neither depends on the seed.

spec = {
    "seq": {"method": "pk", "kwargs": {"start": 100, "step": 10}},
    "shuffled": {"method": "pk", "kwargs": {"style": "permuted", "domain": 1_000, "key": 7}},
    "code": {"method": "pk", "kwargs": {"start": 1, "format": "C-{:06d}"}},
}
df = DataGenerator(spec, seed=1).size(1_000).get_df()
assert df["seq"].tolist()[:3] == [100, 110, 120]
assert sorted(df["shuffled"]) == list(range(1_000))
assert df["code"].iloc[0] == "C-000001"
assert DataGenerator(spec, seed=99).size(1_000).get_df().equals(df)

fk

kwarg Meaning
parent the parent's pk column spec, exactly as the parent declares it
parent_size number of parent rows to pick from (row indices 0 … parent_size-1); at most a permuted parent's domain
skew 0 (default): uniform; > 0: Zipf with exponent skew, the hot parents scattered rather than the first rows

The child must use the parent's exact pk kwargs (format included) and a parent_size no larger than the rows the parent generated, or its keys point at rows that do not exist.

parent = {"method": "pk", "kwargs": {"style": "permuted", "domain": 10_000, "format": "P{:05d}"}}
products = DataGenerator({"product_id": parent}, seed=1).size(200).get_df()

sales = DataGenerator({
    "product_id": {"method": "fk", "kwargs": {"parent": parent, "parent_size": 200, "skew": 1.2}},
}, seed=5).size(4_000).get_df()

assert sales["product_id"].isin(products["product_id"]).all()
top_share = sales["product_id"].value_counts().iloc[0] / len(sales)
assert top_share > 1 / 200 * 10  # a few parents take most of the rows

Event-time eligibility

born_before is an optional FK mapping with exactly four fields: parent_start, parent_rate, child_start, and child_rate. Each is a finite built-in int or float (never bool), and the rates are positive. For child row i, every parent born at or before that child's event time is eligible, inclusively. If no parent is eligible, generation raises RandEngineError naming the FK column. The key calculation is stateless and supports exact large integer inputs.

from rand_engine.validators.exceptions import RandEngineError

event_time_born_before = {
    "parent_start": 0,
    "parent_rate": 1.0,
    "child_start": 2,
    "child_rate": 1.0,
}
event_time_parent = {"method": "pk", "kwargs": {"start": 1}}
event_time_parents = DataGenerator({"customer_id": event_time_parent}, seed=1).size(4).get_df()
event_time_orders = DataGenerator({
    "customer_id": {"method": "fk", "kwargs": {
        "parent": event_time_parent, "parent_size": 4, "born_before": event_time_born_before,
    }},
}, seed=2).size(4).get_df()
try:
    DataGenerator({"customer_id": {"method": "fk", "kwargs": {
        "parent": event_time_parent, "parent_size": 4,
        "born_before": {"parent_start": 10, "parent_rate": 1, "child_start": 0, "child_rate": 1},
    }}}, seed=2).size(1).get_df()
except RandEngineError as error:
    event_time_zero_eligible = "customer_id" in str(error)
else:
    event_time_zero_eligible = False
assert event_time_orders["customer_id"].isin(event_time_parents["customer_id"]).all()

Streams and multi-file writes continue the row index

Each microbatch of stream_dict, writeStream, and each part of write with numFiles starts at the row index where the previous one ended, so pk values never repeat across batches.

from itertools import islice

stream = DataGenerator({"event_id": {"method": "pk", "kwargs": {"start": 1}}}, seed=1).size(10)
records = list(islice(stream.stream_dict(min_throughput=500, max_throughput=1000), 25))
assert [r["event_id"] for r in records] == list(range(1, 26))

Spark

SparkGenerator accepts the same stateless pk and fk definitions through its key-only adapter. An explicit seed controls those keys only; ordinary Spark columns remain native and unseeded.