Code review comment: Can we share code between WriteWithResult and WriteVoid ? This introduces a significant amount of code duplication.

Code under review:
public PCollection<Void> expand(PCollection<T> input) {
      WriteVoid<T> spec = this;
      checkArgument(
          (spec.getDataSourceProviderFn() != null),
          "withDataSourceConfiguration() or withDataSourceProviderFn() is required");
      
      if (input.hasSchema() && !spec.hasStatementAndSetter()) {
        checkArgument(spec.getTable() != null, "table cannot be null if statement is not provided");
        List<SchemaUtil.FieldWithIndex> fields = spec.getFilteredFields(input.getSchema());
        spec =
            spec.toBuilder()
                .setStatement(spec.generateStatement(fields))
                .setPreparedStatementSetter(
                    new AutoGeneratedPreparedStatementSetter(fields, input.getToRowFunction()))
                .build();
      } else {
        checkArgument(spec.getStatement() != null, "withStatement() is required");
        checkArgument(
            spec.getPreparedStatementSetter() != null, "withPreparedStatementSetter() is required");
      }
      PCollection<Iterable<T>> iterables;
      if (input.isBounded() == IsBounded.UNBOUNDED
          && getAutoSharding() != null
          && getAutoSharding()) {
        iterables =
            input
                .apply(WithKeys.<String, T>of(""))
                .apply(
                    GroupIntoBatches.<String, T>ofSize(DEFAULT_BATCH_SIZE)
                        .withMaxBufferingDuration(Duration.millis(200))
                        .withShardedKey())
                .apply(Values.create());
      } else {
        iterables =
            input.apply(
                ParDo.of(
                    new DoFn<T, Iterable<T>>() {
                      List<T> outputList;

                      @ProcessElement
                      public void process(ProcessContext c) {
                        if (outputList == null) {
                          outputList = new ArrayList<>();
                        }
                        outputList.add(c.element());
                      }

                      @FinishBundle
                      public void finish(FinishBundleContext c) {
                        c.output(outputList, Instant.now(), GlobalWindow.INSTANCE);
                        outputList = null;
                      }
                    }));
      }
      return iterables
          .apply(
              ParDo.of(
                  new WriteFn<T, Void>(
                      WriteFnSpec.builder()
                          .setRetryConfiguration(spec.getRetryConfiguration())
                          .setRetryStrategy(spec.getRetryStrategy())
                          .setPreparedStatementSetter(spec.getPreparedStatementSetter())
                          .setDataSourceProviderFn(spec.getDataSourceProviderFn())
                          .setTable(spec.getTable())
                          .setStatement(spec.getStatement())
                          .setBatchSize(spec.getBatchSize())
                          .setReturnResults(false)
                          .build())))
          .setCoder(VoidCoder.of());
    }

Line commented on: PCollection<Iterable<T>> iterables;

Revised code:
public PCollection<Void> expand(PCollection<T> input) {
      WriteVoid<T> spec = this;
      checkArgument(
          (spec.getDataSourceProviderFn() != null),
          "withDataSourceConfiguration() or withDataSourceProviderFn() is required");
      
      if (input.hasSchema() && !spec.hasStatementAndSetter()) {
        checkArgument(spec.getTable() != null, "table cannot be null if statement is not provided");
        List<SchemaUtil.FieldWithIndex> fields = spec.getFilteredFields(input.getSchema());
        spec =
            spec.toBuilder()
                .setStatement(spec.generateStatement(fields))
                .setPreparedStatementSetter(
                    new AutoGeneratedPreparedStatementSetter(fields, input.getToRowFunction()))
                .build();
      } else {
        checkArgument(spec.getStatement() != null, "withStatement() is required");
        checkArgument(
            spec.getPreparedStatementSetter() != null, "withPreparedStatementSetter() is required");
      }

      PCollection<Iterable<T>> iterables = JdbcIO.<T>batchElements(input, getAutoSharding());

      return iterables
          .apply(
              ParDo.of(
                  new WriteFn<T, Void>(
                      WriteFnSpec.builder()
                          .setRetryConfiguration(spec.getRetryConfiguration())
                          .setRetryStrategy(spec.getRetryStrategy())
                          .setPreparedStatementSetter(spec.getPreparedStatementSetter())
                          .setDataSourceProviderFn(spec.getDataSourceProviderFn())
                          .setTable(spec.getTable())
                          .setStatement(spec.getStatement())
                          .setBatchSize(spec.getBatchSize())
                          .setReturnResults(false)
                          .build())))
          .setCoder(VoidCoder.of());
    }