embulk-output-kintone 0.4.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +63 -5
  3. data/build.gradle +1 -0
  4. data/classpath/commons-csv-1.9.0.jar +0 -0
  5. data/classpath/embulk-output-kintone-1.1.0.jar +0 -0
  6. data/classpath/externalsortinginjava-0.6.2.jar +0 -0
  7. data/classpath/{shadow-kintone-java-client-0.4.1-all.jar → shadow-kintone-java-client-1.1.0-all.jar} +0 -0
  8. data/src/main/java/org/embulk/output/kintone/KintoneColumnOption.java +6 -2
  9. data/src/main/java/org/embulk/output/kintone/KintoneColumnType.java +572 -0
  10. data/src/main/java/org/embulk/output/kintone/KintoneColumnVisitor.java +214 -135
  11. data/src/main/java/org/embulk/output/kintone/KintoneMode.java +0 -1
  12. data/src/main/java/org/embulk/output/kintone/KintoneOutputPlugin.java +12 -5
  13. data/src/main/java/org/embulk/output/kintone/KintonePageOutput.java +180 -160
  14. data/src/main/java/org/embulk/output/kintone/KintoneSortColumn.java +33 -0
  15. data/src/main/java/org/embulk/output/kintone/PluginTask.java +35 -0
  16. data/src/main/java/org/embulk/output/kintone/deserializer/DeserializeApplier.java +19 -0
  17. data/src/main/java/org/embulk/output/kintone/deserializer/DeserializeException.java +7 -0
  18. data/src/main/java/org/embulk/output/kintone/deserializer/Deserializer.java +279 -0
  19. data/src/main/java/org/embulk/output/kintone/reducer/CSVInputColumnVisitor.java +78 -0
  20. data/src/main/java/org/embulk/output/kintone/reducer/CSVOutputColumnVisitor.java +79 -0
  21. data/src/main/java/org/embulk/output/kintone/reducer/ReduceException.java +11 -0
  22. data/src/main/java/org/embulk/output/kintone/reducer/ReduceType.java +190 -0
  23. data/src/main/java/org/embulk/output/kintone/reducer/ReducedPageOutput.java +100 -0
  24. data/src/main/java/org/embulk/output/kintone/reducer/Reducer.java +355 -0
  25. data/src/test/java/org/embulk/output/kintone/KintoneColumnOptionBuilder.java +9 -3
  26. data/src/test/java/org/embulk/output/kintone/KintoneColumnTypeTest.java +194 -0
  27. data/src/test/java/org/embulk/output/kintone/KintoneColumnVisitorTest.java +703 -61
  28. data/src/test/java/org/embulk/output/kintone/KintoneColumnVisitorVerifier.java +45 -14
  29. data/src/test/java/org/embulk/output/kintone/KintonePageOutputVerifier.java +43 -5
  30. data/src/test/java/org/embulk/output/kintone/TestKintoneOutputPlugin.java +106 -16
  31. data/src/test/java/org/embulk/output/kintone/TestTaskMode.java +12 -0
  32. data/src/test/java/org/embulk/output/kintone/TestTaskReduce.java +46 -0
  33. data/src/test/java/org/embulk/output/kintone/TestTaskReduceException.java +50 -0
  34. data/src/test/java/org/embulk/output/kintone/TestTaskReduceSubtable.java +46 -0
  35. data/src/test/java/org/embulk/output/kintone/deserializer/DeserializerTest.java +165 -0
  36. data/src/test/java/org/embulk/output/kintone/reducer/ReduceTypeTest.java +154 -0
  37. data/src/test/resources/org/embulk/output/kintone/task/config.yml +1 -1
  38. data/src/test/resources/org/embulk/output/kintone/task/mode/config.yml +110 -0
  39. data/src/test/resources/org/embulk/output/kintone/task/mode/input.csv +7 -7
  40. data/src/test/resources/org/embulk/output/kintone/task/mode/insert_add_ignore_nulls_records.jsonl +6 -0
  41. data/src/test/resources/org/embulk/output/kintone/task/mode/insert_add_prefer_nulls_records.jsonl +6 -0
  42. data/src/test/resources/org/embulk/output/kintone/task/mode/insert_add_records.jsonl +6 -6
  43. data/src/test/resources/org/embulk/output/kintone/task/mode/update_update_ignore_nulls_records.jsonl +3 -0
  44. data/src/test/resources/org/embulk/output/kintone/task/mode/update_update_prefer_nulls_records.jsonl +3 -0
  45. data/src/test/resources/org/embulk/output/kintone/task/mode/update_update_records.jsonl +6 -3
  46. data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_add_ignore_nulls_records.jsonl +3 -0
  47. data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_add_prefer_nulls_records.jsonl +3 -0
  48. data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_add_records.jsonl +2 -2
  49. data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_update_ignore_nulls_records.jsonl +3 -0
  50. data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_update_prefer_nulls_records.jsonl +3 -0
  51. data/src/test/resources/org/embulk/output/kintone/task/mode/upsert_update_records.jsonl +4 -4
  52. data/src/test/resources/org/embulk/output/kintone/task/mode/values_ignore_nulls.json +1 -0
  53. data/src/test/resources/org/embulk/output/kintone/task/mode/values_prefer_nulls.json +1 -0
  54. data/src/test/resources/org/embulk/output/kintone/task/reduce/config.yml +171 -0
  55. data/src/test/resources/org/embulk/output/kintone/task/reduce/input.csv +7 -0
  56. data/src/test/resources/org/embulk/output/kintone/task/reduce/insert_add_ignore_nulls_records.jsonl +6 -0
  57. data/src/test/resources/org/embulk/output/kintone/task/reduce/insert_add_prefer_nulls_records.jsonl +6 -0
  58. data/src/test/resources/org/embulk/output/kintone/task/reduce/insert_add_records.jsonl +6 -0
  59. data/src/test/resources/org/embulk/output/kintone/task/reduce/update_update_ignore_nulls_records.jsonl +3 -0
  60. data/src/test/resources/org/embulk/output/kintone/task/reduce/update_update_prefer_nulls_records.jsonl +3 -0
  61. data/src/test/resources/org/embulk/output/kintone/task/reduce/update_update_records.jsonl +6 -0
  62. data/src/test/resources/org/embulk/output/kintone/task/reduce/upsert_add_ignore_nulls_records.jsonl +3 -0
  63. data/src/test/resources/org/embulk/output/kintone/task/reduce/upsert_add_prefer_nulls_records.jsonl +3 -0
  64. data/src/test/resources/org/embulk/output/kintone/task/reduce/upsert_add_records.jsonl +2 -0
  65. data/src/test/resources/org/embulk/output/kintone/task/reduce/upsert_update_ignore_nulls_records.jsonl +3 -0
  66. data/src/test/resources/org/embulk/output/kintone/task/reduce/upsert_update_prefer_nulls_records.jsonl +3 -0
  67. data/src/test/resources/org/embulk/output/kintone/task/reduce/upsert_update_records.jsonl +4 -0
  68. data/src/test/resources/org/embulk/output/kintone/task/reduce/values.json +1 -0
  69. data/src/test/resources/org/embulk/output/kintone/task/reduce/values_ignore_nulls.json +1 -0
  70. data/src/test/resources/org/embulk/output/kintone/task/reduce/values_prefer_nulls.json +1 -0
  71. data/src/test/resources/org/embulk/output/kintone/task/reduce_exception/config.yml +36 -0
  72. data/src/test/resources/org/embulk/output/kintone/task/reduce_exception/derived_columns.json +1 -0
  73. data/src/test/resources/org/embulk/output/kintone/task/reduce_exception/input.csv +13 -0
  74. data/src/test/resources/org/embulk/output/kintone/task/reduce_exception/insert_add_records.jsonl +2 -0
  75. data/src/test/resources/org/embulk/output/kintone/task/reduce_exception/update_update_records.jsonl +2 -0
  76. data/src/test/resources/org/embulk/output/kintone/task/reduce_subtable/config.yml +343 -0
  77. data/src/test/resources/org/embulk/output/kintone/task/reduce_subtable/derived_columns.json +1 -0
  78. data/src/test/resources/org/embulk/output/kintone/task/reduce_subtable/input.csv +13 -0
  79. data/src/test/resources/org/embulk/output/kintone/task/reduce_subtable/insert_add_ignore_nulls_records.jsonl +6 -0
  80. data/src/test/resources/org/embulk/output/kintone/task/reduce_subtable/insert_add_prefer_nulls_records.jsonl +6 -0
  81. data/src/test/resources/org/embulk/output/kintone/task/reduce_subtable/insert_add_records.jsonl +6 -0
  82. data/src/test/resources/org/embulk/output/kintone/task/reduce_subtable/update_update_ignore_nulls_records.jsonl +3 -0
  83. data/src/test/resources/org/embulk/output/kintone/task/reduce_subtable/update_update_prefer_nulls_records.jsonl +3 -0
  84. data/src/test/resources/org/embulk/output/kintone/task/reduce_subtable/update_update_records.jsonl +6 -0
  85. data/src/test/resources/org/embulk/output/kintone/task/reduce_subtable/upsert_add_ignore_nulls_records.jsonl +3 -0
  86. data/src/test/resources/org/embulk/output/kintone/task/reduce_subtable/upsert_add_prefer_nulls_records.jsonl +3 -0
  87. data/src/test/resources/org/embulk/output/kintone/task/reduce_subtable/upsert_add_records.jsonl +0 -0
  88. data/src/test/resources/org/embulk/output/kintone/task/reduce_subtable/upsert_update_ignore_nulls_records.jsonl +3 -0
  89. data/src/test/resources/org/embulk/output/kintone/task/reduce_subtable/upsert_update_prefer_nulls_records.jsonl +3 -0
  90. data/src/test/resources/org/embulk/output/kintone/task/reduce_subtable/upsert_update_records.jsonl +6 -0
  91. data/src/test/resources/org/embulk/output/kintone/task/reduce_subtable/values.json +1 -0
  92. data/src/test/resources/org/embulk/output/kintone/task/reduce_subtable/values_ignore_nulls.json +1 -0
  93. data/src/test/resources/org/embulk/output/kintone/task/reduce_subtable/values_prefer_nulls.json +1 -0
  94. metadata +73 -4
  95. 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,24 @@ 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 final Map<String, Pair<FieldType, FieldType>> wrongTypeFields = new TreeMap<>();
