Technical

Schema-Preserving Generation: Why Column Types Aren't Enough

Synthpylon team
Schema-Preserving Generation: Why Column Types Aren't Enough

The simplest synthetic data generators treat a dataset as a collection of independent columns. They learn each column's distribution, sample from it, and concatenate the results. The output has the right column names, the right types, and values within the observed ranges. It also breaks almost every data pipeline that tries to load it.

Column types are the most visible layer of schema conformance, but they are not the only layer. Real relational data has constraints that are invisible to a column-level generator: foreign key relationships that define valid joins, uniqueness requirements on primary keys, check constraints that establish valid ranges or value combinations, referential integrity rules that require child rows to have corresponding parent rows, and domain logic that makes certain column combinations semantically impossible even if each column individually is valid.

A synthetic dataset that violates any of these is not just imperfect. It is actively harmful. Pipelines throw integrity errors on load. Join operations return unexpected nulls or inflated row counts. Models trained on the data learn patterns that cannot exist in production, and inference failures follow.

The Layers of Schema Conformance

When we talk about schema-preserving generation, we mean satisfying constraints at multiple levels simultaneously.

Type and format conformance is the baseline. Integers stay integers, date strings use valid calendar dates, categorical columns draw from the observed vocabulary. This is what most generators get right.

Range and domain constraints go further. A column recording transaction amount in USD should not produce negative values unless overdrafts are semantically valid. An age column should not produce values below zero or above a plausible human lifespan. An event timestamp for an order fulfillment record should be after the corresponding order creation timestamp, not before it. These constraints are often implicit in the domain: no one writes them into a schema file, but any record that violates them is nonsensical.

Uniqueness constraints require that primary key columns produce no duplicates across the generated dataset. This sounds trivial, but naive sampling from an empirical distribution will produce duplicates as the dataset grows, especially for low-cardinality identifier columns. A synthetic user ID column that repeats values breaks every join that depends on it.

Referential integrity is where column-level generators fail most visibly. If your dataset has an orders table with a customer_id column and a customers table with a customer_id primary key, then every value in the orders table's customer_id must appear in the customers table. A generator that treats the two tables independently will produce orders that reference customer IDs that do not exist, orders with no corresponding customer record, and customers with no orders. A JOIN between the synthetic tables will return a mess.

Conditional constraints are the subtlest layer. Consider a credit application dataset where a has_prior_default boolean column and a prior_default_amount numeric column are related: when has_prior_default is false, prior_default_amount should be null. A generator that does not model the conditional relationship between these columns will produce records where has_prior_default is false but prior_default_amount contains a non-null value, or vice versa. These incoherent records corrupt any model that relies on the relationship between the two columns.

How We Approach This

When Synthpylon ingests a schema, we extract the constraint graph alongside the column statistics. This includes explicit database constraints (PRIMARY KEY, FOREIGN KEY, UNIQUE, CHECK) where they are declared, as well as constraints we infer from statistical analysis of the real data: column pairs with deterministic or near-deterministic relationships, timestamp columns with ordering constraints implied by their names and correlations, and categorical columns that take null values only under specific conditions in other columns.

Generation happens in a constrained order. Parent tables in the foreign key graph are generated first. Child tables draw foreign key values from the pool of already-generated parent IDs, maintaining referential integrity by construction rather than by post-hoc filtering. Primary keys are generated using collision-free strategies: hash-based integer sequences for numeric keys, UUID generation for string keys, rather than sampling from the observed distribution.

For within-row conditional constraints, we use a conditional generation approach. The row is built left-to-right in a topological order derived from the dependency graph of columns. When a column's valid values depend on a previously generated column's value, the synthesis model conditions on that upstream value. This is computationally more expensive than independent column sampling, but it is the only way to produce rows that are internally coherent.

A Concrete Example: Transactional Order Data

Consider a simplified e-commerce schema: a customers table, an orders table with a foreign key to customers, and an order items table with a foreign key to orders and a product_id reference to a products table.

A column-level generator produces three tables in isolation. When you attempt to join them to reconstruct the order history view that your model trains on, the join fanout is unpredictable: some synthetic customers have hundreds of orders because the customer_id appeared many times in the random sample, others have none, and some order items reference order IDs that no longer exist after the independent sampling step.

A schema-preserving generator builds the hierarchy. It generates a customer population with realistic size distribution (some customers have one order, most have between two and fifteen, a small fraction have more). For each customer, it generates a plausible sequence of orders with timestamps in ascending order and realistic inter-order intervals. For each order, it generates order items where the product mix reflects the conditional distribution of products given order value range, and where quantities are positive integers, not zero or negative.

The resulting dataset can be joined, queried, and loaded into the same pipeline that processes real data. It does not require special handling or constraint relaxation downstream.

The Gap Between Declared and Undeclared Constraints

One honest limitation: our constraint extraction depends on what the schema declares and what we can infer statistically. Many real-world production databases have constraints that exist only in application code, not in the database schema. A check constraint that should prevent negative inventory counts but was never added to the DDL. A validation rule that prevents a specific combination of status codes that only exists in a microservice validator. We cannot infer these from schema introspection alone.

We are not saying this makes schema-preserving generation useless when schemas are incomplete. The constraints we do enforce dramatically reduce the frequency of incoherent records compared to column-level generation. But if your downstream pipeline has strict integrity requirements that depend on application-level constraints, you will want to document those constraints explicitly when you set up a synthesis job, rather than relying on inference.

The goal is a synthetic dataset that behaves like real data when you put it in a pipeline, not one that merely resembles real data in isolation. Those are different standards, and most existing generators only clear the second bar.