1. Project Clover database Wed Nov 12 2025 05:07:35 UTC
  2. Package guru.mikelue.foxglove.jdbc

File JdbcTableFacet.java

 

Coverage histogram

../../../../img/srcFileCovDistChart10.png
0% of files have more coverage

Code metrics

22
89
23
2
633
263
34
0.38
3.87
11.5
1.48

Classes

Class Line # Actions
JdbcTableFacet 44 28 0% 17 0
1.0100%
JdbcTableFacet.Builder 200 61 0% 17 0
1.0100%
 

Contributing tests

This file is covered by 135 tests. .

Source view

1    package guru.mikelue.foxglove.jdbc;
2   
3    import java.util.*;
4    import java.util.function.Consumer;
5    import java.util.function.Supplier;
6   
7    import org.apache.commons.lang3.StringUtils;
8    import org.apache.commons.lang3.Validate;
9   
10    import guru.mikelue.foxglove.ColumnMeta;
11    import guru.mikelue.foxglove.TableFacet;
12    import guru.mikelue.foxglove.TupleAccessor;
13    import guru.mikelue.foxglove.functional.Int4SequenceSupplier;
14    import guru.mikelue.foxglove.functional.SequenceSupplier;
15    import guru.mikelue.foxglove.functional.SupplierDecider;
16    import guru.mikelue.foxglove.jdbc.ColumnSettingSteps.CartesianProductSettingStep;
17    import guru.mikelue.foxglove.jdbc.ColumnSettingSteps.ColumnSimpleStep;
18    import guru.mikelue.foxglove.jdbc.ColumnSettingSteps.KeyOfIntSettingStep;
19    import guru.mikelue.foxglove.jdbc.ColumnSettingSteps.ReferenceSettingStep;
20    import guru.mikelue.foxglove.setting.DataSetting;
21    import guru.mikelue.foxglove.setting.DataSettingInfo;
22    import guru.mikelue.foxglove.setting.SettingAware;
23    import guru.mikelue.foxglove.setting.SettingProvider;
24   
25    /**
26    * JDBC implementation of {@link TableFacet}.
27    *
28    * <p>
29    * <strong style="color:red">Important: this object is not reusable across tests, you should build an new one for each test.</strong>
30    *
31    * <p>
32    * Use {@link #builder(String)} to build this object.
33    *
34    * <pre><code class="language-java">
35    * JdbcTableFacet tableFacet = JdbcTableFacet.builder("test_table")
36    * .build();
37    * </code></pre>
38    *
39    * See more examples in {@link Builder}.
40    *
41    * @see #builder(String)
42    * @see JdbcDataGenerator
43    */
 
