embulk-output-kintone 0.4.1 → 1.0.0

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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -6
  3. data/classpath/embulk-output-kintone-1.0.0.jar +0 -0
  4. data/classpath/{shadow-kintone-java-client-0.4.1-all.jar → shadow-kintone-java-client-1.0.0-all.jar} +0 -0
  5. data/src/main/java/org/embulk/output/kintone/KintoneColumnOption.java +1 -2
  6. data/src/main/java/org/embulk/output/kintone/KintoneColumnType.java +368 -0
  7. data/src/main/java/org/embulk/output/kintone/KintoneColumnVisitor.java +195 -135
  8. data/src/main/java/org/embulk/output/kintone/KintoneMode.java +0 -1
  9. data/src/main/java/org/embulk/output/kintone/KintoneOutputPlugin.java +0 -2
  10. data/src/main/java/org/embulk/output/kintone/KintonePageOutput.java +169 -157
  11. data/src/main/java/org/embulk/output/kintone/PluginTask.java +8 -0
  12. data/src/test/java/org/embulk/output/kintone/KintoneColumnOptionBuilder.java +2 -3
  13. data/src/test/java/org/embulk/output/kintone/KintoneColumnVisitorTest.java +563 -40
  14. data/src/test/java/org/embulk/output/kintone/KintoneColumnVisitorVerifier.java +35 -14
  15. data/src/test/java/org/embulk/output/kintone/KintonePageOutputVerifier.java +2 -7
  16. data/src/test/java/org/embulk/output/kintone/TestKintoneOutputPlugin.java +22 -9
  17. data/src/test/java/org/embulk/output/kintone/TestTaskMode.java +12 -0
  18. data/src/test/resources/org/embulk/output/kintone/task/mode/config.yml +104 -0
  19. data/src/test/resources/org/embulk/output/kintone/task/mode/input.csv +7 -7
  20. data/src/test/resources/org/embulk/output/kintone/task/mode/insert_add_ignore_nulls_records.jsonl +6 -0
  21. data/src/test/resources/org/embulk/output/kintone/task/mode/insert_add_prefer_nulls_records.jsonl +6 -0
  22. data/src/test/resources/org/embulk/output/kintone/task/mode/insert_add_records.jsonl +6 -6
  23. data/src/test/resources/org/embulk/output/kintone/task/mode/update_update_ignore_nulls_records.jsonl +3 -0
  24. data/src/test/resources/org/embulk/output/kintone/task/mode/update_update_prefer_nulls_records.jsonl +3 -0
  25. data/src/test/resources/org/embulk/output/kintone/task/mode/update_update_records.jsonl +6 -3
  26. data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_add_ignore_nulls_records.jsonl +3 -0
  27. data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_add_prefer_nulls_records.jsonl +3 -0
  28. data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_add_records.jsonl +2 -2
  29. data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_update_ignore_nulls_records.jsonl +3 -0
  30. data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_update_prefer_nulls_records.jsonl +3 -0
  31. data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_update_records.jsonl +4 -4
  32. data/src/test/resources/org/embulk/output/kintone/task/mode/values_ignore_nulls.json +1 -0
  33. data/src/test/resources/org/embulk/output/kintone/task/mode/values_prefer_nulls.json +1 -0
  34. metadata +15 -4
  35. data/classpath/embulk-output-kintone-0.4.1.jar +0 -0
@@ -4,24 +4,32 @@ import static org.embulk.spi.util.RetryExecutor.retryExecutor;
4
4
 
5
5
  import com.fasterxml.jackson.databind.JsonNode;
6
6
  import com.fasterxml.jackson.databind.ObjectMapper;
7
+ import com.google.common.collect.Maps;
7
8
  import com.kintone.client.KintoneClient;
8
9
  import com.kintone.client.KintoneClientBuilder;
9
10
  import com.kintone.client.api.record.GetRecordsByCursorResponseBody;
