001 /*
002 * The MIT License
003 *
004 * Copyright (c) 2012-2015, Ninja Squad
005 *
006 * Permission is hereby granted, free of charge, to any person obtaining a copy
007 * of this software and associated documentation files (the "Software"), to deal
008 * in the Software without restriction, including without limitation the rights
009 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010 * copies of the Software, and to permit persons to whom the Software is
011 * furnished to do so, subject to the following conditions:
012 *
013 * The above copyright notice and this permission notice shall be included in
014 * all copies or substantial portions of the Software.
015 *
016 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
017 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
018 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
019 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
020 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
021 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
022 * THE SOFTWARE.
023 */
024
025 package com.ninja_squad.dbsetup.operation;
026
027 import javax.annotation.Nonnull;
028 import javax.annotation.concurrent.Immutable;
029 import java.sql.Connection;
030 import java.sql.ParameterMetaData;
031 import java.sql.PreparedStatement;
032 import java.sql.SQLException;
033 import java.util.ArrayList;
034 import java.util.Arrays;
035 import java.util.HashMap;
036 import java.util.HashSet;
037 import java.util.Iterator;
038 import java.util.LinkedHashMap;
039 import java.util.List;
040 import java.util.Map;
041 import java.util.Set;
042
043 import com.ninja_squad.dbsetup.bind.Binder;
044 import com.ninja_squad.dbsetup.bind.BinderConfiguration;
045 import com.ninja_squad.dbsetup.bind.Binders;
046 import com.ninja_squad.dbsetup.generator.ValueGenerator;
047 import com.ninja_squad.dbsetup.generator.ValueGenerators;
048 import com.ninja_squad.dbsetup.util.Preconditions;
049
050 /**
051 * Operation which inserts one or several rows into a table. Example usage:
052 * <pre>
053 * Insert insert =
054 * Insert.into("CLIENT")
055 * .columns("CLIENT_ID", "FIRST_NAME", "LAST_NAME", "DATE_OF_BIRTH", "CLIENT_TYPE")
056 * .values(1L, "John", "Doe", "1975-07-19", ClientType.NORMAL)
057 * .values(2L, "Jack", "Smith", "1969-08-22", ClientType.HIGH_PRIORITY)
058 * .withDefaultValue("DELETED", false)
059 * .withDefaultValue("VERSION", 1)
060 * .withBinder(new ClientTypeBinder(), "CLIENT_TYPE")
061 * .build();
062 * </pre>
063 *
064 * The above operation will insert two rows inside the CLIENT table. For each row, the column DELETED will be set to
065 * <code>false</code> and the column VERSION will be set to 1. For the column CLIENT_TYPE, instead of using the
066 * {@link Binder} associated to the type of the column found in the metadata of the table, a custom binder will be used.
067 * <p>
068 * Instead of specifying values as an ordered sequence which must match the sequence of column names, some might prefer
069 * passing a map of column/value associations. This makes things more verbose, but can be more readable in some cases,
070 * when the number of columns is high. This also allows not specifying any value for columns that must stay null.
071 * The map can be constructed like any other map and passed to the builder, or it can be added using a fluent builder.
072 * The following snippet:
073 *
074 * <pre>
075 * Insert insert =
076 * Insert.into("CLIENT")
077 * .columns("CLIENT_ID", "FIRST_NAME", "LAST_NAME", "DATE_OF_BIRTH", "CLIENT_TYPE")
078 * .row().column("CLIENT_ID", 1L)
079 * .column("FIRST_NAME", "John")
080 * .column("LAST_NAME", "Doe")
081 * .column("DATE_OF_BIRTH", "1975-07-19")
082 * .end()
083 * .row().column("CLIENT_ID", 2L)
084 * .column("FIRST_NAME", "Jack")
085 * .column("LAST_NAME", "Smith")
086 * .end() // null date of birth, because it's not in the row
087 * .build();
088 * </pre>
089 *
090 * is thus equivalent to:
091 *
092 * <pre>
093 * Map<String, Object> johnDoe = new HashMap<String, Object>();
094 * johnDoe.put("CLIENT_ID", 1L);
095 * johnDoe.put("FIRST_NAME", "John");
096 * johnDoe.put("LAST_NAME", "Doe");
097 * johnDoe.put("DATE_OF_BIRTH", "1975-07-19");
098 *
099 * Map<String, Object> jackSmith = new HashMap<String, Object>();
100 * jackSmith.put("CLIENT_ID", 2L);
101 * jackSmith.put("FIRST_NAME", "Jack");
102 * jackSmith.put("LAST_NAME", "Smith");
103 *
104 * Insert insert =
105 * Insert.into("CLIENT")
106 * .columns("CLIENT_ID", "FIRST_NAME", "LAST_NAME", "DATE_OF_BIRTH", "CLIENT_TYPE")
107 * .values(johnDoe)
108 * .values(jackSmith)
109 * .build();
110 * </pre>
111 *
112 * When building the Insert using column/value associations, it might seem redundant to specify the set of column names
113 * before inserting the rows. Remember, though, that all the rows of an Insert are inserted using the same
114 * parameterized SQL query. We thus need a robust and easy way to know all the columns to insert for every row of the
115 * insert. To be able to spot errors easily and early, and to avoid complex rules, the rule is thus simple: the set of
116 * columns (excluding the generated ones) is specified either by columns(), or by the columns of the first row. All the
117 * subsequent rows may not have additional columns. And <code>null</code> is inserted for all the absent columns of the
118 * subsequent rows. The above example can thus be written as
119 *
120 * <pre>
121 * Insert insert =
122 * Insert.into("CLIENT")
123 * .row().column("CLIENT_ID", 1L)
124 * .column("FIRST_NAME", "John")
125 * .column("LAST_NAME", "Doe")
126 * .column("DATE_OF_BIRTH", "1975-07-19")
127 * .end()
128 * .row().column("CLIENT_ID", 2L)
129 * .column("FIRST_NAME", "Jack")
130 * .column("LAST_NAME", "Smith")
131 * .end() // null date of birth, because it's not in the row
132 * .build();
133 * </pre>
134 *
135 * but the following will throw an exception, because the DATE_OF_BIRTH column is not part of the first row:
136 *
137 * <pre>
138 * Insert insert =
139 * Insert.into("CLIENT")
140 * .row().column("CLIENT_ID", 2L)
141 * .column("FIRST_NAME", "Jack")
142 * .column("LAST_NAME", "Smith")
143 * .column("CLIENT_TYPE", ClientType.HIGH_PRIORITY)
144 * .end()
145 * .row().column("CLIENT_ID", 1L)
146 * .column("FIRST_NAME", "John")
147 * .column("LAST_NAME", "Doe")
148 * .column("DATE_OF_BIRTH", "1975-07-19")
149 * .column("CLIENT_TYPE", ClientType.NORMAL)
150 * .end()
151 * .build();
152 * </pre>
153 *
154 * @author JB Nizet
155 */
156 @Immutable
157 public final class Insert implements Operation {
158 private final String table;
159 private final List<String> columnNames;
160 private final Map<String, List<Object>> generatedValues;
161 private final List<List<?>> rows;
162 private final boolean metadataUsed;
163
164 private final Map<String, Binder> binders;
165
166 private Insert(Builder builder) {
167 this.table = builder.table;
168 this.columnNames = builder.columnNames;
169 this.rows = builder.rows;
170 this.generatedValues = generateValues(builder.valueGenerators, rows.size());
171 this.binders = builder.binders;
172 this.metadataUsed = builder.metadataUsed;
173 }
174
175 private Map<String, List<Object>> generateValues(Map<String, ValueGenerator<?>> valueGenerators,
176 int count) {
177 Map<String, List<Object>> result = new LinkedHashMap<String, List<Object>>();
178 for (Map.Entry<String, ValueGenerator<?>> entry : valueGenerators.entrySet()) {
179 result.put(entry.getKey(), generateValues(entry.getValue(), count));
180 }
181 return result;
182 }
183
184 private List<Object> generateValues(ValueGenerator<?> valueGenerator, int count) {
185 List<Object> result = new ArrayList<Object>(count);
186 for (int i = 0; i < count; i++) {
187 result.add(valueGenerator.nextValue());
188 }
189 return result;
190 }
191
192 /**
193 * Inserts the values and generated values in the table. Unless <code>useMetadata</code> has been set to
194 * <code>false</code>, the given configuration is used to get the appropriate binder. Nevertheless, if a binder
195 * has explicitely been associated to a given column, this binder will always be used for this column.
196 */
197 @edu.umd.cs.findbugs.annotations.SuppressWarnings(
198 value = "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING",
199 justification = "The point here is precisely to compose a SQL String from column names coming from the user")
200 @Override
201 public void execute(Connection connection, BinderConfiguration configuration) throws SQLException {
202 List<String> allColumnNames = new ArrayList<String>(columnNames);
203 allColumnNames.addAll(generatedValues.keySet());
204
205 String query = generateSqlQuery(allColumnNames);
206
207 PreparedStatement stmt = connection.prepareStatement(query);
208
209 try {
210 Map<String, Binder> usedBinders = initializeBinders(stmt, allColumnNames, configuration);
211
212 int rowIndex = 0;
213 for (List<?> row : rows) {
214 int i = 0;
215 for (Object value : row) {
216 String columnName = columnNames.get(i);
217 Binder binder = usedBinders.get(columnName);
218 binder.bind(stmt, i + 1, value);
219 i++;
220 }
221 for (Map.Entry<String, List<Object>> entry : generatedValues.entrySet()) {
222 String columnName = entry.getKey();
223 List<Object> rowValues = entry.getValue();
224 Binder binder = usedBinders.get(columnName);
225 binder.bind(stmt, i + 1, rowValues.get(rowIndex));
226 i++;
227 }
228
229 stmt.executeUpdate();
230 rowIndex++;
231 }
232 }
233 finally {
234 stmt.close();
235 }
236 }
237
238 /**
239 * Gets the number of rows that are inserted in the database table when this insert operation is executed.
240 */
241 public int getRowCount() {
242 return rows.size();
243 }
244
245 private String generateSqlQuery(List<String> allColumnNames) {
246 StringBuilder sql = new StringBuilder("insert into ").append(table).append(" (");
247 for (Iterator<String> it = allColumnNames.iterator(); it.hasNext(); ) {
248 String columnName = it.next();
249 sql.append(columnName);
250 if (it.hasNext()) {
251 sql.append(", ");
252 }
253 }
254 sql.append(") values (");
255 for (Iterator<String> it = allColumnNames.iterator(); it.hasNext(); ) {
256 it.next();
257 sql.append("?");
258 if (it.hasNext()) {
259 sql.append(", ");
260 }
261 }
262 sql.append(")");
263
264 return sql.toString();
265 }
266
267 private Map<String, Binder> initializeBinders(PreparedStatement stmt,
268 List<String> allColumnNames,
269 BinderConfiguration configuration) throws SQLException {
270 Map<String, Binder> result = new HashMap<String, Binder>();
271 ParameterMetaData metadata = null;
272 if (metadataUsed) {
273 try {
274 metadata = stmt.getParameterMetaData();
275 }
276 catch (SQLException e) {
277 metadata = null;
278 // the parameter metadata are probably not supported by the database. Pass null to the configuration.
279 // The default configuration will return the default binder, just as if useMetadata(false) had been used
280 }
281 }
282 int i = 1;
283 for (String columnName : allColumnNames) {
284 Binder binder = this.binders.get(columnName);
285 if (binder == null) {
286 binder = configuration.getBinder(metadata, i);
287 if (binder == null) {
288 throw new IllegalStateException("null binder returned from configuration "
289 + configuration.getClass());
290 }
291 }
292 result.put(columnName, binder);
293 i++;
294 }
295 return result;
296 }
297
298 @Override
299 public String toString() {
300 return "insert into "
301 + table
302 + " [columns="
303 + columnNames
304 + ", generatedValues="
305 + generatedValues
306 + ", rows="
307 + rows
308 + ", metadataUsed="
309 + metadataUsed
310 + ", binders="
311 + binders
312 + "]";
313
314 }
315
316 @Override
317 public int hashCode() {
318 final int prime = 31;
319 int result = 1;
320 result = prime * result + binders.hashCode();
321 result = prime * result + columnNames.hashCode();
322 result = prime * result + generatedValues.hashCode();
323 result = prime * result + Boolean.valueOf(metadataUsed).hashCode();
324 result = prime * result + rows.hashCode();
325 result = prime * result + table.hashCode();
326 return result;
327 }
328
329 @Override
330 public boolean equals(Object obj) {
331 if (this == obj) {
332 return true;
333 }
334 if (obj == null) {
335 return false;
336 }
337 if (getClass() != obj.getClass()) {
338 return false;
339 }
340 Insert other = (Insert) obj;
341
342 return binders.equals(other.binders)
343 && columnNames.equals(other.columnNames)
344 && generatedValues.equals(other.generatedValues)
345 && metadataUsed == other.metadataUsed
346 && rows.equals(other.rows)
347 && table.equals(other.table);
348 }
349
350 /**
351 * Creates a new Builder instance, in order to build an Insert operation into the given table
352 * @param table the name of the table to insert into
353 * @return the created Builder
354 */
355 public static Builder into(@Nonnull String table) {
356 Preconditions.checkNotNull(table, "table may not be null");
357 return new Builder(table);
358 }
359
360 /**
361 * A builder used to create an Insert operation. Such a builder may only be used once. Once it has built its Insert
362 * operation, all its methods throw an {@link IllegalStateException}.
363 * @see Insert
364 * @see Insert#into(String)
365 * @author JB Nizet
366 */
367 public static final class Builder {
368 private final String table;
369 private final List<String> columnNames = new ArrayList<String>();
370 private final Map<String, ValueGenerator<?>> valueGenerators = new LinkedHashMap<String, ValueGenerator<?>>();
371 private final List<List<?>> rows = new ArrayList<List<?>>();
372
373 private boolean metadataUsed = true;
374 private final Map<String, Binder> binders = new HashMap<String, Binder>();
375
376 private boolean built;
377
378 private Builder(String table) {
379 this.table = table;
380 }
381
382 /**
383 * Specifies the list of columns into which values will be inserted. The values must the be specified, after,
384 * using the {@link #values(Object...)} method, or with the {@link #values(java.util.Map)} method, or by adding
385 * a row with named columns fluently using {@link #row()}.
386 * @param columns the names of the columns to insert into.
387 * @return this Builder instance, for chaining.
388 * @throws IllegalStateException if the Insert has already been built, or if this method has already been
389 * called, or if one of the given columns is also specified as one of the generated value columns, or if the
390 * set of columns has already been defined by adding a first row to the builder.
391 */
392 public Builder columns(@Nonnull String... columns) {
393 Preconditions.checkState(!built, "The insert has already been built");
394 Preconditions.checkState(columnNames.isEmpty(), "columns have already been specified");
395 for (String column : columns) {
396 Preconditions.checkNotNull(column, "column may not be null");
397 Preconditions.checkState(!valueGenerators.containsKey(column),
398 "column "
399 + column
400 + " has already been specified as generated value column");
401 }
402 columnNames.addAll(Arrays.asList(columns));
403 return this;
404 }
405
406 /**
407 * Adds a row of values to insert.
408 * @param values the values to insert.
409 * @return this Builder instance, for chaining.
410 * @throws IllegalStateException if the Insert has already been built, or if the number of values doesn't match
411 * the number of columns.
412 */
413 public Builder values(@Nonnull Object... values) {
414 Preconditions.checkState(!built, "The insert has already been built");
415 Preconditions.checkArgument(values.length == columnNames.size(),
416 "The number of values doesn't match the number of columns");
417 rows.add(new ArrayList<Object>(Arrays.asList(values)));
418 return this;
419 }
420
421 /**
422 * Starts building a new row with named columns to insert. If the row is the first one being added and the
423 * columns haven't been set yet by calling <code>columns()</code>, then the columns of this row constitute the
424 * column names (excluding the generated ones) of the Insert being built
425 * @return a {@link RowBuilder} instance, which, when built, will add a row to this insert builder.
426 * @throws IllegalStateException if the Insert has already been built.
427 * @see RowBuilder
428 */
429 public RowBuilder row() {
430 Preconditions.checkState(!built, "The insert has already been built");
431 return new RowBuilder(this);
432 }
433
434 /**
435 * Adds a row to this builder. If no row has been added yet and the columns haven't been set yet by calling
436 * <code>columns()</code>, then the keys of this map constitute the column names (excluding the generated ones)
437 * of the Insert being built, in the order of the keys in the map (which is arbitrary unless an ordered or
438 * sorted map is used).
439 * @param row the row to add. The keys of the map are the column names, which must match with
440 * the column names specified in the call to {@link #columns(String...)}, or with the column names of the first
441 * added row. If a column name is not present in the map, null is inserted for this column.
442 * @return this Builder instance, for chaining.
443 * @throws IllegalStateException if the Insert has already been built.
444 * @throws IllegalArgumentException if a column name of the map doesn't match with any of the column names
445 * specified with {@link #columns(String...)}
446 */
447 public Builder values(@Nonnull Map<String, ?> row) {
448 Preconditions.checkState(!built, "The insert has already been built");
449 Preconditions.checkNotNull(row, "The row may not be null");
450
451 boolean setColumns = rows.isEmpty() && columnNames.isEmpty();
452 if (setColumns) {
453 columns(row.keySet().toArray(new String[row.size()]));
454 }
455 else {
456 Set<String> rowColumnNames = new HashSet<String>(row.keySet());
457 rowColumnNames.removeAll(columnNames);
458 if (!rowColumnNames.isEmpty()) {
459 throw new IllegalArgumentException(
460 "The following columns of the row don't match with any column name: " + rowColumnNames);
461 }
462 }
463
464 List<Object> values = new ArrayList<Object>(columnNames.size());
465 for (String columnName : columnNames) {
466 values.add(row.get(columnName));
467 }
468 rows.add(values);
469 return this;
470 }
471
472 /**
473 * Associates a Binder to one or several columns.
474 * @param binder the binder to use, regardless of the metadata, for the given columns
475 * @param columns the name of the columns to associate with the given Binder
476 * @return this Builder instance, for chaining.
477 * @throws IllegalStateException if the Insert has already been built,
478 * @throws IllegalArgumentException if any of the given columns is not
479 * part of the columns or "generated value" columns.
480 */
481 public Builder withBinder(@Nonnull Binder binder, @Nonnull String... columns) {
482 Preconditions.checkState(!built, "The insert has already been built");
483 Preconditions.checkNotNull(binder, "binder may not be null");
484 for (String columnName : columns) {
485 Preconditions.checkArgument(this.columnNames.contains(columnName)
486 || this.valueGenerators.containsKey(columnName),
487 "column "
488 + columnName
489 + " is not one of the registered column names");
490 binders.put(columnName, binder);
491 }
492 return this;
493 }
494
495 /**
496 * Specifies a default value to be inserted in a column for all the rows inserted by the Insert operation.
497 * Calling this method is equivalent to calling
498 * <code>withGeneratedValue(column, ValueGenerators.constant(value))</code>
499 * @param column the name of the column
500 * @param value the default value to insert into the column
501 * @return this Builder instance, for chaining.
502 * @throws IllegalStateException if the Insert has already been built, or if the given column is part
503 * of the columns to insert.
504 */
505 public Builder withDefaultValue(@Nonnull String column, Object value) {
506 return withGeneratedValue(column, ValueGenerators.constant(value));
507 }
508
509 /**
510 * Allows the given column to be populated by a value generator, which will be called for every row of the
511 * Insert operation being built.
512 * @param column the name of the column
513 * @param valueGenerator the generator generating values for the given column of every row
514 * @return this Builder instance, for chaining.
515 * @throws IllegalStateException if the Insert has already been built, or if the given column is part
516 * of the columns to insert.
517 */
518 public Builder withGeneratedValue(@Nonnull String column, @Nonnull ValueGenerator<?> valueGenerator) {
519 Preconditions.checkState(!built, "The insert has already been built");
520 Preconditions.checkNotNull(column, "column may not be null");
521 Preconditions.checkNotNull(valueGenerator, "valueGenerator may not be null");
522 Preconditions.checkArgument(!columnNames.contains(column),
523 "column "
524 + column
525 + " is already listed in the list of column names");
526 valueGenerators.put(column, valueGenerator);
527 return this;
528 }
529
530 /**
531 * Determines if the metadata must be used to get the appropriate binder for each inserted column (except
532 * the ones which have been associated explicitely with a Binder). The default is <code>true</code>. The insert
533 * can be faster if set to <code>false</code>, but in this case, the binder used will be the one returned
534 * by the {@link BinderConfiguration} for a null metadata (which is, by default, the
535 * {@link Binders#defaultBinder() default binder}), except the ones which have been associated explicitely with
536 * a Binder.<br/>
537 * Before version 1.3.0, a SQLException was thrown if the database doesn't support parameter metadata and
538 * <code>useMetadata(false)</code> wasn't called. Since version 1.3.0, if <code>useMetadata</code> is true
539 * (the default) but the database doesn't support metadata, then the default binder configuration returns the
540 * default binder. Using this method is thus normally unnecessary as of 1.3.0.
541 * @return this Builder instance, for chaining.
542 * @throws IllegalStateException if the Insert has already been built.
543 */
544 public Builder useMetadata(boolean useMetadata) {
545 Preconditions.checkState(!built, "The insert has already been built");
546 this.metadataUsed = useMetadata;
547 return this;
548 }
549
550 /**
551 * Builds the Insert operation.
552 * @return the created Insert operation.
553 * @throws IllegalStateException if the Insert has already been built, or if no column and no generated value
554 * column has been specified.
555 */
556 public Insert build() {
557 Preconditions.checkState(!built, "The insert has already been built");
558 Preconditions.checkState(!this.columnNames.isEmpty() || !this.valueGenerators.isEmpty(),
559 "no column and no generated value column has been specified");
560 built = true;
561 return new Insert(this);
562 }
563 }
564
565 /**
566 * A row builder, constructed with {@link com.ninja_squad.dbsetup.operation.Insert.Builder#row()}. This builder
567 * allows adding a row with named columns to an Insert:
568 *
569 * <pre>
570 * Insert insert =
571 * Insert.into("CLIENT")
572 * .columns("CLIENT_ID", "FIRST_NAME", "LAST_NAME", "DATE_OF_BIRTH", "CLIENT_TYPE")
573 * .row().column("CLIENT_ID", 1L)
574 * .column("FIRST_NAME", "John")
575 * .column("LAST_NAME", "Doe")
576 * .column("DATE_OF_BIRTH", "1975-07-19")
577 * .column("CLIENT_TYPE", ClientType.NORMAL)
578 * .end()
579 * .row().column("CLIENT_ID", 2L)
580 * .column("FIRST_NAME", "Jack")
581 * .column("LAST_NAME", "Smith")
582 * .column("DATE_OF_BIRTH", "1969-08-22")
583 * .column("CLIENT_TYPE", ClientType.HIGH_PRIORITY)
584 * .end()
585 * .build();
586 * </pre>
587 *
588 * You may omit the call to <code>columns()</code>. In that case, the columns of the Insert will be the columns
589 * specified in the first added row.
590 */
591 public static final class RowBuilder {
592 private final Builder builder;
593 private final Map<String, Object> row;
594 private boolean ended;
595
596 private RowBuilder(Builder builder) {
597 this.builder = builder;
598 // note: very important to use a LinkedHashMap here, to guarantee the ordering of the columns.
599 this.row = new LinkedHashMap<String, Object>();
600 }
601
602 /**
603 * Adds a new named column to the row. If a previous value has already been added for the same column, it's
604 * replaced by this new value.
605 * @param name the name of the column, which must match with a column name defined in the Insert Builder
606 * @param value the value of the column for the constructed row
607 * @return this builder, for chaining
608 * @throws IllegalArgumentException if the given name is not the name of one of the columns to insert
609 */
610 public RowBuilder column(@Nonnull String name, Object value) {
611 Preconditions.checkState(!ended, "The row has already been ended and added to the Insert Builder");
612 if (!builder.columnNames.isEmpty()) {
613 Preconditions.checkNotNull(name, "the column name may not be null");
614 Preconditions.checkArgument(builder.columnNames.contains(name),
615 "column " + name + " is not one of the registered column names");
616 }
617 row.put(name, value);
618 return this;
619 }
620
621 /**
622 * Ends the row, adds it to the Insert Builder and returns it, for chaining.
623 * @return the Insert Builder
624 */
625 public Builder end() {
626 Preconditions.checkState(!ended, "The row has already been ended and added to the Insert Builder");
627 ended = true;
628 return builder.values(row);
629 }
630 }
631 }