embulk-output-td 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +7 -0
- data/CHANGELOG.md +4 -0
- data/README.md +1 -0
- data/build.gradle +5 -1
- data/config/checkstyle/checkstyle.xml +117 -0
- data/embulk-output-td.gemspec +1 -1
- data/gradle/check.gradle +34 -0
- data/src/main/java/com/treasuredata/api/TdApiClient.java +47 -23
- data/src/main/java/com/treasuredata/api/TdApiClientConfig.java +3 -3
- data/src/main/java/com/treasuredata/api/TdApiConstants.java +6 -2
- data/src/main/java/com/treasuredata/api/TdApiExecutionInterruptedException.java +2 -1
- data/src/main/java/com/treasuredata/api/TdApiExecutionTimeoutException.java +2 -1
- data/src/main/java/com/treasuredata/api/model/TDArrayColumnType.java +1 -1
- data/src/main/java/com/treasuredata/api/model/TDBulkImportSession.java +6 -4
- data/src/main/java/com/treasuredata/api/model/TDColumn.java +4 -2
- data/src/main/java/com/treasuredata/api/model/TDColumnTypeDeserializer.java +26 -13
- data/src/main/java/com/treasuredata/api/model/TDDatabase.java +2 -1
- data/src/main/java/com/treasuredata/api/model/TDMapColumnType.java +1 -1
- data/src/main/java/com/treasuredata/api/model/TDTablePermission.java +4 -2
- data/src/main/java/com/treasuredata/api/model/TDTableType.java +2 -1
- data/src/main/java/org/embulk/output/td/FinalizableExecutorService.java +35 -17
- data/src/main/java/org/embulk/output/td/MsgpackGZFileBuilder.java +13 -7
- data/src/main/java/org/embulk/output/td/RecordWriter.java +21 -382
- data/src/main/java/org/embulk/output/td/TdOutputPlugin.java +175 -40
- data/src/main/java/org/embulk/output/td/writer/BooleanFieldWriter.java +23 -0
- data/src/main/java/org/embulk/output/td/writer/DoubleFieldWriter.java +23 -0
- data/src/main/java/org/embulk/output/td/writer/FieldWriter.java +38 -0
- data/src/main/java/org/embulk/output/td/writer/FieldWriterSet.java +206 -0
- data/src/main/java/org/embulk/output/td/writer/LongFieldWriter.java +23 -0
- data/src/main/java/org/embulk/output/td/writer/StringFieldWriter.java +23 -0
- data/src/main/java/org/embulk/output/td/writer/TimestampFieldLongDuplicator.java +28 -0
- data/src/main/java/org/embulk/output/td/writer/TimestampLongFieldWriter.java +23 -0
- data/src/main/java/org/embulk/output/td/writer/TimestampStringFieldWriter.java +27 -0
- data/src/main/java/org/embulk/output/td/writer/UnixTimestampFieldDuplicator.java +27 -0
- data/src/main/java/org/embulk/output/td/writer/UnixTimestampLongFieldWriter.java +26 -0
- data/src/test/java/com/treasuredata/api/TestTdApiClient.java +1 -1
- data/src/test/java/org/embulk/output/td/TestRecordWriter.java +198 -0
- data/src/test/java/org/embulk/output/td/TestTdOutputPlugin.java +529 -0
- data/src/test/java/org/embulk/output/td/writer/TestFieldWriterSet.java +146 -0
- metadata +29 -14
- data/src/test/java/org/embulk/output/td/TestFieldWriter.java +0 -105
@@ -6,6 +6,7 @@ import java.util.Map;
|
|
6
6
|
import javax.validation.constraints.Min;
|
7
7
|
import javax.validation.constraints.Max;
|
8
8
|
|
9
|
+
import com.google.common.annotations.VisibleForTesting;
|
9
10
|
import com.google.common.base.Optional;
|
10
11
|
import com.google.common.base.Throwables;
|
11
12
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
@@ -26,7 +27,7 @@ import org.embulk.config.ConfigSource;
|
|
26
27
|
import org.embulk.config.ConfigException;
|
27
28
|
import org.embulk.config.Task;
|
28
29
|
import org.embulk.config.TaskSource;
|
29
|
-
import org.embulk.output.td.
|
30
|
+
import org.embulk.output.td.writer.FieldWriterSet;
|
30
31
|
import org.embulk.spi.Exec;
|
31
32
|
import org.embulk.spi.ExecSession;
|
32
33
|
import org.embulk.spi.OutputPlugin;
|
@@ -34,7 +35,6 @@ import org.embulk.spi.Schema;
|
|
34
35
|
import org.embulk.spi.TransactionalPageOutput;
|
35
36
|
import org.embulk.spi.time.Timestamp;
|
36
37
|
import org.embulk.spi.time.TimestampFormatter;
|
37
|
-
import org.joda.time.DateTimeZone;
|
38
38
|
import org.joda.time.format.DateTimeFormat;
|
39
39
|
import org.slf4j.Logger;
|
40
40
|
|
@@ -61,7 +61,9 @@ public class TdOutputPlugin
|
|
61
61
|
|
62
62
|
// TODO connect_timeout, read_timeout, send_timeout
|
63
63
|
|
64
|
-
|
64
|
+
@Config("mode")
|
65
|
+
@ConfigDefault("\"append\"")
|
66
|
+
public Mode getMode();
|
65
67
|
|
66
68
|
@Config("auto_create_table")
|
67
69
|
@ConfigDefault("true")
|
@@ -73,6 +75,9 @@ public class TdOutputPlugin
|
|
73
75
|
@Config("table")
|
74
76
|
public String getTable();
|
75
77
|
|
78
|
+
public void setLoadTargetTableName(String name);
|
79
|
+
public String getLoadTargetTableName();
|
80
|
+
|
76
81
|
@Config("session")
|
77
82
|
@ConfigDefault("null")
|
78
83
|
public Optional<String> getSession();
|
@@ -130,6 +135,37 @@ public class TdOutputPlugin
|
|
130
135
|
extends Task, TimestampFormatter.TimestampColumnOption
|
131
136
|
{}
|
132
137
|
|
138
|
+
public enum Mode
|
139
|
+
{
|
140
|
+
APPEND, REPLACE;
|
141
|
+
|
142
|
+
@JsonCreator
|
143
|
+
public static Mode fromConfig(String value)
|
144
|
+
{
|
145
|
+
switch(value) {
|
146
|
+
case "append":
|
147
|
+
return APPEND;
|
148
|
+
case "replace":
|
149
|
+
return REPLACE;
|
150
|
+
default:
|
151
|
+
throw new ConfigException(String.format("Unknown mode '%s'. Supported modes are [append, replace]", value));
|
152
|
+
}
|
153
|
+
}
|
154
|
+
|
155
|
+
@JsonValue
|
156
|
+
public String toString()
|
157
|
+
{
|
158
|
+
switch(this) {
|
159
|
+
case APPEND:
|
160
|
+
return "append";
|
161
|
+
case REPLACE:
|
162
|
+
return "replace";
|
163
|
+
default:
|
164
|
+
throw new IllegalStateException();
|
165
|
+
}
|
166
|
+
}
|
167
|
+
}
|
168
|
+
|
133
169
|
public interface HttpProxyTask
|
134
170
|
extends Task
|
135
171
|
{
|
@@ -173,7 +209,7 @@ public class TdOutputPlugin
|
|
173
209
|
case "nano": return NANO;
|
174
210
|
default:
|
175
211
|
throw new ConfigException(
|
176
|
-
String.format("Unknown unix_timestamp_unit '%s'. Supported units are sec, milli, micro, and nano"));
|
212
|
+
String.format("Unknown unix_timestamp_unit '%s'. Supported units are sec, milli, micro, and nano", s));
|
177
213
|
}
|
178
214
|
}
|
179
215
|
|
@@ -197,12 +233,8 @@ public class TdOutputPlugin
|
|
197
233
|
{
|
198
234
|
final PluginTask task = config.loadConfig(PluginTask.class);
|
199
235
|
|
200
|
-
// TODO mode check
|
201
|
-
|
202
236
|
// check column_options is valid or not
|
203
|
-
|
204
|
-
schema.lookupColumn(columnName); // throws SchemaConfigException
|
205
|
-
}
|
237
|
+
checkColumnOptions(schema, task.getColumnOptions());
|
206
238
|
|
207
239
|
// generate session name
|
208
240
|
task.setSessionName(buildBulkImportSessionName(task, Exec.session()));
|
@@ -210,11 +242,24 @@ public class TdOutputPlugin
|
|
210
242
|
try (TdApiClient client = newTdApiClient(task)) {
|
211
243
|
String databaseName = task.getDatabase();
|
212
244
|
String tableName = task.getTable();
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
245
|
+
|
246
|
+
switch (task.getMode()) {
|
247
|
+
case APPEND:
|
248
|
+
if (task.getAutoCreateTable()) {
|
249
|
+
// auto_create_table is valid only with append mode (replace mode always creates a new table)
|
250
|
+
createTableIfNotExists(client, databaseName, tableName);
|
251
|
+
}
|
252
|
+
else {
|
253
|
+
// check if the database and/or table exist or not
|
254
|
+
validateTableExists(client, databaseName, tableName);
|
255
|
+
}
|
256
|
+
task.setLoadTargetTableName(tableName);
|
257
|
+
break;
|
258
|
+
|
259
|
+
case REPLACE:
|
260
|
+
task.setLoadTargetTableName(
|
261
|
+
createTemporaryTableWithPrefix(client, databaseName, makeTablePrefix(task)));
|
262
|
+
break;
|
218
263
|
}
|
219
264
|
|
220
265
|
// validate FieldWriterSet configuration before transaction is started
|
@@ -226,20 +271,32 @@ public class TdOutputPlugin
|
|
226
271
|
|
227
272
|
public ConfigDiff resume(TaskSource taskSource,
|
228
273
|
Schema schema, int processorCount,
|
229
|
-
OutputPlugin.Control control)
|
274
|
+
OutputPlugin.Control control)
|
275
|
+
{
|
230
276
|
PluginTask task = taskSource.loadTask(PluginTask.class);
|
231
277
|
try (TdApiClient client = newTdApiClient(task)) {
|
232
278
|
return doRun(client, task, control);
|
233
279
|
}
|
234
280
|
}
|
235
281
|
|
236
|
-
|
282
|
+
@VisibleForTesting
|
283
|
+
ConfigDiff doRun(TdApiClient client, PluginTask task, OutputPlugin.Control control)
|
237
284
|
{
|
238
|
-
boolean doUpload = startBulkImportSession(client, task.getSessionName(), task.getDatabase(), task.
|
285
|
+
boolean doUpload = startBulkImportSession(client, task.getSessionName(), task.getDatabase(), task.getLoadTargetTableName());
|
239
286
|
task.setDoUpload(doUpload);
|
240
287
|
control.run(task.dump());
|
241
288
|
completeBulkImportSession(client, task.getSessionName(), 0); // TODO perform job priority
|
242
289
|
|
290
|
+
// commit
|
291
|
+
switch (task.getMode()) {
|
292
|
+
case APPEND:
|
293
|
+
// already done
|
294
|
+
break;
|
295
|
+
case REPLACE:
|
296
|
+
// rename table
|
297
|
+
renameTable(client, task.getDatabase(), task.getLoadTargetTableName(), task.getTable());
|
298
|
+
}
|
299
|
+
|
243
300
|
ConfigDiff configDiff = Exec.newConfigDiff();
|
244
301
|
configDiff.set("last_session", task.getSessionName());
|
245
302
|
return configDiff;
|
@@ -257,14 +314,29 @@ public class TdOutputPlugin
|
|
257
314
|
}
|
258
315
|
}
|
259
316
|
|
260
|
-
private
|
317
|
+
private String makeTablePrefix(PluginTask task)
|
318
|
+
{
|
319
|
+
return task.getTable() + "_" + task.getSessionName();
|
320
|
+
}
|
321
|
+
|
322
|
+
@VisibleForTesting
|
323
|
+
void checkColumnOptions(Schema schema, Map<String, TimestampColumnOption> columnOptions)
|
324
|
+
{
|
325
|
+
for (String columnName : columnOptions.keySet()) {
|
326
|
+
schema.lookupColumn(columnName); // throws SchemaConfigException
|
327
|
+
}
|
328
|
+
}
|
329
|
+
|
330
|
+
@VisibleForTesting
|
331
|
+
TdApiClient newTdApiClient(final PluginTask task)
|
261
332
|
{
|
262
333
|
Optional<HttpProxyConfig> httpProxyConfig = newHttpProxyConfig(task.getHttpProxy());
|
263
334
|
TdApiClientConfig config = new TdApiClientConfig(task.getEndpoint(), task.getUseSsl(), httpProxyConfig);
|
264
335
|
TdApiClient client = new TdApiClient(task.getApiKey(), config);
|
265
336
|
try {
|
266
337
|
client.start();
|
267
|
-
}
|
338
|
+
}
|
339
|
+
catch (IOException e) {
|
268
340
|
throw Throwables.propagate(e);
|
269
341
|
}
|
270
342
|
return client;
|
@@ -276,37 +348,63 @@ public class TdOutputPlugin
|
|
276
348
|
if (task.isPresent()) {
|
277
349
|
HttpProxyTask pt = task.get();
|
278
350
|
httpProxyConfig = Optional.of(new HttpProxyConfig(pt.getHost(), pt.getPort(), pt.getUseSsl()));
|
279
|
-
}
|
351
|
+
}
|
352
|
+
else {
|
280
353
|
httpProxyConfig = Optional.absent();
|
281
354
|
}
|
282
355
|
return httpProxyConfig;
|
283
356
|
}
|
284
357
|
|
285
|
-
|
358
|
+
@VisibleForTesting
|
359
|
+
void createTableIfNotExists(TdApiClient client, String databaseName, String tableName)
|
286
360
|
{
|
287
361
|
log.debug("Creating table \"{}\".\"{}\" if not exists", databaseName, tableName);
|
288
362
|
try {
|
289
363
|
client.createTable(databaseName, tableName);
|
290
364
|
log.debug("Created table \"{}\".\"{}\"", databaseName, tableName);
|
291
|
-
}
|
365
|
+
}
|
366
|
+
catch (TdApiNotFoundException e) {
|
292
367
|
try {
|
293
368
|
client.createDatabase(databaseName);
|
294
369
|
log.debug("Created database \"{}\"", databaseName);
|
295
|
-
}
|
370
|
+
}
|
371
|
+
catch (TdApiConflictException ex) {
|
296
372
|
// ignorable error
|
297
373
|
}
|
298
374
|
try {
|
299
375
|
client.createTable(databaseName, tableName);
|
300
376
|
log.debug("Created table \"{}\".\"{}\"", databaseName, tableName);
|
301
|
-
}
|
377
|
+
}
|
378
|
+
catch (TdApiConflictException exe) {
|
302
379
|
// ignorable error
|
303
380
|
}
|
304
|
-
}
|
381
|
+
}
|
382
|
+
catch (TdApiConflictException e) {
|
305
383
|
// ignorable error
|
306
384
|
}
|
307
385
|
}
|
308
386
|
|
309
|
-
|
387
|
+
@VisibleForTesting
|
388
|
+
String createTemporaryTableWithPrefix(TdApiClient client, String databaseName, String tablePrefix)
|
389
|
+
throws TdApiConflictException
|
390
|
+
{
|
391
|
+
String tableName = tablePrefix;
|
392
|
+
while (true) {
|
393
|
+
log.debug("Creating temporal table \"{}\".\"{}\"", databaseName, tableName);
|
394
|
+
try {
|
395
|
+
client.createTable(databaseName, tableName);
|
396
|
+
log.debug("Created temporal table \"{}\".\"{}\"", databaseName, tableName);
|
397
|
+
return tableName;
|
398
|
+
}
|
399
|
+
catch (TdApiConflictException e) {
|
400
|
+
log.debug("\"{}\".\"{}\" table already exists. Renaming temporal table.", databaseName, tableName);
|
401
|
+
tableName += "_";
|
402
|
+
}
|
403
|
+
}
|
404
|
+
}
|
405
|
+
|
406
|
+
@VisibleForTesting
|
407
|
+
void validateTableExists(TdApiClient client, String databaseName, String tableName)
|
310
408
|
{
|
311
409
|
try {
|
312
410
|
for (TDTable table : client.getTables(databaseName)) {
|
@@ -315,16 +413,19 @@ public class TdOutputPlugin
|
|
315
413
|
}
|
316
414
|
}
|
317
415
|
throw new ConfigException(String.format("Table \"%s\".\"%s\" doesn't exist", databaseName, tableName));
|
318
|
-
}
|
416
|
+
}
|
417
|
+
catch (TdApiNotFoundException ex) {
|
319
418
|
throw new ConfigException(String.format("Database \"%s\" doesn't exist", databaseName), ex);
|
320
419
|
}
|
321
420
|
}
|
322
421
|
|
323
|
-
|
422
|
+
@VisibleForTesting
|
423
|
+
String buildBulkImportSessionName(PluginTask task, ExecSession exec)
|
324
424
|
{
|
325
425
|
if (task.getSession().isPresent()) {
|
326
426
|
return task.getSession().get();
|
327
|
-
}
|
427
|
+
}
|
428
|
+
else {
|
328
429
|
Timestamp time = exec.getTransactionTime(); // TODO implement Exec.getTransactionUniqueName()
|
329
430
|
return String.format("embulk_%s_%09d",
|
330
431
|
DateTimeFormat.forPattern("yyyyMMdd_HHmmss").withZoneUTC().print(time.getEpochSecond() * 1000),
|
@@ -333,14 +434,16 @@ public class TdOutputPlugin
|
|
333
434
|
}
|
334
435
|
|
335
436
|
// return false if all files are already uploaded
|
336
|
-
|
437
|
+
@VisibleForTesting
|
438
|
+
boolean startBulkImportSession(TdApiClient client,
|
337
439
|
String sessionName, String databaseName, String tableName)
|
338
440
|
{
|
339
441
|
log.info("Create bulk_import session {}", sessionName);
|
340
442
|
TDBulkImportSession session;
|
341
443
|
try {
|
342
444
|
client.createBulkImportSession(sessionName, databaseName, tableName);
|
343
|
-
}
|
445
|
+
}
|
446
|
+
catch (TdApiConflictException ex) {
|
344
447
|
// ignorable error
|
345
448
|
}
|
346
449
|
session = client.getBulkImportSession(sessionName);
|
@@ -366,7 +469,8 @@ public class TdOutputPlugin
|
|
366
469
|
}
|
367
470
|
}
|
368
471
|
|
369
|
-
|
472
|
+
@VisibleForTesting
|
473
|
+
void completeBulkImportSession(TdApiClient client, String sessionName, int priority)
|
370
474
|
{
|
371
475
|
TDBulkImportSession session = client.getBulkImportSession(sessionName);
|
372
476
|
|
@@ -376,7 +480,8 @@ public class TdOutputPlugin
|
|
376
480
|
// freeze
|
377
481
|
try {
|
378
482
|
client.freezeBulkImportSession(sessionName);
|
379
|
-
}
|
483
|
+
}
|
484
|
+
catch (TdApiConflictException e) {
|
380
485
|
// ignorable error
|
381
486
|
}
|
382
487
|
}
|
@@ -417,7 +522,8 @@ public class TdOutputPlugin
|
|
417
522
|
}
|
418
523
|
}
|
419
524
|
|
420
|
-
|
525
|
+
@VisibleForTesting
|
526
|
+
TDBulkImportSession waitForStatusChange(TdApiClient client, String sessionName,
|
421
527
|
ImportStatus current, ImportStatus expecting, String operation)
|
422
528
|
{
|
423
529
|
TDBulkImportSession importSession;
|
@@ -427,21 +533,47 @@ public class TdOutputPlugin
|
|
427
533
|
if (importSession.is(expecting)) {
|
428
534
|
return importSession;
|
429
535
|
|
430
|
-
}
|
536
|
+
}
|
537
|
+
else if (importSession.is(current)) {
|
431
538
|
// in progress
|
432
539
|
|
433
|
-
}
|
540
|
+
}
|
541
|
+
else {
|
434
542
|
throw new RuntimeException(String.format("Failed to %s bulk import session '%s'",
|
435
543
|
operation, sessionName));
|
436
544
|
}
|
437
545
|
|
438
546
|
try {
|
439
547
|
Thread.sleep(3000);
|
440
|
-
}
|
548
|
+
}
|
549
|
+
catch (InterruptedException e) {
|
441
550
|
}
|
442
551
|
}
|
443
552
|
}
|
444
553
|
|
554
|
+
@VisibleForTesting
|
555
|
+
void renameTable(TdApiClient client, String databaseName, String oldName, String newName)
|
556
|
+
{
|
557
|
+
log.debug("Renaming table \"{}\".\"{}\" to \"{}\"", databaseName, oldName, newName);
|
558
|
+
try {
|
559
|
+
client.renameTable(databaseName, oldName, newName);
|
560
|
+
}
|
561
|
+
catch (TdApiConflictException e) {
|
562
|
+
try {
|
563
|
+
client.deleteTable(databaseName, newName);
|
564
|
+
log.debug("Deleted original table \"{}\".\"{}\"", databaseName, newName);
|
565
|
+
}
|
566
|
+
catch (TdApiNotFoundException ex) {
|
567
|
+
// ignoreable error
|
568
|
+
}
|
569
|
+
catch (IOException ex) {
|
570
|
+
throw Throwables.propagate(ex);
|
571
|
+
}
|
572
|
+
|
573
|
+
client.renameTable(databaseName, oldName, newName);
|
574
|
+
}
|
575
|
+
}
|
576
|
+
|
445
577
|
@Override
|
446
578
|
public TransactionalPageOutput open(TaskSource taskSource, Schema schema, int taskIndex)
|
447
579
|
{
|
@@ -450,14 +582,17 @@ public class TdOutputPlugin
|
|
450
582
|
RecordWriter closeLater = null;
|
451
583
|
try {
|
452
584
|
FieldWriterSet fieldWriters = new FieldWriterSet(log, task, schema);
|
453
|
-
|
585
|
+
closeLater = new RecordWriter(task, taskIndex, newTdApiClient(task), fieldWriters);
|
586
|
+
RecordWriter recordWriter = closeLater;
|
454
587
|
recordWriter.open(schema);
|
455
588
|
closeLater = null;
|
456
589
|
return recordWriter;
|
457
590
|
|
458
|
-
}
|
591
|
+
}
|
592
|
+
catch (IOException e) {
|
459
593
|
throw Throwables.propagate(e);
|
460
|
-
}
|
594
|
+
}
|
595
|
+
finally {
|
461
596
|
if (closeLater != null) {
|
462
597
|
closeLater.close();
|
463
598
|
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
package org.embulk.output.td.writer;
|
2
|
+
|
3
|
+
import org.embulk.output.td.MsgpackGZFileBuilder;
|
4
|
+
import org.embulk.spi.Column;
|
5
|
+
import org.embulk.spi.PageReader;
|
6
|
+
|
7
|
+
import java.io.IOException;
|
8
|
+
|
9
|
+
public class BooleanFieldWriter
|
10
|
+
extends FieldWriter
|
11
|
+
{
|
12
|
+
public BooleanFieldWriter(String keyName)
|
13
|
+
{
|
14
|
+
super(keyName);
|
15
|
+
}
|
16
|
+
|
17
|
+
@Override
|
18
|
+
public void writeValue(MsgpackGZFileBuilder builder, PageReader reader, Column column)
|
19
|
+
throws IOException
|
20
|
+
{
|
21
|
+
builder.writeBoolean(reader.getBoolean(column));
|
22
|
+
}
|
23
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
package org.embulk.output.td.writer;
|
2
|
+
|
3
|
+
import org.embulk.output.td.MsgpackGZFileBuilder;
|
4
|
+
import org.embulk.spi.Column;
|
5
|
+
import org.embulk.spi.PageReader;
|
6
|
+
|
7
|
+
import java.io.IOException;
|
8
|
+
|
9
|
+
public class DoubleFieldWriter
|
10
|
+
extends FieldWriter
|
11
|
+
{
|
12
|
+
public DoubleFieldWriter(String keyName)
|
13
|
+
{
|
14
|
+
super(keyName);
|
15
|
+
}
|
16
|
+
|
17
|
+
@Override
|
18
|
+
public void writeValue(MsgpackGZFileBuilder builder, PageReader reader, Column column)
|
19
|
+
throws IOException
|
20
|
+
{
|
21
|
+
builder.writeDouble(reader.getDouble(column));
|
22
|
+
}
|
23
|
+
}
|