10
11
  import com.kintone.client.exception.KintoneApiRuntimeException;
12
+ import com.kintone.client.model.app.field.FieldProperty;
11
13
  import com.kintone.client.model.record.FieldType;
12
14
  import com.kintone.client.model.record.Record;
13
15
  import com.kintone.client.model.record.RecordForUpdate;
14
16
  import com.kintone.client.model.record.UpdateKey;
15
17
  import java.io.IOException;
18
+ import java.lang.invoke.MethodHandles;
16
19
  import java.math.BigDecimal;
17
20
  import java.util.ArrayList;
18
21
  import java.util.Arrays;
19
22
  import java.util.Collections;
20
23
  import java.util.List;
24
+ import java.util.Map;
25
+ import java.util.Objects;
26
+ import java.util.TreeMap;
27
+ import java.util.function.Function;
28
+ import java.util.function.Supplier;
21
29
  import java.util.stream.Collectors;
30
+ import org.apache.commons.lang3.tuple.Pair;
22
31
  import org.embulk.config.ConfigException;
23
32
  import org.embulk.config.TaskReport;
24
- import org.embulk.spi.Column;
25
33
  import org.embulk.spi.Exec;
26
34
  import org.embulk.spi.Page;
27
35
  import org.embulk.spi.PageReader;
@@ -33,22 +41,25 @@ import org.slf4j.Logger;
33
41
  import org.slf4j.LoggerFactory;
34
42
 
