Faker

Faker is the value engine. Once the analyzer and the factory have decided what a column should hold, Faker is almost always what produces the actual value, both inside the tool at codegen time (for a handful of scaffolding strings) and, far more importantly, inside the generated generator code that the tool emits and hands back to you.

Faker calls are the output, not just a dependency

src/test_data_workbench/adaptation/generator_factory.py does not call Faker itself to produce data. It writes Python source text that calls Faker, via Jinja2 templates (see Jinja2). The distinction matters for Design Principles: Faker is a runtime dependency of the generated project, not a hidden implementation detail that disappears once generation is done. A generated UsersGenerator imports faker and calls it directly, in code you can read.

Two layers pick the Faker method

The factory decides which Faker call to emit through two layers, checked in order, both visible in _get_column_generator and the entity-specific generator methods:

Column-name patterns first. A column named anything containing email, phone, name, address, city, country, or url gets the matching Faker call regardless of its declared SQL type:

if 'email' in col_name_lower:
    return 'self.fake.email()'
elif 'phone' in col_name_lower:
    return 'self.fake.phone_number()'
elif 'name' in col_name_lower:
    return 'self.fake.name()'
...

Declared type second. When no name pattern matches, faker_mapping maps the column’s SQL type substring to a Faker method: varchar/text to self.fake.text, integer/bigint to self.fake.random_int, decimal/numeric to self.fake.pydecimal, boolean to self.fake.boolean, date to self.fake.date, timestamp to self.fake.date_time, uuid to self.fake.uuid4. A generic varchar/text type is deferred to name-based temporal detection first (_temporal_kind), because SQLite in particular stores dates with TEXT affinity and the declared type alone can’t be trusted to mean “just text.”

Both layers are the same fallback-ladder philosophy from The Fallback Ladder applied at column granularity: specific signal (the name) beats generic signal (the type), and there is always a default (self.fake.text(max_nb_chars=50)) so a column with no useful name or type still gets something plausible.

Entity-specific Faker usage

The specialized generators layer on top of the generic mapping with domain-appropriate calls: self.fake.catch_phrase() for product names, self.fake.text(max_nb_chars=200) for descriptions, self.fake.text(max_nb_chars=300) for review comments. These are choices about which Faker provider reads right for a field, not just which one type-checks; a catch phrase looks more like a product name than generic lorem ipsum text does.

Uniqueness is handled by hand, not by fake.unique

Generated user/customer generators need unique emails across a batch. Rather than relying on Faker’s built-in .unique proxy, the emitted code tracks what it has produced itself:

def _unique_email(self) -> str:
    """Generate unique email address."""
    while True:
        email = self.fake.email()
        if email not in self._generated_emails:
            self._generated_emails.add(email)
            return email

This is deliberate: the generated code is meant to be self-contained and readable per Design Principles, and a loop-and-retry against a local set is easier for someone unfamiliar with Faker to read and modify than a stateful proxy object’s semantics. It is also exactly the uniqueness mechanism Test Data Principles describes as necessary for any uniquely-constrained column.

Determinism: Faker.seed(), not per-instance seeding

Both the tool’s own CLI seeding helper (src/test_data_workbench/cli/_seeding.py) and every generated generator’s __init__ seed the same way:

if seed is not None:
    Faker.seed(seed)
    random.seed(seed)

Faker.seed() is a classmethod: it seeds Faker’s shared random source process-wide, not just one Faker() instance. That is what makes the byte-for-byte reproducibility in Design Principles work across a whole generated dataset, every generator instantiated afterward draws from the same seeded state, not an independently-seeded one. _seeding.py seeds once, at CLI entry, before any analysis or generation begins, which is what closes the ordering leak: seeding after some Faker calls have already consumed random state would make the run depend on how much work happened before the seed was set.