44    public class JdbcTableFacet implements TableFacet, SettingProvider {
45    /**
46    * Starts to build a {@link JdbcTableFacet} with table name.
47    *
48    * @param tableName The name of table
49    *
50    * @return The builder to build {@link JdbcTableFacet}
51    */
 
52  397 toggle public static Builder builder(String tableName)
53    {
54  397 return new Builder(tableName);
55    }
56   
57    /**
58    * Builds a {@link JdbcTableFacet} with table name.
59    *
60    * <h2>Number of rows</h2>
61    *
62    * You can only use <strong>one of</strong> following ways to decide the number of rows to be generated:
63    *
64    * <ul>
65    * <li>Using {@link #numberOfRows(int)} in the builder.</li>
66    * <li>Using {@link #keyOfInt(String)} by limit the amount of generated keys.</li>
67    * <li>Using {@link #cartesianProduct(String)} on at least one column.</li>
68    * <li>Using {@link #referencing(String)} with cardinality(self side) to parent rows.</li>
69    * </ul>
70    *
71    * <hr>
72    *
73    * <h2>Basic example</h2>
74    *
75    * <ul>
76    * <li>Use {@link #column(String)} to configure value generator for a column.</li>
77    * <li>Use {@link #numberOfRows(int)} to configure fixed number of rows to be generated.</li>
78    * <li>Use {@link #includeColumns(String...)} to include those columns only(others are excluded).</li>
79    * <li>Use {@link #excludeColumns(String...)} to exclude some columns.</li>
80    * </ul>
81    *
82    * Example(<em>builds {@code 10} rows</em>):
83    * <pre><code class="language-java">
84    * var facet = JdbcTableFacet.builder(TABLE_CAR)
85    * .numberOfRows(10)
86    * // Round robin among several brands
87    * .column("cr_brand")
88    * .roundRobin("Toyota", "Honda", "Ford", "BMW", "Audi")
89    * // Generates year between 2015 and 2025
90    * .column("cr_year")
91    * .useSpec(() -&gt; gen().shorts().range((short)2015, (short)2025))
92    * // Random choice for number of seats
93    * .column("cr_seats")
94    * .useSpec(() -&gt; gen().oneOf(2, 4, 5, 7))
95    * // Color with nullable property get nullable values
96    * .&lt;String&gt;column("cr_color")
97    * .decideSupplier(columnMeta -&gt; {
98    * var colorGenerator = gen().oneOf("Red", "Blue", "Green");
99    *
100    * if (columnMeta.properties().contains(Property.NULLABLE)) {
101    * return colorGenerator.nullable();
102    * }
103    *
104    * return colorGenerator;
105    * })
106    * // Fixed value for status column
107    * .column("cr_status")
108    * .fixed(null)
109    * // Includes these two columns by auto-generating their values
110    * .includeColumns("cr_license_plate", "cr_daily_rate", "cr_created_at", "cr_model")
111    * .build();
112    * </code></pre>
113    *
114    * <h2>Example for deciding number of rows</h2>
115    *
116    * <h3>Key column</h3>
117    * Use {@link #keyOfInt(String)} to configure a key column as integral value,
118    *
119    * <p>Example(<em>builds {@code 10} rows by number of key values</em>):
120    * <pre><code class="language-java">
121    * var facet = JdbcTableFacet.builder(TABLE_CAR)
122    * .keyOfInt("cr_id").limit(1000, 10)
123    * .build();
124    * </code></pre>
125    *
126    * <h3>Cartesian product</h3>
127    *
128    * Use {@link #cartesianProduct(String)} to configure columns for Cartesian product.
129    *
130    * <p>Example(<em>builds <code>3 * 3 = 9</code> rows</em>):
131    * <pre><code class="language-java">
132    * var facet = JdbcTableFacet.builder(TABLE_CAR)
133    * .cartesianProduct("cr_brand")
134    * .domain("Toyota", "Honda", "Ford")
135    * .cartesianProduct("cr_year")
136    * .domain(2020, 2021, 2022)
137    * .build();
138    * </code></pre>
139    *
140    * <h3>Referencing</h3>
141    *
142    * Use {@link #referencing(String)} to configure a column referencing to another table,
143    *
144    * <p>Example(<em>builds <code>10 * 2 = 20</code> rows by number of key values</em>):
145    *
146    * <pre><code class="language-java">
147    * // Prepares parent facet
148    * var carFacet = JdbcTableFacet.builder(TABLE_CAR)
149    * .numberOfRows(10)
150    * .build();
151    *
152    * // Generates rows referencing to the parent facet.
153    * var featureFacet = JdbcTableFacet.builder(TABLE_CAR_FEATURE)
154    * .referencing("cf_cr_id").parent(carFacet, "cr_id")
155    * // Every car is referenced by one feature.
156    * .cardinality(2)
157    * .column("cf_feature_name")
158    * .fixed("Sunroof")
159    * .build();
160    * </code></pre>
161    *
162    * <h2>Other features</h2>
163    *
164    * <h3>Change row values before insertion</h3>
165    *
166    * Use {@link #onTupleGenerated(Consumer)} to change generated values before insertion.
167    *
168    * <pre><code class="language-java">
169    * var facet = JdbcTableFacet.builder(TABLE_CAR)
170    * .numberOfRows(RANDOM_SIZE)
171    * .column("cr_brand")
172    * .roundRobin("Toyota", "Honda", "Ford", "BMW", "Audi")
173    * // Sets the value of "cr_license_plate" by combination of "cr_brand" and a random number
174    * .onTupleGenerated(tuple -> tuple.setValue(
175    * "cr_license_plate",
176    * tuple.getValue("cr_brand") + "-" + randomNumber.get()
177    * ))
178    * .build();
179    * </code></pre>
180    *
181    * <h3>Applies {@link DataSetting}</h3>
182    *
183    * {@link #withSetting(DataSettingInfo)} can be used to apply {@link DataSettingInfo} locally.
184    *
185    * <pre><code class="language-java">
186    * var setting = new DataSetting()
187    * // For any column with type of "VARCHAR", using the supplier
188    * .givenType(JDBCType.VARCHAR)
189    * .useSupplier(() -> sampleText + suffixSupplier.get())
190    *
191    * var facet = JdbcTableFacet.builder(TABLE_CAR)
192    * // Uses the setting on the whole table
193    * .withSetting(setting)
194    * .numberOfRows(RANDOM_SIZE)
195    * .build();
196    * </code></pre>
197    *
198    * @see DataSetting
199    */
 
200    public static class Builder implements SettingAware<Builder> {
201    private JdbcTableFacet newTableFacet = new JdbcTableFacet();
202    private CartesianProductBuilder cartesianProductBuilder = null;
203    private NumberSource sourceOfRowNumber = null;
204   
205    /**
206    * Constructs the builder with table name.
207    *
208    * @param newTableName The name of table
209    */
 
210  397 toggle private Builder(String newTableName)
211    {
212  397 newTableName = StringUtils.trimToNull(newTableName);
213   
214  397 Validate.notNull(newTableName, "Table name must not be blank");
215   
216  397 newTableFacet.tableName = newTableName;
217  397 newTableFacet.valueTomb = new ValueTomb(newTableName);
218    }
219   
220    /**
221    * Sets the number of rows to be generated for the table.
222    *
223    * @param numberOfRows The number of rows to be generated
224    *
225    * @return This builder
226    */
 
227  111 toggle public Builder numberOfRows(int numberOfRows)
228    {
229  111 validateRowNumberIntegrity(null);
230  109 Validate.isTrue(numberOfRows > 0, "Number of numberOfRows must be greater than zero");
231  109 sourceOfRowNumber = NumberSource.Plain;
232   
233  109 newTableFacet.supplierForNumberOfRows = () -> numberOfRows;
234  109 return this;
235    }
236   
237    /**
238    * Starts to configure key column as integral value,
239    * which would limit for number of generated rows for this table.
240    *
241    * If you don't have need to limit the number of generated values,
242    * you can use {@link #column(String)} with {@link Int4SequenceSupplier}, etc.
243    *
244    * @param columnName The name of column
245    *
246    * @return The next step to configure value generator for the key column
247    *
248    * @see SequenceSupplier
249    */
 
250  26 toggle public KeyOfIntSettingStep keyOfInt(String columnName)
251    {
252  26 validateRowNumberIntegrity(columnName);
253  24 final String safeColumnName = safeColumnName(columnName);
254  24 sourceOfRowNumber = NumberSource.KeyColumn;
255   
256  24 return new KeySettingStepImpl(
257    this,
258    keySupplier -> {
259  24 newTableFacet.supplierForNumberOfRows = keySupplier::getNumberOfRows;
260   
261  24 @SuppressWarnings("unchecked")
262    var objectSupplier = (Supplier<Object>)(Object)keySupplier;
263  24 newTableFacet.columnSuppliers.put(
264    safeColumnName,
265    columnMeta -> objectSupplier
266    );
267    }
268    );
269    }
270   
271    /**
272    * Starts to configure table reference for a column.
273    *
274    * <p>
275    * This is differ from {@link ColumnSimpleStep#from(JdbcTableFacet, String)}
276    * that this method would decide the number of rows to be generated.
277    *
278    * @param <T> The type of value generated for the column
279    * @param columnName The name of column referencing to another table
280    *
281    * @return The next step to configure table reference for the column
282    */
 
283  6 toggle @SuppressWarnings("unchecked")
284    public <T> ReferenceSettingStep<T> referencing(String columnName)
285    {
286  6 validateRowNumberIntegrity(columnName);
287   
288  6 final String safeColumnName = safeColumnName(columnName);
289  6 sourceOfRowNumber = NumberSource.Reference;
290   
291  6 return new ReferenceSettingStepImpl<>(
292    this,
293    cardinalityInfo -> {
294  6 newTableFacet.supplierForNumberOfRows = cardinalityInfo::getNumberOfRows;
295  6 newTableFacet.columnSuppliers.put(
296    safeColumnName,
297    columnMeta -> (Supplier<Object>)cardinalityInfo.buildLazySupplier()
298    );
299    }
300    );
301    }
302   
303    /**
304    * Starts to configure cartesian product for a column.
305    *
306    * @param <T> The type of value generated for the column
307    * @param columnName The name of column
308    *
309    * @return The next step to configure cartesian product for the column
310    */
 
311  28 toggle public <T> CartesianProductSettingStep<T> cartesianProduct(String columnName)
312    {
313  28 Validate.isTrue(
314    sourceOfRowNumber == null ||
315    sourceOfRowNumber == NumberSource.CartesianProduct,
316    "Number of rows conflict[%s]: %s", columnName, sourceOfRowNumber
317    );
318  27 sourceOfRowNumber = NumberSource.CartesianProduct;
319   
320  27 if (cartesianProductBuilder == null) {
321  16 cartesianProductBuilder = new CartesianProductBuilder();
322    }
323   
324  27 final String safeColumnName = safeColumnName(columnName);
325   
326  27 return new CartesianProductStepImpl<T>(
327    this,
328    values -> cartesianProductBuilder.putDomain(safeColumnName, values)
329    );
330    }
331   
332    /**
333    * Starts to configure value generator by column name.
334    *
335    * @param <T> The type of value generated for the column
336    * @param columnName The name of column
337    *
338    * @return The next step to configure value generator for the column
339    */
 
340  116 toggle public <T> ColumnSimpleStep<T> column(String columnName)
341    {
342  116 final String safeColumnName = safeColumnName(columnName);
343   
344  111 return new ColumnSettingStepImpl<T>(this,
345    decider -> {
346  111 newTableFacet.columnSuppliers.put(safeColumnName, decider);
347    }
348    );
349    }
350   
351    /**
352    * These columns would be generated automatically in spite of their properties.
353    *
354    * <p>
355    * This setting would take precedence over {@link DataSettingInfo#isAutoGenerating(ColumnMeta)}.
356    *
357    * @param columns The column names
358    *
359    * @return This builder
360    */
 
361  16 toggle public Builder includeColumns(String... columns)
362    {
363  16 Validate.isTrue(
364    newTableFacet.exclusionForColumns.isEmpty(),
365    "Cannot include columns when exclusion for columns is set already"
366    );
367  16 Validate.notEmpty(
368    columns, "At least one column must be specified to include"
369    );
370   
371  16 for (var includingColumn : columns) {
372  27 newTableFacet.inclusionForColumns.add(safeColumnName(includingColumn));
373    }
374   
375  14 return this;
376    }
377   
378    /**
379    * Excludes the columns by their name.
380    *
381    * <p>
382    * This method is differ from {@link #includeColumns(String...)},
383    * the the value generation for un-set columns is decided by {@link DataSetting}.
384    *
385    * @param columns The column names
386    *
387    * @return This builder
388    */
 
389  11 toggle public Builder excludeColumns(String... columns)
390    {
391  11 Validate.isTrue(
392    newTableFacet.inclusionForColumns.isEmpty(),
393    "Cannot exclude columns when inclusion for columns is set already"
394    );
395  10 Validate.notEmpty(
396    columns, "At least one column must be specified to exclude"
397    );
398   
399  10 for (var excludingColumn : columns) {
400  18 newTableFacet.exclusionForColumns.add(safeColumnName(excludingColumn));
401    }
402   
403  8 return this;
404    }
405   
406    /**
407    * Sets the consumer to handle generated tuple.
408    *
409    * You can use this callback to change generated values before insertion,
410    *
411    * @param tupleGeneratedConsumer The consumer to handle generated tuple
412    *
413    * @return This builder
414    */
 
415  3 toggle public Builder onTupleGenerated(Consumer<TupleAccessor> tupleGeneratedConsumer)
416    {
417  3 Validate.notNull(tupleGeneratedConsumer, "Tuple generated consumer must not be null");
418   
419  3 newTableFacet.tupleHandler = tupleGeneratedConsumer;
420  3 return this;
421    }
422   
423    /**
424    * This object has highest priority than {@link JdbcDataGenerator#withSetting(DataSettingInfo)} and
425    * {@link DataSetting#defaults()}.
426    *
427    * @param setting The setting to use
428    *
429    * @return This instance
430    */
 
431  4 toggle @Override
432    public Builder withSetting(DataSettingInfo setting)
433    {
434  4 Validate.notNull(setting, "Data setting must not be null");
435   
436  4 newTableFacet.dataSetting = Optional.of(setting);
437  4 return this;
438    }
439   
440    /**
441    * Builds the {@link JdbcTableFacet}.
442    *
443    * @return The built {@link JdbcTableFacet}
444    */
 
445  375 toggle public JdbcTableFacet build()
446    {
447    /*
448    * Puts built values of columns following Cartesian product configuration
449    */
450  375 if (cartesianProductBuilder != null) {
451  13 newTableFacet.supplierForNumberOfRows = cartesianProductBuilder::getNumberOfRows;
452   
453  13 for (var columnName : cartesianProductBuilder.getColumnNames()) {
454  24 newTableFacet.columnSuppliers.put(
455    columnName,
456    columnMeta -> cartesianProductBuilder.buildLazySupplier(columnName)
457    );
458    }
459    }
460    // :~)
461   
462  375 return newTableFacet;
463    }
464   
 
465  143 toggle private void validateRowNumberIntegrity(String columnName)
466    {
467  143 Validate.isTrue(
468    sourceOfRowNumber == null,
469    "Number of rows conflict[%s]: %s",
470  143 columnName != null ? columnName : "<UNKNOWN>", sourceOfRowNumber
471    );
472    }
473   
 
474  218 toggle private String safeColumnName(String columnName)
475    {
476  218 final String safeColumnName = StringUtils.trimToEmpty(columnName).toLowerCase();
477  218 Validate.notBlank(safeColumnName, "Column name must not be blank");
478   
479    /*
480    * Ensures the integrity of column configuration
481    */
482  218 Validate.isTrue(!newTableFacet.inclusionForColumns.contains(safeColumnName),
483    "Column '%s' is set to be included(auto-generating) already", safeColumnName
484    );
485  216 Validate.isTrue(!newTableFacet.exclusionForColumns.contains(safeColumnName),
486    "Column '%s' is set to be excluded(auto-generating) already", safeColumnName
487    );
488  214 Validate.isTrue(!newTableFacet.columnSuppliers.containsKey(safeColumnName),
489    "Column '%s' has been configured already", safeColumnName
490    );
491   
492  210 if (cartesianProductBuilder != null) {
493  30 Validate.isTrue(!cartesianProductBuilder.isExisting(safeColumnName),
494    "Column '%s' has been configured by Cartesian product already", safeColumnName
495    );
496    }
497    // :~)
498   
499  209 return safeColumnName;
500    }
501    }
502   
503    private Supplier<Integer> supplierForNumberOfRows = null;
504   
505    private String tableName = null;
506    private Optional<DataSettingInfo> dataSetting = Optional.empty();
507   
508    private Consumer<TupleAccessor> tupleHandler = null;
509   
510    /*
511    * Any kind of supplier is put to this:
512    *
513    * 1. General suppliers
514    * 2. Key column
515    * 3. Columns defined by Cartesian product
516    */
517    private Map<String, SupplierDecider<?>> columnSuppliers = new HashMap<>(8);
518   
519    private Set<String> inclusionForColumns = new HashSet<>();
520    private Set<String> exclusionForColumns = new HashSet<>();
521   
522    private ValueTomb valueTomb;
523   
 
524  397 toggle private JdbcTableFacet() {}
525   
526    /**
527    * {@inheritDoc}
528    */
 
529  495 toggle @Override
530    public String tableName()
531    {
532  495 return tableName;
533    }
534   
535    /**
536    * {@inheritDoc}
537    */
 
538  111 toggle @Override
539    public int getNumberOfRows()
540    {
541  111 if (supplierForNumberOfRows != null) {
542  109 return supplierForNumberOfRows.get();
543    }
544   
545  2 if (dataSetting.isPresent()) {
546  1 return dataSetting.get().getDefaultNumberOfRows();
547    }
548   
549  1 return DataSetting.defaults().getDefaultNumberOfRows();
550    }
551   
552    /**
553    * Retrieves the configured {@link Supplier} for given column name.
554    *
555    * @param columnName The name of column
556    *
557    * @return The configured {@link Supplier} or empty
558    */
 
559  773 toggle @SuppressWarnings("unchecked")
560    <T> Optional<Supplier<T>> resolveSupplier(ColumnMeta columnMeta)
561    {
562  773 var lowerCaseName = columnMeta.name().toLowerCase();
563   
564  773 if (columnSuppliers.containsKey(lowerCaseName)) {
565  114 SupplierDecider<T> foundDecider = (SupplierDecider<T>)columnSuppliers.get(lowerCaseName);
566   
567  114 return Optional.of(foundDecider.apply(columnMeta));
568    }
569   
570  659 return Optional.empty();
571    }
572   
573    /**
574    * {@inheritDoc}
575    */
 
576  88 toggle @Override
577    public Optional<DataSettingInfo> getSetting()
578    {
579  88 return this.dataSetting;
580    }
581   
 
582  152 toggle ValueTomb getValueTomb()
583    {
584  152 return valueTomb;
585    }
586   
 
587  91 toggle List<String> getConfiguredNamesOfColumn()
588    {
589  91 var result = new ArrayList<String>(
590    columnSuppliers.size() +
591    inclusionForColumns.size() + exclusionForColumns.size()
592    );
593   
594  91 result.addAll(columnSuppliers.keySet());
595  91 result.addAll(inclusionForColumns);
596  91 result.addAll(exclusionForColumns);
597   
598  91 return result;
599    }
600   
601    /**
602    * Since there is automatic generation of columns,
603    * this method give three states:
604    *
605    * 1. Include - the column is must to be generated
606    * 2. Exclude - the column is must not to be generated
607    * 3. NotSet - let DataSetting to decide
608    */
 
609  1072 toggle ColumnInclusionMode getColumnInclusion(ColumnMeta columnMeta)
610    {
611  1072 var lowerCaseName = columnMeta.name().toLowerCase();
612   
613  1072 if (columnSuppliers.containsKey(lowerCaseName)) {
614  106 return ColumnInclusionMode.Include;
615    }
616   
617  966 if (!inclusionForColumns.isEmpty()) {
618  48 return inclusionForColumns.contains(lowerCaseName) ?
619    ColumnInclusionMode.Include : ColumnInclusionMode.Exclude;
620    }
621   
622  918 if (exclusionForColumns.contains(lowerCaseName)) {
623  10 return ColumnInclusionMode.Exclude;
624    }
625   
626  908 return ColumnInclusionMode.NotSet;
627    }
628   
 
629  91 toggle Consumer<TupleAccessor> getHandlerOfTuple()
630    {
631  91 return tupleHandler;
632    }
633    }