35
43
  public class KintonePageOutput implements TransactionalPageOutput {
36
- public static final int UPSERT_BATCH_SIZE = 10000;
37
- public static final int CHUNK_SIZE = 100;
38
- private static final Logger LOGGER = LoggerFactory.getLogger(KintonePageOutput.class);
44
+ private static final Logger LOGGER =
45
+ LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
39
46
  private static final List<String> RETRYABLE_ERROR_CODES =
40
47
  Arrays.asList(
41
- "GAIA_TM12", // 作成できるカーソルの上限に達しているため、カーソ ルを作成できません。不要なカーソルを削除するか、しばらく経ってから再実行してください。
48
+ "GAIA_TM12", // 作成できるカーソルの上限に達しているため、カーソルを作成できません。不要なカーソルを削除するか、しばらく経ってから再実行してください。
42
49
  "GAIA_RE18", // データベースのロックに失敗したため、変更を保存できませんでした。時間をおいて再度お試しください。
43
50
  "GAIA_DA02" // データベースのロックに失敗したため、変更を保存できませんでした。時間をおいて再度お試しください。
44
51
  );
45
- private final PageReader pageReader;
52
+ private static final int UPSERT_BATCH_SIZE = 10000;
53
+ private static final int CHUNK_SIZE = 100;
54
+ private final Map<String, Pair<FieldType, FieldType>> wrongTypeFields = new TreeMap<>();
46
55
  private final PluginTask task;
56
+ private final PageReader reader;
47
57
  private KintoneClient client;
58
+ private Map<String, FieldProperty> formFields;
48
59
 
49
60
  public KintonePageOutput(PluginTask task, Schema schema) {
50
- this.pageReader = new PageReader(schema);
51
61
  this.task = task;
62
+ reader = new PageReader(schema);
52
63
  }
53
64
 
54
65
  @Override
@@ -76,11 +87,11 @@ public class KintonePageOutput implements TransactionalPageOutput {
76
87
 
77
88
  @Override
78
89
  public void close() {
79
- if (this.client == null) {
80
- return;
90
+ if (client == null) {
91
+ return; // Not connected
81
92
  }
82
93
  try {
83
- this.client.close();
94
+ client.close();
84
95
  } catch (Exception e) {
85
96
  throw new RuntimeException("kintone throw exception", e);
86
97
  }
@@ -93,78 +104,73 @@ public class KintonePageOutput implements TransactionalPageOutput {
93
104
 
94
105
  @Override
95
106
  public TaskReport commit() {
107
+ wrongTypeFields.forEach(
108
+ (key, value) ->
109
+ LOGGER.warn(
110
+ String.format(
111
+ "Field type of %s is expected %s but actual %s",
112
+ key, value.getLeft(), value.getRight())));
96
113
  return Exec.newTaskReport();
97
114
  }
98
115
 
99
- public interface Consumer<T> {
100
- void accept(T t);
101
- }
102
-
103
- public void connect(final PluginTask task) {
116
+ public void connectIfNeeded() {
117
+ if (client != null) {
118
+ return; // Already connected
119
+ }
104
120
  KintoneClientBuilder builder = KintoneClientBuilder.create("https://" + task.getDomain());
105
121
  if (task.getGuestSpaceId().isPresent()) {
106
- builder.setGuestSpaceId(task.getGuestSpaceId().orElse(-1));
122
+ builder.setGuestSpaceId(task.getGuestSpaceId().get());
107
123
  }
108
124
  if (task.getBasicAuthUsername().isPresent() && task.getBasicAuthPassword().isPresent()) {
109
125
  builder.withBasicAuth(task.getBasicAuthUsername().get(), task.getBasicAuthPassword().get());
110
126
  }
111
-
112
127
  if (task.getUsername().isPresent() && task.getPassword().isPresent()) {
113
- this.client =
114
- builder.authByPassword(task.getUsername().get(), task.getPassword().get()).build();
128
+ builder.authByPassword(task.getUsername().get(), task.getPassword().get());
115
129
  } else if (task.getToken().isPresent()) {
116
- this.client = builder.authByApiToken(task.getToken().get()).build();
130
+ builder.authByApiToken(task.getToken().get());
131
+ } else {
132
+ throw new ConfigException("Username and password or token must be configured.");
117
133
  }
134
+ client = builder.build();
135
+ formFields = client.app().getFormFields(task.getAppId());
118
136
  }
119
137
 
120
- private void update(ArrayList<RecordForUpdate> records) {
121
- execute(
122
- client -> {
123
- client.record().updateRecords(task.getAppId(), records);
124
- });
138
+ private void insert(List<Record> records) {
139
+ executeWithRetry(() -> client.record().addRecords(task.getAppId(), records));
125
140
  }
126
141
 
127
- private void insert(ArrayList<Record> records) {
128
- execute(
129
- client -> {
130
- client.record().addRecords(task.getAppId(), records);
131
- });
142
+ private void update(List<RecordForUpdate> records) {
143
+ executeWithRetry(() -> client.record().updateRecords(task.getAppId(), records));
132
144
  }
133
145
 
134
- private void execute(Consumer<KintoneClient> operation) {
135
- connect(task);
136
- if (this.client == null) {
137
- throw new RuntimeException("Failed to connect to kintone.");
138
- }
146
+ private <T> T executeWithRetry(Supplier<T> operation) {
147
+ connectIfNeeded();
139
148
  KintoneRetryOption retryOption = task.getRetryOptions();
140
149
  try {
141
- retryExecutor()
150
+ return retryExecutor()
142
151
  .withRetryLimit(retryOption.getLimit())
143
152
  .withInitialRetryWait(retryOption.getInitialWaitMillis())
144
153
  .withMaxRetryWait(retryOption.getMaxWaitMillis())
145
154
  .runInterruptible(
146
- new Retryable<Void>() {
147
-
155
+ new Retryable<T>() {
148
156
  @Override
149
- public Void call() throws Exception {
150
- operation.accept(client);
151
- return null;
157
+ public T call() throws Exception {
158
+ return operation.get();
152
159
  }
153
160
 
154
161
  @Override
155
- public boolean isRetryableException(Exception e) {
156
- if (!(e instanceof KintoneApiRuntimeException)) {
162
+ public boolean isRetryableException(Exception exception) {
163
+ if (!(exception instanceof KintoneApiRuntimeException)) {
157
164
  return false;
158
165
  }
159
-
160
166
  try {
161
167
  ObjectMapper mapper = new ObjectMapper();
162
168
  JsonNode content =
163
- mapper.readTree(((KintoneApiRuntimeException) e).getContent());
169
+ mapper.readTree(((KintoneApiRuntimeException) exception).getContent());
164
170
  String code = content.get("code").textValue();
165
171
  return RETRYABLE_ERROR_CODES.contains(code);
166
- } catch (IOException ex) {
167
- throw new RuntimeException(ex);
172
+ } catch (IOException e) {
173
+ throw new RuntimeException(e);
168
174
  }
169
175
  }
170
176
 
@@ -192,143 +198,108 @@ public class KintonePageOutput implements TransactionalPageOutput {
192
198
  }
193
199
  }
194
200
 
195
- private void insertPage(final Page page) {
196
-
197
- ArrayList<Record> records = new ArrayList<>();
198
- pageReader.setPage(page);
199
- KintoneColumnVisitor visitor = new KintoneColumnVisitor(pageReader, task.getColumnOptions());
200
- while (pageReader.nextRecord()) {
201
+ private void insertPage(Page page) {
202
+ List<Record> records = new ArrayList<>();
203
+ reader.setPage(page);
204
+ KintoneColumnVisitor visitor =
205
+ new KintoneColumnVisitor(
206
+ reader, task.getColumnOptions(), task.getPreferNulls(), task.getIgnoreNulls());
207
+ while (reader.nextRecord()) {
201
208
  Record record = new Record();
202
209
  visitor.setRecord(record);
203
- for (Column column : pageReader.getSchema().getColumns()) {
204
- column.visit(visitor);
205
- }
206
-
210
+ reader.getSchema().visitColumns(visitor);
211
+ putWrongTypeFields(record);
207
212
  records.add(record);
208
213
  if (records.size() == CHUNK_SIZE) {
209
214
  insert(records);
210
215
  records.clear();
211
216
  }
212
217
  }
213
- if (records.size() > 0) {
218
+ if (!records.isEmpty()) {
214
219
  insert(records);
215
220
  }
216
221
  }
217
222
 
218
- private void updatePage(final Page page) {
219
- ArrayList<RecordForUpdate> updateRecords = new ArrayList<>();
220
- pageReader.setPage(page);
221
-
223
+ private void updatePage(Page page) {
224
+ List<RecordForUpdate> records = new ArrayList<>();
225
+ reader.setPage(page);
222
226
  KintoneColumnVisitor visitor =
223
227
  new KintoneColumnVisitor(
224
- pageReader,
228
+ reader,
225
229
  task.getColumnOptions(),
230
+ task.getPreferNulls(),
231
+ task.getIgnoreNulls(),
226
232
  task.getUpdateKeyName()
227
233
  .orElseThrow(() -> new RuntimeException("unreachable"))); // Already validated
228
- while (pageReader.nextRecord()) {
234
+ while (reader.nextRecord()) {
229
235
  Record record = new Record();
230
236
  UpdateKey updateKey = new UpdateKey();
231
237
  visitor.setRecord(record);
232
238
  visitor.setUpdateKey(updateKey);
233
- for (Column column : pageReader.getSchema().getColumns()) {
234
- column.visit(visitor);
235
- }
236
-
237
- if (updateKey.getValue() == "") {
239
+ reader.getSchema().visitColumns(visitor);
240
+ putWrongTypeFields(record);
241
+ if (updateKey.getValue() == null || updateKey.getValue().toString().isEmpty()) {
242
+ LOGGER.warn("Record skipped because no update key value was specified");
238
243
  continue;
239
244
  }
240
-
241
- record.removeField(updateKey.getField());
242
- updateRecords.add(new RecordForUpdate(updateKey, record));
243
- if (updateRecords.size() == CHUNK_SIZE) {
244
- update(updateRecords);
245
- updateRecords.clear();
245
+ records.add(new RecordForUpdate(updateKey, record.removeField(updateKey.getField())));
246
+ if (records.size() == CHUNK_SIZE) {
247
+ update(records);
248
+ records.clear();
246
249
  }
247
250
  }
248
- if (updateRecords.size() > 0) {
249
- update(updateRecords);
251
+ if (!records.isEmpty()) {
252
+ update(records);
250
253
  }
251
254
  }
252
255
 
253
- private void upsertPage(final Page page) {
254
- execute(
255
- client -> {
256
- ArrayList<Record> records = new ArrayList<>();
257
- ArrayList<UpdateKey> updateKeys = new ArrayList<>();
258
- pageReader.setPage(page);
259
-
260
- KintoneColumnVisitor visitor =
261
- new KintoneColumnVisitor(
262
- pageReader,
263
- task.getColumnOptions(),
264
- task.getUpdateKeyName()
265
- .orElseThrow(() -> new RuntimeException("unreachable"))); // Already validated
266
- while (pageReader.nextRecord()) {
267
- Record record = new Record();
268
- UpdateKey updateKey = new UpdateKey();
269
- visitor.setRecord(record);
270
- visitor.setUpdateKey(updateKey);
271
- for (Column column : pageReader.getSchema().getColumns()) {
272
- column.visit(visitor);
273
- }
274
- records.add(record);
275
- updateKeys.add(updateKey);
276
-
277
- if (records.size() == UPSERT_BATCH_SIZE) {
278
- upsert(records, updateKeys);
279
- records.clear();
280
- updateKeys.clear();
281
- }
282
- }
283
- if (records.size() > 0) {
284
- upsert(records, updateKeys);
285
- }
286
- });
256
+ private void upsertPage(Page page) {
257
+ List<Record> records = new ArrayList<>();
258
+ List<UpdateKey> updateKeys = new ArrayList<>();
259
+ reader.setPage(page);
260
+ KintoneColumnVisitor visitor =
261
+ new KintoneColumnVisitor(
262
+ reader,
263
+ task.getColumnOptions(),
264
+ task.getPreferNulls(),
265
+ task.getIgnoreNulls(),
266
+ task.getUpdateKeyName()
267
+ .orElseThrow(() -> new RuntimeException("unreachable"))); // Already validated
268
+ while (reader.nextRecord()) {
269
+ Record record = new Record();
270
+ UpdateKey updateKey = new UpdateKey();
271
+ visitor.setRecord(record);
272
+ visitor.setUpdateKey(updateKey);
273
+ reader.getSchema().visitColumns(visitor);
274
+ putWrongTypeFields(record);
275
+ records.add(record);
276
+ updateKeys.add(updateKey);
277
+ if (records.size() == UPSERT_BATCH_SIZE) {
278
+ upsert(records, updateKeys);
279
+ records.clear();
280
+ updateKeys.clear();
281
+ }
282
+ }
283
+ if (!records.isEmpty()) {
284
+ upsert(records, updateKeys);
285
+ }
287
286
  }
288
287
 
289
- private void upsert(ArrayList<Record> records, ArrayList<UpdateKey> updateKeys) {
288
+ private void upsert(List<Record> records, List<UpdateKey> updateKeys) {
290
289
  if (records.size() != updateKeys.size()) {
291
290
  throw new RuntimeException("records.size() != updateKeys.size()");
292
291
  }
293
- FieldType updateKeyFieldType =
294
- client.app().getFormFields(task.getAppId()).get(updateKeys.get(0).getField()).getType();
295
- if (!Arrays.asList(FieldType.SINGLE_LINE_TEXT, FieldType.NUMBER).contains(updateKeyFieldType)) {
296
- throw new ConfigException("The update_key must be 'SINGLE_LINE_TEXT' or 'NUMBER'.");
297
- }
298
-
299
- List<Record> existingRecords = getExistingRecordsByUpdateKey(updateKeys);
300
- String updateField = updateKeys.get(0).getField();
301
- List<String> existingValues =
302
- existingRecords.stream()
303
- .map(
304
- (r) -> {
305
- switch (updateKeyFieldType) {
306
- case SINGLE_LINE_TEXT:
307
- String s = r.getSingleLineTextFieldValue(updateField);
308
- return s == null ? null : s.toString();
309
- case NUMBER:
310
- BigDecimal bd = r.getNumberFieldValue(updateField);
311
- return bd == null ? null : bd.toPlainString();
312
- default:
313
- return null;
314
- }
315
- })
316
- .filter(v -> v != null)
317
- .collect(Collectors.toList());
318
-
319
- ArrayList<Record> insertRecords = new ArrayList<>();
320
- ArrayList<RecordForUpdate> updateRecords = new ArrayList<>();
292
+ List<String> existingValues = executeWithRetry(() -> getExistingValuesByUpdateKey(updateKeys));
293
+ List<Record> insertRecords = new ArrayList<>();
294
+ List<RecordForUpdate> updateRecords = new ArrayList<>();
321
295
  for (int i = 0; i < records.size(); i++) {
322
296
  Record record = records.get(i);
323
297
  UpdateKey updateKey = updateKeys.get(i);
324
-
325
298
  if (existsRecord(existingValues, updateKey)) {
326
- record.removeField(updateKey.getField());
327
- updateRecords.add(new RecordForUpdate(updateKey, record));
299
+ updateRecords.add(new RecordForUpdate(updateKey, record.removeField(updateKey.getField())));
328
300
  } else {
329
301
  insertRecords.add(record);
330
302
  }
331
-
332
303
  if (insertRecords.size() == CHUNK_SIZE) {
333
304
  insert(insertRecords);
334
305
  insertRecords.clear();
@@ -337,25 +308,40 @@ public class KintonePageOutput implements TransactionalPageOutput {
337
308
  updateRecords.clear();
338
309
  }
339
310
  }
340
- if (insertRecords.size() > 0) {
311
+ if (!insertRecords.isEmpty()) {
341
312
  insert(insertRecords);
342
313
  }
343
- if (updateRecords.size() > 0) {
314
+ if (!updateRecords.isEmpty()) {
344
315
  update(updateRecords);
345
316
  }
346
317
  }
347
318
 
348
- private List<Record> getExistingRecordsByUpdateKey(ArrayList<UpdateKey> updateKeys) {
349
- String fieldCode = updateKeys.get(0).getField();
319
+ private List<String> getExistingValuesByUpdateKey(List<UpdateKey> updateKeys) {
320
+ String fieldCode =
321
+ updateKeys.stream()
322
+ .map(UpdateKey::getField)
323
+ .filter(Objects::nonNull)
324
+ .findFirst()
325
+ .orElse(null);
326
+ if (fieldCode == null) {
327
+ return Collections.emptyList();
328
+ }
329
+ Function<Record, String> fieldValueAsString;
330
+ FieldType fieldType = getFieldType(fieldCode);
331
+ if (fieldType == FieldType.SINGLE_LINE_TEXT) {
332
+ fieldValueAsString = record -> record.getSingleLineTextFieldValue(fieldCode);
333
+ } else if (fieldType == FieldType.NUMBER) {
334
+ fieldValueAsString = record -> toString(record.getNumberFieldValue(fieldCode));
335
+ } else {
336
+ throw new ConfigException("The update_key must be 'SINGLE_LINE_TEXT' or 'NUMBER'.");
337
+ }
350
338
  List<String> queryValues =
351
339
  updateKeys.stream()
352
- .filter(k -> k.getValue() != "")
353
- .map(k -> "\"" + k.getValue().toString() + "\"")
340
+ .filter(k -> k.getValue() != null && !k.getValue().toString().isEmpty())
341
+ .map(k -> "\"" + k.getValue() + "\"")
354
342
  .collect(Collectors.toList());
355
-
356
- List<Record> allRecords = new ArrayList<>();
357
343
  if (queryValues.isEmpty()) {
358
- return allRecords;
344
+ return Collections.emptyList();
359
345
  }
360
346
  String cursorId =
361
347
  client
@@ -364,19 +350,45 @@ public class KintonePageOutput implements TransactionalPageOutput {
364
350
  task.getAppId(),
365
351
  Collections.singletonList(fieldCode),
366
352
  fieldCode + " in (" + String.join(",", queryValues) + ")");
353
+ List<Record> allRecords = new ArrayList<>();
367
354
  while (true) {
368
355
  GetRecordsByCursorResponseBody resp = client.record().getRecordsByCursor(cursorId);
369
356
  List<Record> records = resp.getRecords();
370
357
  allRecords.addAll(records);
371
-
372
358
  if (!resp.hasNext()) {
373
359
  break;
374
360
  }
375
361
  }
376
- return allRecords;
362
+ return allRecords.stream()
363
+ .map(fieldValueAsString)
364
+ .filter(Objects::nonNull)
365
+ .collect(Collectors.toList());
366
+ }
367
+
368
+ private boolean existsRecord(List<String> existingValues, UpdateKey updateKey) {
369
+ String value = toString(updateKey.getValue());
370
+ return value != null && existingValues.stream().anyMatch(v -> v.equals(value));
371
+ }
372
+
373
+ private void putWrongTypeFields(Record record) {
374
+ record.getFieldCodes(true).stream()
375
+ .map(
376
+ fieldCode ->
377
+ Maps.immutableEntry(
378
+ fieldCode, Pair.of(record.getFieldType(fieldCode), getFieldType(fieldCode))))
379
+ .filter(entry -> entry.getValue().getLeft() != entry.getValue().getRight())
380
+ .forEach(entry -> wrongTypeFields.put(entry.getKey(), entry.getValue()));
381
+ }
382
+
383
+ private FieldType getFieldType(String fieldCode) {
384
+ connectIfNeeded();
385
+ FieldProperty field = formFields.get(fieldCode);
386
+ return field == null ? null : field.getType();
377
387
  }
378
388
 
379
- private boolean existsRecord(List<String> distValues, UpdateKey updateKey) {
380
- return distValues.stream().anyMatch(v -> v.equals(updateKey.getValue().toString()));
389
+ private static String toString(Object value) {
390
+ return value == null
391
+ ? null
392
+ : value instanceof BigDecimal ? ((BigDecimal) value).toPlainString() : value.toString();
381
393
  }
382
394
  }
@@ -41,6 +41,14 @@ public interface PluginTask extends Task {
41
41
  @ConfigDefault("{}")
42
42
  Map<String, KintoneColumnOption> getColumnOptions();
43
43
 
44
+ @Config("prefer_nulls")
45
+ @ConfigDefault("\"false\"")
46
+ boolean getPreferNulls();
47
+
48
+ @Config("ignore_nulls")
49
+ @ConfigDefault("\"false\"")
50
+ boolean getIgnoreNulls();
51
+
44
52
  @Config("mode")
45
53
  @ConfigDefault("\"insert\"")
46
54
  String getMode();
@@ -1,6 +1,5 @@
1
1
  package org.embulk.output.kintone;
2
2
 
3
- import java.util.Optional;
4
3
  import org.embulk.config.TaskSource;
5
4
 
6
5
  public class KintoneColumnOptionBuilder {
@@ -42,8 +41,8 @@ public class KintoneColumnOptionBuilder {
42
41
  }
43
42
 
44
43
  @Override
45
- public Optional<String> getTimezone() {
46
- return Optional.ofNullable(timezone);
44
+ public String getTimezone() {
45
+ return timezone;
47
46
  }
48
47
 
49
48
  @Override