Generators
Bloviate picks a generator for every column from its JDBC type by default. When you want to apply a custom generator broadly — across many columns or whole types — or generate realistic, referentially-consistent data, reach for the registry, the Datafaker module, and the key/cardinality generators described here.
Custom generator registry
Section titled “Custom generator registry”Per-column overrides are precise but must be wired one column at a time. When you want a custom
generator applied broadly — every column named email, every uuid vendor type, or every
INTEGER — register it once on a GeneratorRegistry and attach it to the DatabaseConfiguration.
No subclassing of DatabaseSupport required.
import io.bloviate.db.*;import io.bloviate.ext.GeneratorRegistry;import io.bloviate.ext.PostgresSupport;import io.bloviate.gen.*;import java.sql.JDBCType;import java.util.Set;
GeneratorRegistry registry = new GeneratorRegistry.Builder() // by column-name pattern (case-insensitive regex, full match) .registerColumnNamePattern("email", (column, random) -> new SimpleStringGenerator.Builder(random).size(column.maxSize()).build()) // by vendor type name (case-insensitive) .registerTypeName("uuid", (column, random) -> new UUIDGenerator.Builder(random).build()) // by JDBCType (overrides the built-in default for that type) .registerJdbcType(JDBCType.INTEGER, (column, random) -> new IntegerGenerator.Builder(random).start(1).end(1000).build()) // auto-discover GeneratorPlugins contributed by other jars on the classpath .discover() .build();
DatabaseConfiguration config = new DatabaseConfiguration( 128, 100, new PostgresSupport(), null, registry);
new DatabaseFiller.Builder(connection, config) .build() .fill();Resolution precedence (first match wins, highest to lowest):
- per-column
ColumnConfiguration(from aTableConfiguration) - registry column-name pattern
- registry vendor type name
- registry JDBCType
- built-in
DatabaseSupportdefault
Every path is constructed with the engine’s seeded Random, so custom generators stay
reproducible exactly like the built-ins.
Plugin discovery. Third-party jars can contribute rules automatically by implementing
io.bloviate.ext.GeneratorPlugin and declaring it in
META-INF/services/io.bloviate.ext.GeneratorPlugin. Calling .discover() on the builder loads
every such plugin via ServiceLoader:
public class MyGenerators implements GeneratorPlugin { @Override public void contribute(GeneratorRegistry.Builder builder) { builder.registerTypeName("geometry", (column, random) -> new MyGeometryGenerator.Builder(random).build()); }}Semantic / realistic data (bloviate-datafaker)
Section titled “Semantic / realistic data (bloviate-datafaker)”The type-driven default fills an email VARCHAR from its type (random text). The optional
bloviate-datafaker module maps common column names (email, first_name, phone,
city, zip, …) to realistic Datafaker values — keeping Datafaker
out of the core (it only lands if you add this module).
It’s opt-in twice over: add the module, and build a GeneratorRegistry that includes its plugin
— either by discover() (it registers via ServiceLoader) or explicitly:
import io.bloviate.datafaker.DatafakerGeneratorPlugin;import io.bloviate.ext.GeneratorRegistry;
GeneratorRegistry registry = new GeneratorRegistry.Builder() .discover() // picks up DatafakerGeneratorPlugin from the classpath .build();
DatabaseConfiguration config = new DatabaseConfiguration( 128, 100, new PostgresSupport(), null, registry); // registry sits between per-column overrides and type defaultsSo an email column now yields jane.doe@example.com, first_name a real first name, phone a
(555) 555-01xx number, and so on. Properties:
- Reproducible — realistic values are seeded from the engine’s column seed, so the same seed
gives the same data; the locale is fixed (default
Locale.ENGLISH) so output is identical across machines. - Safe by default — reserved
example.*email domains and555-01xxphone numbers (never real identifiers); values are truncated to the column’s length. - Unmatched columns are untouched — anything the dictionary doesn’t recognize falls through to
the normal type-based generator. Don’t route
UNIQUE/primary-key columns through it — realistic dictionaries repeat at scale, so keep Bloviate’s sequence/seeded generators for keys.
Correlated columns (referential realism)
Section titled “Correlated columns (referential realism)”The plugin above makes each column realistic independently — so a row’s email won’t match its
first_name/last_name. For columns that should agree, share one RowContext and register
each as a projection of the same per-row entity. People.context(...) provides a coherent
person whose email, username, and full_name are derived from the row’s name:
import io.bloviate.datafaker.People;import io.bloviate.datafaker.Person;import io.bloviate.datafaker.RowContext;
RowContext<Person> person = People.context(42L, java.util.Locale.ENGLISH);
Set<ColumnConfiguration> columns = Set.of( new ColumnConfiguration("first_name", person.project(Person::firstName)), new ColumnConfiguration("last_name", person.project(Person::lastName)), new ColumnConfiguration("full_name", person.project(Person::fullName)), new ColumnConfiguration("email", person.project(Person::email)), // jane.doe7@example.com new ColumnConfiguration("username", person.project(Person::username)) // jane.doe7);Now full_name matches first_name/last_name, and email/username are derived from them. The
entity is a pure function of (seed, rowIndex), so the data is reproducible and
order-independent — sequential and parallel/partitioned fills produce identical values. Email and
username carry a row-index suffix so they stay unique at scale.
The same mechanism gives consistent geo tuples: Places.unitedStates(...) draws one real place
per row from a bundled reference dataset, so a row’s city, state, zip, and area_code agree
(a real city in its real state with a valid local ZIP — not “Springfield, WY 90210”):
import io.bloviate.datafaker.Geo;import io.bloviate.datafaker.Places;
RowContext<Geo> place = Places.unitedStates(42L);
Set<ColumnConfiguration> columns = Set.of( new ColumnConfiguration("city", place.project(Geo::city)), new ColumnConfiguration("state", place.project(Geo::stateAbbreviation)), new ColumnConfiguration("zip", place.project(Geo::zip)), new ColumnConfiguration("area_code", place.project(Geo::areaCode)));The bundled geo dataset is a representative sample of US places, not exhaustive, and tuples repeat across rows — keep
UNIQUEcolumns on Bloviate’s sequence/seeded generators.
Composite keys & foreign key fidelity
Section titled “Composite keys & foreign key fidelity”For schemas with composite primary keys, CompositeKeyComponentGenerator fills each key component
as one dimension of a dense, row-ordered cartesian product. Each component emits
start + ((rowNumber / repeat) % cycle), so a table receives exactly its cartesian cardinality of
rows with unique, collision-free keys — and a child table reusing the same formula produces
foreign keys that line up exactly with its parent’s keys.
For example, a stock table keyed by (s_w_id, s_i_id) across 2 warehouses × 10 items (20 rows) —
repeat for the outer dimension is the size of the inner one:
import io.bloviate.db.*;import io.bloviate.gen.CompositeKeyComponentGenerator;import java.util.Set;
int warehouses = 2, items = 10;
Set<ColumnConfiguration> stockKey = Set.of( // outer dimension: changes every `items` rows, cycles through warehouses new ColumnConfiguration("s_w_id", r -> new CompositeKeyComponentGenerator.Builder(r) .start(1).repeat(items).cycle(warehouses).build()), // inner dimension: changes every row, cycles through items new ColumnConfiguration("s_i_id", r -> new CompositeKeyComponentGenerator.Builder(r) .start(1).repeat(1).cycle(items).build()));
Set<TableConfiguration> tableConfigs = Set.of( new TableConfiguration("stock", (long) warehouses * items, stockKey));// → (1,1) (1,2) ... (1,10) (2,1) ... (2,10): every pair exactly onceVariable parent/child cardinality
Section titled “Variable parent/child cardinality”Real schemas rarely have a fixed number of children per parent (an order has some number of line
items). ChildCardinality assigns each parent a deterministic child count in [min, max], and the
matching generators keep a parent table and its child table perfectly in sync — no shared mutable
state, O(1) memory, reproducible from a seed.
import io.bloviate.db.*;import io.bloviate.gen.*;import java.util.Set;
long orders = 1000;
// each order gets 1–10 lines; the same seed makes the split reproducibleChildCardinality cardinality = new ChildCardinality(1, 10, 42L);long orderLineRows = cardinality.total(orders); // exact total, computed up front
// parent: a column holding each order's line countSet<ColumnConfiguration> orderCols = Set.of( new ColumnConfiguration("line_count", r -> new ChildCountGenerator.Builder(r).cardinality(cardinality).build()));
// child: parent key repeated per line, plus a 1-based line sequenceSet<ColumnConfiguration> lineCols = Set.of( new ColumnConfiguration("order_id", r -> new ChildKeyComponentGenerator.Builder(r) .cardinality(cardinality).start(1).repeat(1).cycle((int) orders).build()), new ColumnConfiguration("line_number", r -> new ChildKeyComponentGenerator.Builder(r) .cardinality(cardinality).sequence().start(1).build()));
Set<TableConfiguration> tableConfigs = Set.of( new TableConfiguration("orders", orders, orderCols), new TableConfiguration("order_lines", orderLineRows, lineCols));TPC-C benchmark data
Section titled “TPC-C benchmark data”Bloviate ships a ready-made configuration for the full TPC-C schema. A single call wires up all nine tables with spec-faithful cardinalities, composite keys, foreign-key fidelity, variable order-line counts, per-district customer-id permutations, deterministic last-name enumeration, and delivery state — matching the TPC-C initial database population (clause 4.3.3.1).
import io.bloviate.db.*;import io.bloviate.ext.PostgresSupport;import io.bloviate.gen.tpcc.TPCCConfiguration;import java.util.Set;
// Build all TPC-C table configs for a given scale factor (number of warehouses)Set<TableConfiguration> tpcc = TPCCConfiguration.build(2);
DatabaseConfiguration config = new DatabaseConfiguration( 128, 100, new PostgresSupport(), tpcc);
new DatabaseFiller.Builder(connection, config) .build() .fill();Need finer control over the cardinalities? An overload exposes every dimension:
Set<TableConfiguration> tpcc = TPCCConfiguration.build( 2, // warehouses (scale factor) 100, // items 10, // districts per warehouse 3000, // customers (and orders) per district 5, // min order lines per order 15 // max order lines per order);The matching create_tpcc.{postgres,mysql,cockroachdb}.sql DDL is available in the
Bloviate repository. The reusable
building blocks behind TPCCConfiguration — CompositeKeyComponentGenerator, ChildCardinality,
GroupedPermutationGenerator (per-group shuffles), and GroupedPrefixGenerator (nullable/sparse
columns) — live in io.bloviate.gen and can be composed for other benchmark schemas (TPC-H,
TPC-DS, and so on).
First-of-month dates
Section titled “First-of-month dates”TruncatedDateGenerator (since 3.5.0) produces the first day of a month, quarter or year, for a
column guarded by a check such as CHECK (date_trunc('month', billing_month) = billing_month) or
CHECK (EXTRACT(day FROM period_start) = 1). On PostgreSQL the engine picks it for you when it reads
such a check (see Constraint conformance); use it
directly to override a column explicitly, or on a database that isn’t read for constraints:
import io.bloviate.db.*;import io.bloviate.gen.TruncatedDateGenerator;import io.bloviate.gen.TruncatedDateGenerator.Unit;import java.time.LocalDate;import java.util.Set;
Set<ColumnConfiguration> columns = Set.of( // a DATE column: some first of a month between 2022 and 2024 new ColumnConfiguration("billing_month", random -> new TruncatedDateGenerator.Builder(random) .start(LocalDate.of(2022, 1, 1)).end(LocalDate.of(2025, 1, 1)).build()), // a TIMESTAMP or TIMESTAMPTZ column: midnight on the first of a quarter new ColumnConfiguration("period_start", random -> new TruncatedDateGenerator.Builder(random) .unit(Unit.QUARTER).timestamp(true).build()));
new TableConfiguration("invoices", 1_000, columns);- The range is
[start, end): the generator draws uniformly from the period starts that fall in it. The default is the ten years from 2015-01-01 up to (excluding) 2025-01-01, anchored to a fixed reference date rather than the wall clock, so the same seed gives the same data on every run. - A
DATEcolumn is bound as aLocalDate. Withtimestamp(true)aTIMESTAMPorTIMESTAMP WITH TIME ZONEcolumn is bound as a zone-less midnight, which the database reads in its session time zone — the zone adate_trunccheck is evaluated in — so the check holds whatever that zone is. (A zone whose clocks skip midnight on the first is the one exception.) get(ResultSet, int)reads a column back as the sameLocalDate. For atimestamptzthat is the date in the zone the database renders the value in (PostgreSQL: the session zone), taken from the driver’s text form; the typed accessors can’t give it (pgjdbc rejectsLocalDateTimefor atimestamptz, and itsOffsetDateTimeis normalised to UTC, a day early east of UTC). A driver whose text form is notyyyy-MM-dd[ HH:mm[:ss[.f]][offset]]falls back togetObject, which is exact only if the driver reports the zone the value was bound in.
Relative date windows
Section titled “Relative date windows”The temporal generators can draw from a window relative to the fill’s asOf anchor (since 3.7.0)
instead of fixed dates, so a column can be “within the last 90 days” and still be reproducible. The
generator builders take a resolved window with window(...):
import io.bloviate.db.*;import io.bloviate.gen.*;import java.util.Set;
Set<ColumnConfiguration> columns = Set.of( // TIMESTAMP[TZ]: uniform over [asOf - 90d, asOf) ColumnConfiguration.relative("placed_at", RelativeWindow.withinLast("90d"), (random, window) -> new SqlTimestampGenerator.Builder(random).window(window).build()), // DATE: one of the UTC dates from 30 days ago up to, excluding, 7 days ahead ColumnConfiguration.relative("due_on", RelativeWindow.between("-30d", "+7d"), (random, window) -> new SqlDateGenerator.Builder(random).window(window).build()), // first of a month in the last six months ColumnConfiguration.relative("billing_month", RelativeWindow.withinLast("6M"), (random, window) -> new TruncatedDateGenerator.Builder(random).window(window).build()), // mostly recent: skewed toward the end of the last 90 days new ColumnConfiguration("last_login", Distributions.recentTimestamps(RelativeWindow.withinLast("90d"), 3.0)));window(...) exists on SqlTimestampGenerator, SqlDateGenerator (whole UTC dates), DateGenerator,
InstantGenerator, SkewedTimestampGenerator and TruncatedDateGenerator. A window is always
end-exclusive; SkewedTimestampGenerator keeps that by setting its own end (which is inclusive when set
directly with end(...)) to the window end minus one millisecond. A generator that needs the anchor for something else builds itself from the
GenerationContext, through ColumnGeneratorFactory.contextual(...) or, in a registry rule,
GeneratorFactory.contextual(...). Offset syntax, boundary semantics and the rule that makes the output
reproducible (pin asOf) are in Relative date ranges and asOf.
SqlDateGenerator, SqlTimeGenerator, SqlTimestampGenerator, SkewedTimestampGenerator and
CurrentSqlTimestampGenerator bind their values in UTC, not the JVM’s default zone, so what they
store is the same on every machine — see
Reproducible data with seeds. A custom generator
that binds a java.sql temporal should do the same, through io.bloviate.gen.TemporalBinding.
Data generator types
Section titled “Data generator types”Bloviate includes generators for all common database types:
// Numeric generators (ranges are [start, end))new IntegerGenerator.Builder(random).start(1).end(1000).build()new DoubleGenerator.Builder(random).start(0.0).end(100.0).build()new BigDecimalGenerator.Builder(random).precision(10).digits(2).build()
// String generatorsnew SimpleStringGenerator.Builder(random).size(20).build()new VariableStringGenerator.Builder(random).minLength(5).maxLength(50).build()new UUIDGenerator.Builder(random).build()
// Date/Time generatorsnew DateGenerator.Builder(random).build()new SqlTimestampGenerator.Builder(random).build()new InstantGenerator.Builder(random).build()
// ... within a window relative to the fill's asOf anchor (since 3.7.0)new SqlTimestampGenerator.Builder(random).window(RelativeWindow.withinLast("90d").resolve(asOf)).build()
// First day of a month / quarter / year, for columns that only admit such dates (since 3.5.0)new TruncatedDateGenerator.Builder(random).build() // DATE, first of a monthnew TruncatedDateGenerator.Builder(random).unit(Unit.QUARTER).build() // Jan/Apr/Jul/Oct 1stnew TruncatedDateGenerator.Builder(random).timestamp(true).build() // TIMESTAMP[TZ], midnight on the 1st
// Boolean and specialized generatorsnew BooleanGenerator.Builder(random).build()new JsonbGenerator.Builder(random).build()new InetGenerator.Builder(random).build()
// Constant / fixed-scale generators (handy for column overrides)new StaticStringGenerator.Builder(random).value("OE").build()new StaticIntegerGenerator.Builder(random).value(0).build()new ScaledBigDecimalGenerator.Builder(random).start(0.0).end(0.2).scale(4).build()
// Deterministic / sequential generatorsnew SequentialIntegerGenerator.Builder(random).start(1).end(1000).build()Additional generators cover Long, Float, Byte, Short, Character, java.sql date/time
types, arrays, and CockroachDB-specific types (bit strings, intervals). See the io.bloviate.gen
package for the full set.