46
54
  private final PluginTask task;
55
+ private final PageReader reader;
47
56
  private KintoneClient client;
57
+ private Map<String, FieldProperty> formFields;
48
58
 
49
59
  public KintonePageOutput(PluginTask task, Schema schema) {
50
- this.pageReader = new PageReader(schema);
51
60
  this.task = task;
61
+ reader = new PageReader(schema);
52
62
  }
53
63
 
54
64
  @Override
@@ -76,11 +86,11 @@ public class KintonePageOutput implements TransactionalPageOutput {
76
86
 
77
87
  @Override
78
88
  public void close() {
79
- if (this.client == null) {
80
- return;
89
+ if (client == null) {
90
+ return; // Not connected
81
91
  }
82
92
  try {
83
- this.client.close();
93
+ client.close();
84
94
  } catch (Exception e) {
85
95
  throw new RuntimeException("kintone throw exception", e);
86
96
  }
@@ -93,78 +103,73 @@ public class KintonePageOutput implements TransactionalPageOutput {
93
103
 
94
104
  @Override
95
105
  public TaskReport commit() {
106
+ wrongTypeFields.forEach(
107
+ (key, value) ->
108
+ LOGGER.warn(
109
+ String.format(
110
+ "Field type of %s is expected %s but actual %s",
111
+ key, value.getLeft(), value.getRight())));
96
112
  return Exec.newTaskReport();
97
113
  }
98
114
 
99
- public interface Consumer<T> {
100
- void accept(T t);
101
- }
102
-
103
- public void connect(final PluginTask task) {
115
+ private void connectIfNeeded() {
116
+ if (client != null) {
117
+ return; // Already connected
118
+ }
104
119
  KintoneClientBuilder builder = KintoneClientBuilder.create("https://" + task.getDomain());
105
120
  if (task.getGuestSpaceId().isPresent()) {
106
- builder.setGuestSpaceId(task.getGuestSpaceId().orElse(-1));
121
+ builder.setGuestSpaceId(task.getGuestSpaceId().get());
107
122
  }
108
123
  if (task.getBasicAuthUsername().isPresent() && task.getBasicAuthPassword().isPresent()) {
109
124
  builder.withBasicAuth(task.getBasicAuthUsername().get(), task.getBasicAuthPassword().get());
110
125
  }
111
-
112
126
  if (task.getUsername().isPresent() && task.getPassword().isPresent()) {
113
- this.client =
114
- builder.authByPassword(task.getUsername().get(), task.getPassword().get()).build();
127
+ builder.authByPassword(task.getUsername().get(), task.getPassword().get());
115
128
  } else if (task.getToken().isPresent()) {
116
- this.client = builder.authByApiToken(task.getToken().get()).build();
129
+ builder.authByApiToken(task.getToken().get());
130
+ } else {
131
+ throw new ConfigException("Username and password or token must be configured.");
117
132
  }
133
+ client = builder.build();
134
+ formFields = client.app().getFormFields(task.getAppId());
118
135
  }
119
136
 
120
- private void update(ArrayList<RecordForUpdate> records) {
121
- execute(
122
- client -> {
123
- client.record().updateRecords(task.getAppId(), records);
124
- });
137
+ private void insert(List<Record> records) {
138
+ executeWithRetry(() -> client.record().addRecords(task.getAppId(), records));
125
139
  }
126
140
 
127
- private void insert(ArrayList<Record> records) {
128
- execute(
129
- client -> {
130
- client.record().addRecords(task.getAppId(), records);
131
- });
141
+ private void update(List<RecordForUpdate> records) {
142
+ executeWithRetry(() -> client.record().updateRecords(task.getAppId(), records));
132
143
  }
133
144
 
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
- }
145
+ private <T> T executeWithRetry(Supplier<T> operation) {
146
+ connectIfNeeded();
139
147
  KintoneRetryOption retryOption = task.getRetryOptions();
140
148
  try {
141
- retryExecutor()
149
+ return retryExecutor()
142
150
  .withRetryLimit(retryOption.getLimit())
143
151
  .withInitialRetryWait(retryOption.getInitialWaitMillis())
144
152
  .withMaxRetryWait(retryOption.getMaxWaitMillis())
145
153
  .runInterruptible(
146
- new Retryable<Void>() {
147
-
154
+ new Retryable<T>() {
148
155
  @Override
149
- public Void call() throws Exception {
150
- operation.accept(client);
151
- return null;
156
+ public T call() throws Exception {
157
+ return operation.get();
152
158
  }
153
159
 
154
160
  @Override
155
- public boolean isRetryableException(Exception e) {
156
- if (!(e instanceof KintoneApiRuntimeException)) {
161
+ public boolean isRetryableException(Exception exception) {
162
+ if (!(exception instanceof KintoneApiRuntimeException)) {
157
163
  return false;
158
164
  }
159
-
160
165
  try {
161
166
  ObjectMapper mapper = new ObjectMapper();
162
167
  JsonNode content =
163
- mapper.readTree(((KintoneApiRuntimeException) e).getContent());
168
+ mapper.readTree(((KintoneApiRuntimeException) exception).getContent());
164
169
  String code = content.get("code").textValue();
165
170
  return RETRYABLE_ERROR_CODES.contains(code);
166
- } catch (IOException ex) {
167
- throw new RuntimeException(ex);
171
+ } catch (IOException e) {
172
+ throw new RuntimeException(e);
168
173
  }
169
174
  }
170
175
 
@@ -192,170 +197,159 @@ public class KintonePageOutput implements TransactionalPageOutput {
192
197
  }
193
198
  }
194
199
 
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()) {
200
+ private void insertPage(Page page) {
201
+ List<Record> records = new ArrayList<>();
202
+ reader.setPage(page);
203
+ KintoneColumnVisitor visitor =
204
+ new KintoneColumnVisitor(
205
+ reader,
206
+ task.getDerivedColumns(),
207
+ task.getColumnOptions(),
208
+ task.getPreferNulls(),
209
+ task.getIgnoreNulls(),
210
+ task.getReduceKeyName().orElse(null));
211
+ while (reader.nextRecord()) {
201
212
  Record record = new Record();
202
213
  visitor.setRecord(record);
203
- for (Column column : pageReader.getSchema().getColumns()) {
204
- column.visit(visitor);
205
- }
206
-
214
+ reader.getSchema().visitColumns(visitor);
215
+ putWrongTypeFields(record);
207
216
  records.add(record);
208
- if (records.size() == CHUNK_SIZE) {
217
+ if (records.size() == task.getChunkSize()) {
209
218
  insert(records);
210
219
  records.clear();
211
220
  }
212
221
  }
213
- if (records.size() > 0) {
222
+ if (!records.isEmpty()) {
214
223
  insert(records);
215
224
  }
216
225
  }
217
226
 
218
- private void updatePage(final Page page) {
219
- ArrayList<RecordForUpdate> updateRecords = new ArrayList<>();
220
- pageReader.setPage(page);
221
-
227
+ private void updatePage(Page page) {
228
+ List<RecordForUpdate> records = new ArrayList<>();
229
+ reader.setPage(page);
222
230
  KintoneColumnVisitor visitor =
223
231
  new KintoneColumnVisitor(
224
- pageReader,
232
+ reader,
233
+ task.getDerivedColumns(),
225
234
  task.getColumnOptions(),
235
+ task.getPreferNulls(),
236
+ task.getIgnoreNulls(),
237
+ task.getReduceKeyName().orElse(null),
226
238
  task.getUpdateKeyName()
227
239
  .orElseThrow(() -> new RuntimeException("unreachable"))); // Already validated
228
- while (pageReader.nextRecord()) {
240
+ while (reader.nextRecord()) {
229
241
  Record record = new Record();
230
242
  UpdateKey updateKey = new UpdateKey();
231
243
  visitor.setRecord(record);
232
244
  visitor.setUpdateKey(updateKey);
233
- for (Column column : pageReader.getSchema().getColumns()) {
234
- column.visit(visitor);
235
- }
236
-
237
- if (updateKey.getValue() == "") {
245
+ reader.getSchema().visitColumns(visitor);
246
+ putWrongTypeFields(record);
247
+ if (updateKey.getValue() == null || updateKey.getValue().toString().isEmpty()) {
248
+ LOGGER.warn("Record skipped because no update key value was specified");
238
249
  continue;
239
250
  }
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();
251
+ records.add(new RecordForUpdate(updateKey, record.removeField(updateKey.getField())));
252
+ if (records.size() == task.getChunkSize()) {
253
+ update(records);
254
+ records.clear();
246
255
  }
247
256
  }
248
- if (updateRecords.size() > 0) {
249
- update(updateRecords);
257
+ if (!records.isEmpty()) {
258
+ update(records);
250
259
  }
251
260
  }
252
261
 
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
- });
262
+ private void upsertPage(Page page) {
263
+ List<Record> records = new ArrayList<>();
264
+ List<UpdateKey> updateKeys = new ArrayList<>();
265
+ reader.setPage(page);
266
+ KintoneColumnVisitor visitor =
267
+ new KintoneColumnVisitor(
268
+ reader,
269
+ task.getDerivedColumns(),
270
+ task.getColumnOptions(),
271
+ task.getPreferNulls(),
272
+ task.getIgnoreNulls(),
273
+ task.getReduceKeyName().orElse(null),
274
+ task.getUpdateKeyName()
275
+ .orElseThrow(() -> new RuntimeException("unreachable"))); // Already validated
276
+ while (reader.nextRecord()) {
277
+ Record record = new Record();
278
+ UpdateKey updateKey = new UpdateKey();
279
+ visitor.setRecord(record);
280
+ visitor.setUpdateKey(updateKey);
281
+ reader.getSchema().visitColumns(visitor);
282
+ putWrongTypeFields(record);
283
+ records.add(record);
284
+ updateKeys.add(updateKey);
285
+ if (records.size() == UPSERT_BATCH_SIZE) {
286
+ upsert(records, updateKeys);
287
+ records.clear();
288
+ updateKeys.clear();
289
+ }
290
+ }
291
+ if (!records.isEmpty()) {
292
+ upsert(records, updateKeys);
293
+ }
287
294
  }
288
295
 
289
- private void upsert(ArrayList<Record> records, ArrayList<UpdateKey> updateKeys) {
296
+ private void upsert(List<Record> records, List<UpdateKey> updateKeys) {
290
297
  if (records.size() != updateKeys.size()) {
291
298
  throw new RuntimeException("records.size() != updateKeys.size()");
292
299
  }
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<>();
300
+ List<String> existingValues = executeWithRetry(() -> getExistingValuesByUpdateKey(updateKeys));
301
+ List<Record> insertRecords = new ArrayList<>();
302
+ List<RecordForUpdate> updateRecords = new ArrayList<>();
321
303
  for (int i = 0; i < records.size(); i++) {
322
304
  Record record = records.get(i);
323
305
  UpdateKey updateKey = updateKeys.get(i);
324
-
325
306
  if (existsRecord(existingValues, updateKey)) {
326
- record.removeField(updateKey.getField());
327
- updateRecords.add(new RecordForUpdate(updateKey, record));
307
+ updateRecords.add(new RecordForUpdate(updateKey, record.removeField(updateKey.getField())));
328
308
  } else {
329
309
  insertRecords.add(record);
330
310
  }
331
-
332
- if (insertRecords.size() == CHUNK_SIZE) {
311
+ if (insertRecords.size() == task.getChunkSize()) {
333
312
  insert(insertRecords);
334
313
  insertRecords.clear();
335
- } else if (updateRecords.size() == CHUNK_SIZE) {
314
+ } else if (updateRecords.size() == task.getChunkSize()) {
336
315
  update(updateRecords);
337
316
  updateRecords.clear();
338
317
  }
339
318
  }
340
- if (insertRecords.size() > 0) {
319
+ if (!insertRecords.isEmpty()) {
341
320
  insert(insertRecords);
342
321
  }
343
- if (updateRecords.size() > 0) {
322
+ if (!updateRecords.isEmpty()) {
344
323
  update(updateRecords);
345
324
  }
346
325
  }
347
326
 
348
- private List<Record> getExistingRecordsByUpdateKey(ArrayList<UpdateKey> updateKeys) {
349
- String fieldCode = updateKeys.get(0).getField();
327
+ private List<String> getExistingValuesByUpdateKey(List<UpdateKey> updateKeys) {
328
+ String fieldCode =
329
+ updateKeys.stream()
330
+ .map(UpdateKey::getField)
331
+ .filter(Objects::nonNull)
332
+ .findFirst()
333
+ .orElse(null);
334
+ if (fieldCode == null) {
335
+ return Collections.emptyList();
336
+ }
337
+ Function<Record, String> fieldValueAsString;
338
+ FieldType fieldType = getFieldType(fieldCode);
339
+ if (fieldType == FieldType.SINGLE_LINE_TEXT) {
340
+ fieldValueAsString = record -> record.getSingleLineTextFieldValue(fieldCode);
341
+ } else if (fieldType == FieldType.NUMBER) {
342
+ fieldValueAsString = record -> toString(record.getNumberFieldValue(fieldCode));
343
+ } else {
344
+ throw new ConfigException("The update_key must be 'SINGLE_LINE_TEXT' or 'NUMBER'.");
345
+ }
350
346
  List<String> queryValues =
351
347
  updateKeys.stream()
352
- .filter(k -> k.getValue() != "")
353
- .map(k -> "\"" + k.getValue().toString() + "\"")
348
+ .filter(k -> k.getValue() != null && !k.getValue().toString().isEmpty())
349
+ .map(k -> "\"" + k.getValue() + "\"")
354
350
  .collect(Collectors.toList());
355
-
356
- List<Record> allRecords = new ArrayList<>();
357
351
  if (queryValues.isEmpty()) {
358
- return allRecords;
352
+ return Collections.emptyList();
359
353
  }
360
354
  String cursorId =
361
355
  client
@@ -364,19 +358,45 @@ public class KintonePageOutput implements TransactionalPageOutput {
364
358
  task.getAppId(),
365
359
  Collections.singletonList(fieldCode),
366
360
  fieldCode + " in (" + String.join(",", queryValues) + ")");
361
+ List<Record> allRecords = new ArrayList<>();
367
362
  while (true) {
368
363
  GetRecordsByCursorResponseBody resp = client.record().getRecordsByCursor(cursorId);
369
364
  List<Record> records = resp.getRecords();
370
365
  allRecords.addAll(records);
371
-
372
366
  if (!resp.hasNext()) {
373
367
  break;
374
368
  }
375
369
  }
376
- return allRecords;
370
+ return allRecords.stream()
371
+ .map(fieldValueAsString)
372
+ .filter(Objects::nonNull)
373
+ .collect(Collectors.toList());
374
+ }
375
+
376
+ private void putWrongTypeFields(Record record) {
377
+ record.getFieldCodes(true).stream()
378
+ .map(
379
+ fieldCode ->
380
+ Maps.immutableEntry(
381
+ fieldCode, Pair.of(record.getFieldType(fieldCode), getFieldType(fieldCode))))
382
+ .filter(entry -> entry.getValue().getLeft() != entry.getValue().getRight())
383
+ .forEach(entry -> wrongTypeFields.put(entry.getKey(), entry.getValue()));
384
+ }
385
+
386
+ private FieldType getFieldType(String fieldCode) {
387
+ connectIfNeeded();
388
+ FieldProperty field = formFields.get(fieldCode);
389
+ return field == null ? null : field.getType();
390
+ }
391
+
392
+ private static boolean existsRecord(List<String> existingValues, UpdateKey updateKey) {
393
+ String value = toString(updateKey.getValue());
394
+ return value != null && existingValues.stream().anyMatch(v -> v.equals(value));
377
395
  }
378
396
 
379
- private boolean existsRecord(List<String> distValues, UpdateKey updateKey) {
380
- return distValues.stream().anyMatch(v -> v.equals(updateKey.getValue().toString()));
397
+ private static String toString(Object value) {
398
+ return value == null
399
+ ? null
400
+ : value instanceof BigDecimal ? ((BigDecimal) value).toPlainString() : value.toString();
381
401
  }
382
402
  }
@@ -0,0 +1,33 @@
1
+ package org.embulk.output.kintone;
2
+
3
+ import com.fasterxml.jackson.annotation.JsonCreator;
4
+ import com.fasterxml.jackson.annotation.JsonProperty;
5
+
6
+ public class KintoneSortColumn {
7
+ private final String name;
8
+ private final Order order;
9
+
10
+ @JsonCreator
11
+ public KintoneSortColumn(@JsonProperty("name") String name, @JsonProperty("order") Order order) {
12
+ this.name = name;
13
+ this.order = order;
14
+ }
15
+
16
+ public String getName() {
17
+ return name;
18
+ }
19
+
20
+ public Order getOrder() {
21
+ return order;
22
+ }
23
+
24
+ public enum Order {
25
+ ASC,
26
+ DESC;
27
+
28
+ @JsonCreator
29
+ public static Order of(String name) {
30
+ return valueOf(name.toUpperCase());
31
+ }
32
+ }
33
+ }
@@ -1,10 +1,13 @@
1
1
  package org.embulk.output.kintone;
2
2
 
3
+ import java.util.List;
3
4
  import java.util.Map;
4
5
  import java.util.Optional;
6
+ import java.util.Set;
5
7
  import org.embulk.config.Config;
6
8
  import org.embulk.config.ConfigDefault;
7
9
  import org.embulk.config.Task;
10
+ import org.embulk.spi.Column;
8
11
 
9
12
  public interface PluginTask extends Task {
10
13
  @Config("domain")
@@ -41,6 +44,14 @@ public interface PluginTask extends Task {
41
44
  @ConfigDefault("{}")
42
45
  Map<String, KintoneColumnOption> getColumnOptions();
43
46
 
47
+ @Config("prefer_nulls")
48
+ @ConfigDefault("\"false\"")
49
+ boolean getPreferNulls();
50
+
51
+ @Config("ignore_nulls")
52
+ @ConfigDefault("\"false\"")
53
+ boolean getIgnoreNulls();
54
+
44
55
  @Config("mode")
45
56
  @ConfigDefault("\"insert\"")
46
57
  String getMode();
@@ -49,7 +60,31 @@ public interface PluginTask extends Task {
49
60
  @ConfigDefault("null")
50
61
  Optional<String> getUpdateKeyName();
51
62
 
63
+ @Config("reduce_key")
64
+ @ConfigDefault("null")
65
+ Optional<String> getReduceKeyName();
66
+
67
+ @Config("sort_columns")
68
+ @ConfigDefault("[]")
69
+ List<KintoneSortColumn> getSortColumns();
70
+
71
+ @Config("max_sort_tmp_files")
72
+ @ConfigDefault("null")
73
+ Optional<Integer> getMaxSortTmpFiles();
74
+
75
+ @Config("max_sort_memory")
76
+ @ConfigDefault("null")
77
+ Optional<Long> getMaxSortMemory();
78
+
79
+ @Config("chunk_size")
80
+ @ConfigDefault("100")
81
+ Integer getChunkSize();
82
+
52
83
  @Config("retry_options")
53
84
  @ConfigDefault("{}")
54
85
  KintoneRetryOption getRetryOptions();
86
+
87
+ Set<Column> getDerivedColumns();
88
+
89
+ void setDerivedColumns(Set<Column> columns);
55
90
  }
@@ -0,0 +1,19 @@
1
+ package org.embulk.output.kintone.deserializer;
2
+
3
+ import com.fasterxml.jackson.core.JsonParser;
4
+ import com.fasterxml.jackson.databind.DeserializationContext;
5
+ import com.fasterxml.jackson.databind.JsonDeserializer;
6
+ import java.util.function.BiFunction;
7
+
8
+ public class DeserializeApplier<T> extends JsonDeserializer<T> {
9
+ private final BiFunction<JsonParser, DeserializationContext, T> deserializer;
10
+
11
+ public DeserializeApplier(BiFunction<JsonParser, DeserializationContext, T> deserializer) {
12
+ this.deserializer = deserializer;
13
+ }
14
+
15
+ @Override
16
+ public T deserialize(JsonParser parser, DeserializationContext context) {
17
+ return deserializer.apply(parser, context);
18
+ }
19
+ }
@@ -0,0 +1,7 @@
1
+ package org.embulk.output.kintone.deserializer;
2
+
3
+ public class DeserializeException extends RuntimeException {
4
+ public DeserializeException(Throwable cause) {
5
+ super(cause);
6
+ }
7
+ }