embulk-output-jdbc 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/build.gradle +2 -2
  3. data/classpath/embulk-output-jdbc-0.4.2.jar +0 -0
  4. data/lib/embulk/output/jdbc.rb +3 -3
  5. data/src/main/java/org/embulk/output/JdbcOutputPlugin.java +137 -138
  6. data/src/main/java/org/embulk/output/jdbc/AbstractJdbcOutputPlugin.java +972 -973
  7. data/src/main/java/org/embulk/output/jdbc/BatchInsert.java +52 -53
  8. data/src/main/java/org/embulk/output/jdbc/JdbcColumn.java +116 -116
  9. data/src/main/java/org/embulk/output/jdbc/JdbcColumnOption.java +34 -34
  10. data/src/main/java/org/embulk/output/jdbc/JdbcOutputConnection.java +492 -492
  11. data/src/main/java/org/embulk/output/jdbc/JdbcOutputConnector.java +8 -8
  12. data/src/main/java/org/embulk/output/jdbc/JdbcSchema.java +60 -60
  13. data/src/main/java/org/embulk/output/jdbc/JdbcUtils.java +153 -155
  14. data/src/main/java/org/embulk/output/jdbc/StandardBatchInsert.java +200 -200
  15. data/src/main/java/org/embulk/output/jdbc/ToString.java +54 -54
  16. data/src/main/java/org/embulk/output/jdbc/ToStringMap.java +34 -35
  17. data/src/main/java/org/embulk/output/jdbc/setter/BigDecimalColumnSetter.java +68 -70
  18. data/src/main/java/org/embulk/output/jdbc/setter/BooleanColumnSetter.java +53 -54
  19. data/src/main/java/org/embulk/output/jdbc/setter/ByteColumnSetter.java +75 -76
  20. data/src/main/java/org/embulk/output/jdbc/setter/ColumnSetter.java +44 -45
  21. data/src/main/java/org/embulk/output/jdbc/setter/ColumnSetterFactory.java +196 -196
  22. data/src/main/java/org/embulk/output/jdbc/setter/ColumnSetterVisitor.java +95 -96
  23. data/src/main/java/org/embulk/output/jdbc/setter/DefaultValueSetter.java +48 -48
  24. data/src/main/java/org/embulk/output/jdbc/setter/DoubleColumnSetter.java +60 -61
  25. data/src/main/java/org/embulk/output/jdbc/setter/FloatColumnSetter.java +60 -61
  26. data/src/main/java/org/embulk/output/jdbc/setter/IntColumnSetter.java +75 -76
  27. data/src/main/java/org/embulk/output/jdbc/setter/LongColumnSetter.java +71 -72
  28. data/src/main/java/org/embulk/output/jdbc/setter/NStringColumnSetter.java +58 -59
  29. data/src/main/java/org/embulk/output/jdbc/setter/NullColumnSetter.java +53 -53
  30. data/src/main/java/org/embulk/output/jdbc/setter/NullDefaultValueSetter.java +105 -105
  31. data/src/main/java/org/embulk/output/jdbc/setter/PassThroughColumnSetter.java +58 -59
  32. data/src/main/java/org/embulk/output/jdbc/setter/ShortColumnSetter.java +75 -76
  33. data/src/main/java/org/embulk/output/jdbc/setter/SkipColumnSetter.java +43 -43
  34. data/src/main/java/org/embulk/output/jdbc/setter/SqlDateColumnSetter.java +58 -59
  35. data/src/main/java/org/embulk/output/jdbc/setter/SqlTimeColumnSetter.java +58 -59
  36. data/src/main/java/org/embulk/output/jdbc/setter/SqlTimestampColumnSetter.java +58 -58
  37. data/src/main/java/org/embulk/output/jdbc/setter/StringColumnSetter.java +58 -59
  38. data/src/test/java/org/embulk/output/TestJdbcOutputPlugin.java +5 -5
  39. metadata +3 -3
  40. data/classpath/embulk-output-jdbc-0.4.1.jar +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 011dc3e66d8ccb1d049311f46ffc83a49a1863c5
4
- data.tar.gz: 1dd9e921ebdfaf9155c1d2eaedb2fbeabf77cb74
3
+ metadata.gz: 0a490e2eac0e4a99d84d946d6c36c67e6dddb087
4
+ data.tar.gz: 27e23980d6cccc521575fe9d18f50b0cfd1ea641
5
5
  SHA512:
6
- metadata.gz: 309a873c516a8e6ecdbcc1410854fd98eee6f1e999d415fc76a4c8343cbdfffdbd0e77a16d02fbb06b68da6592dbd6b4348639d92123895c0bf9fb081c4e30c8
7
- data.tar.gz: 7c0a97867da33914850106b6b938788efcca67da609e5fd9ff2bd3cfa5f608b29d3bae849d73c44b0533695cad7ce7504c16610320d99990dece155e7d296b22
6
+ metadata.gz: db82ee6612664a6eebdbccaba36437c85ecae78ab05bda0ef09e1b49500b10aff2ee247a9e9b80460404fa37ae44e7c8d538fbcc454434854f04a55d8be1f9e7
7
+ data.tar.gz: 4b659c34dfd93468553647cbbc79f40f9c13e1031f95df5f118ff81cc28a55c174ab9960e1bf108deb7b17edfc04f686711952042207002b9e2a8877d5371f09
data/build.gradle CHANGED
@@ -1,2 +1,2 @@
1
- dependencies {
2
- }
1
+ dependencies {
2
+ }
@@ -1,3 +1,3 @@
1
- Embulk::JavaPlugin.register_output(
2
- :jdbc, "org.embulk.output.JdbcOutputPlugin",
3
- File.expand_path('../../../../classpath', __FILE__))
1
+ Embulk::JavaPlugin.register_output(
2
+ :jdbc, "org.embulk.output.JdbcOutputPlugin",
3
+ File.expand_path('../../../../classpath', __FILE__))
@@ -1,138 +1,137 @@
1
- package org.embulk.output;
2
-
3
- import java.util.List;
4
- import java.util.Set;
5
- import java.util.Properties;
6
- import java.sql.Driver;
7
- import java.io.IOException;
8
- import java.sql.Connection;
9
- import java.sql.SQLException;
10
- import com.google.common.base.Optional;
11
- import com.google.common.base.Throwables;
12
- import com.google.common.collect.ImmutableSet;
13
- import org.embulk.config.Config;
14
- import org.embulk.config.ConfigDefault;
15
- import org.embulk.output.jdbc.AbstractJdbcOutputPlugin;
16
- import org.embulk.output.jdbc.BatchInsert;
17
- import org.embulk.output.jdbc.StandardBatchInsert;
18
- import org.embulk.output.jdbc.JdbcOutputConnector;
19
- import org.embulk.output.jdbc.JdbcOutputConnection;
20
-
21
- public class JdbcOutputPlugin
22
- extends AbstractJdbcOutputPlugin
23
- {
24
- public interface GenericPluginTask extends PluginTask
25
- {
26
- @Config("driver_path")
27
- @ConfigDefault("null")
28
- public Optional<String> getDriverPath();
29
-
30
- @Config("driver_class")
31
- public String getDriverClass();
32
-
33
- @Config("url")
34
- public String getUrl();
35
-
36
- @Config("user")
37
- @ConfigDefault("null")
38
- public Optional<String> getUser();
39
-
40
- @Config("password")
41
- @ConfigDefault("null")
42
- public Optional<String> getPassword();
43
-
44
- @Config("schema")
45
- @ConfigDefault("null")
46
- public Optional<String> getSchema();
47
-
48
- @Config("max_table_name_length")
49
- @ConfigDefault("30")
50
- public int getMaxTableNameLength();
51
- }
52
-
53
- @Override
54
- protected Class<? extends PluginTask> getTaskClass()
55
- {
56
- return GenericPluginTask.class;
57
- }
58
-
59
- @Override
60
- protected Features getFeatures(PluginTask task)
61
- {
62
- GenericPluginTask t = (GenericPluginTask) task;
63
- return new Features()
64
- .setMaxTableNameLength(t.getMaxTableNameLength())
65
- .setSupportedModes(ImmutableSet.of(Mode.INSERT, Mode.INSERT_DIRECT, Mode.TRUNCATE_INSERT, Mode.REPLACE));
66
- }
67
-
68
- @Override
69
- protected GenericOutputConnector getConnector(PluginTask task, boolean retryableMetadataOperation)
70
- {
71
- GenericPluginTask t = (GenericPluginTask) task;
72
-
73
- if (t.getDriverPath().isPresent()) {
74
- loadDriverJar(t.getDriverPath().get());
75
- }
76
-
77
- Properties props = new Properties();
78
-
79
- props.putAll(t.getOptions());
80
-
81
- if (t.getUser().isPresent()) {
82
- props.setProperty("user", t.getUser().get());
83
- }
84
- logger.info("Connecting to {} options {}", t.getUrl(), props);
85
- if (t.getPassword().isPresent()) {
86
- props.setProperty("password", t.getPassword().get());
87
- }
88
-
89
- return new GenericOutputConnector(t.getUrl(), props, t.getDriverClass(),
90
- t.getSchema().orNull());
91
- }
92
-
93
- private static class GenericOutputConnector
94
- implements JdbcOutputConnector
95
- {
96
- private final Driver driver;
97
- private final String url;
98
- private final Properties properties;
99
- private final String schemaName;
100
-
101
- public GenericOutputConnector(String url, Properties properties, String driverClass,
102
- String schemaName)
103
- {
104
- try {
105
- // TODO check Class.forName(driverClass) is a Driver before newInstance
106
- // for security
107
- this.driver = (Driver) Class.forName(driverClass).newInstance();
108
- } catch (Exception ex) {
109
- throw Throwables.propagate(ex);
110
- }
111
- this.url = url;
112
- this.properties = properties;
113
- this.schemaName = schemaName;
114
- }
115
-
116
- @Override
117
- public JdbcOutputConnection connect(boolean autoCommit) throws SQLException
118
- {
119
- Connection c = driver.connect(url, properties);
120
- try {
121
- c.setAutoCommit(autoCommit);
122
- JdbcOutputConnection con = new JdbcOutputConnection(c, schemaName);
123
- c = null;
124
- return con;
125
- } finally {
126
- if (c != null) {
127
- c.close();
128
- }
129
- }
130
- }
131
- }
132
-
133
- @Override
134
- protected BatchInsert newBatchInsert(PluginTask task, Optional<List<String>> mergeKeys) throws IOException, SQLException
135
- {
136
- return new StandardBatchInsert(getConnector(task, true), mergeKeys);
137
- }
138
- }
1
+ package org.embulk.output;
2
+
3
+ import java.util.List;
4
+ import java.util.Properties;
5
+ import java.sql.Driver;
6
+ import java.io.IOException;
7
+ import java.sql.Connection;
8
+ import java.sql.SQLException;
9
+ import com.google.common.base.Optional;
10
+ import com.google.common.base.Throwables;
11
+ import com.google.common.collect.ImmutableSet;
12
+ import org.embulk.config.Config;
13
+ import org.embulk.config.ConfigDefault;
14
+ import org.embulk.output.jdbc.AbstractJdbcOutputPlugin;
15
+ import org.embulk.output.jdbc.BatchInsert;
16
+ import org.embulk.output.jdbc.StandardBatchInsert;
17
+ import org.embulk.output.jdbc.JdbcOutputConnector;
18
+ import org.embulk.output.jdbc.JdbcOutputConnection;
19
+
20
+ public class JdbcOutputPlugin
21
+ extends AbstractJdbcOutputPlugin
22
+ {
23
+ public interface GenericPluginTask extends PluginTask
24
+ {
25
+ @Config("driver_path")
26
+ @ConfigDefault("null")
27
+ public Optional<String> getDriverPath();
28
+
29
+ @Config("driver_class")
30
+ public String getDriverClass();
31
+
32
+ @Config("url")
33
+ public String getUrl();
34
+
35
+ @Config("user")
36
+ @ConfigDefault("null")
37
+ public Optional<String> getUser();
38
+
39
+ @Config("password")
40
+ @ConfigDefault("null")
41
+ public Optional<String> getPassword();
42
+
43
+ @Config("schema")
44
+ @ConfigDefault("null")
45
+ public Optional<String> getSchema();
46
+
47
+ @Config("max_table_name_length")
48
+ @ConfigDefault("30")
49
+ public int getMaxTableNameLength();
50
+ }
51
+
52
+ @Override
53
+ protected Class<? extends PluginTask> getTaskClass()
54
+ {
55
+ return GenericPluginTask.class;
56
+ }
57
+
58
+ @Override
59
+ protected Features getFeatures(PluginTask task)
60
+ {
61
+ GenericPluginTask t = (GenericPluginTask) task;
62
+ return new Features()
63
+ .setMaxTableNameLength(t.getMaxTableNameLength())
64
+ .setSupportedModes(ImmutableSet.of(Mode.INSERT, Mode.INSERT_DIRECT, Mode.TRUNCATE_INSERT, Mode.REPLACE));
65
+ }
66
+
67
+ @Override
68
+ protected GenericOutputConnector getConnector(PluginTask task, boolean retryableMetadataOperation)
69
+ {
70
+ GenericPluginTask t = (GenericPluginTask) task;
71
+
72
+ if (t.getDriverPath().isPresent()) {
73
+ loadDriverJar(t.getDriverPath().get());
74
+ }
75
+
76
+ Properties props = new Properties();
77
+
78
+ props.putAll(t.getOptions());
79
+
80
+ if (t.getUser().isPresent()) {
81
+ props.setProperty("user", t.getUser().get());
82
+ }
83
+ logger.info("Connecting to {} options {}", t.getUrl(), props);
84
+ if (t.getPassword().isPresent()) {
85
+ props.setProperty("password", t.getPassword().get());
86
+ }
87
+
88
+ return new GenericOutputConnector(t.getUrl(), props, t.getDriverClass(),
89
+ t.getSchema().orNull());
90
+ }
91
+
92
+ private static class GenericOutputConnector
93
+ implements JdbcOutputConnector
94
+ {
95
+ private final Driver driver;
96
+ private final String url;
97
+ private final Properties properties;
98
+ private final String schemaName;
99
+
100
+ public GenericOutputConnector(String url, Properties properties, String driverClass,
101
+ String schemaName)
102
+ {
103
+ try {
104
+ // TODO check Class.forName(driverClass) is a Driver before newInstance
105
+ // for security
106
+ this.driver = (Driver) Class.forName(driverClass).newInstance();
107
+ } catch (Exception ex) {
108
+ throw Throwables.propagate(ex);
109
+ }
110
+ this.url = url;
111
+ this.properties = properties;
112
+ this.schemaName = schemaName;
113
+ }
114
+
115
+ @Override
116
+ public JdbcOutputConnection connect(boolean autoCommit) throws SQLException
117
+ {
118
+ Connection c = driver.connect(url, properties);
119
+ try {
120
+ c.setAutoCommit(autoCommit);
121
+ JdbcOutputConnection con = new JdbcOutputConnection(c, schemaName);
122
+ c = null;
123
+ return con;
124
+ } finally {
125
+ if (c != null) {
126
+ c.close();
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ @Override
133
+ protected BatchInsert newBatchInsert(PluginTask task, Optional<List<String>> mergeKeys) throws IOException, SQLException
134
+ {
135
+ return new StandardBatchInsert(getConnector(task, true), mergeKeys);
136
+ }
137
+ }
@@ -1,973 +1,972 @@
1
- package org.embulk.output.jdbc;
2
-
3
- import java.util.HashSet;
4
- import java.util.List;
5
- import java.util.Map;
6
- import java.util.Locale;
7
- import java.util.Properties;
8
- import java.util.Set;
9
- import java.util.concurrent.ExecutionException;
10
- import java.io.IOException;
11
- import java.nio.charset.Charset;
12
- import java.nio.file.Paths;
13
- import java.sql.Types;
14
- import java.sql.ResultSet;
15
- import java.sql.DatabaseMetaData;
16
- import java.sql.SQLException;
17
- import org.slf4j.Logger;
18
- import org.joda.time.DateTimeZone;
19
- import com.fasterxml.jackson.annotation.JsonCreator;
20
- import com.fasterxml.jackson.annotation.JsonValue;
21
- import com.fasterxml.jackson.annotation.JsonProperty;
22
- import com.google.common.base.Optional;
23
- import com.google.common.base.Function;
24
- import com.google.common.base.Supplier;
25
- import com.google.common.base.Throwables;
26
- import com.google.common.collect.Lists;
27
- import com.google.common.collect.ImmutableList;
28
- import com.google.common.collect.ImmutableSet;
29
- import org.embulk.config.CommitReport;
30
- import org.embulk.config.Config;
31
- import org.embulk.config.ConfigDefault;
32
- import org.embulk.config.ConfigDiff;
33
- import org.embulk.config.ConfigException;
34
- import org.embulk.config.ConfigSource;
35
- import org.embulk.config.Task;
36
- import org.embulk.config.TaskSource;
37
- import org.embulk.plugin.PluginClassLoader;
38
- import org.embulk.spi.Exec;
39
- import org.embulk.spi.Column;
40
- import org.embulk.spi.ColumnVisitor;
41
- import org.embulk.spi.OutputPlugin;
42
- import org.embulk.spi.Schema;
43
- import org.embulk.spi.TransactionalPageOutput;
44
- import org.embulk.spi.Page;
45
- import org.embulk.spi.PageReader;
46
- import org.embulk.spi.time.Timestamp;
47
- import org.embulk.output.jdbc.setter.ColumnSetter;
48
- import org.embulk.output.jdbc.setter.ColumnSetterFactory;
49
- import org.embulk.output.jdbc.setter.ColumnSetterVisitor;
50
- import org.embulk.spi.util.RetryExecutor.Retryable;
51
- import static org.embulk.spi.util.RetryExecutor.retryExecutor;
52
- import static org.embulk.output.jdbc.JdbcSchema.filterSkipColumns;
53
-
54
- public abstract class AbstractJdbcOutputPlugin
55
- implements OutputPlugin
56
- {
57
- private final static Set<String> loadedJarGlobs = new HashSet<String>();
58
-
59
- protected final Logger logger = Exec.getLogger(getClass());
60
-
61
- public interface PluginTask
62
- extends Task
63
- {
64
- @Config("options")
65
- @ConfigDefault("{}")
66
- public ToStringMap getOptions();
67
-
68
- @Config("table")
69
- public String getTable();
70
-
71
- @Config("mode")
72
- public Mode getMode();
73
-
74
- @Config("batch_size")
75
- @ConfigDefault("16777216")
76
- // TODO set minimum number
77
- public int getBatchSize();
78
-
79
- @Config("merge_keys")
80
- @ConfigDefault("null")
81
- public Optional<List<String>> getMergeKeys();
82
-
83
- @Config("column_options")
84
- @ConfigDefault("{}")
85
- public Map<String, JdbcColumnOption> getColumnOptions();
86
-
87
- @Config("default_timezone")
88
- @ConfigDefault("\"UTC\"")
89
- public DateTimeZone getDefaultTimeZone();
90
-
91
- public void setMergeKeys(Optional<List<String>> keys);
92
-
93
- public void setFeatures(Features features);
94
- public Features getFeatures();
95
-
96
- public JdbcSchema getTargetTableSchema();
97
- public void setTargetTableSchema(JdbcSchema schema);
98
-
99
- public Optional<List<String>> getIntermediateTables();
100
- public void setIntermediateTables(Optional<List<String>> names);
101
- }
102
-
103
- public static class Features
104
- {
105
- private int maxTableNameLength = 64;
106
- private Set<Mode> supportedModes = ImmutableSet.copyOf(Mode.values());
107
- private boolean ignoreMergeKeys = false;
108
-
109
- public Features()
110
- { }
111
-
112
- @JsonProperty
113
- public int getMaxTableNameLength()
114
- {
115
- return maxTableNameLength;
116
- }
117
-
118
- @JsonProperty
119
- public Features setMaxTableNameLength(int bytes)
120
- {
121
- this.maxTableNameLength = bytes;
122
- return this;
123
- }
124
-
125
- @JsonProperty
126
- public Set<Mode> getSupportedModes()
127
- {
128
- return supportedModes;
129
- }
130
-
131
- @JsonProperty
132
- public Features setSupportedModes(Set<Mode> modes)
133
- {
134
- this.supportedModes = modes;
135
- return this;
136
- }
137
-
138
- @JsonProperty
139
- public boolean getIgnoreMergeKeys()
140
- {
141
- return ignoreMergeKeys;
142
- }
143
-
144
- @JsonProperty
145
- public Features setIgnoreMergeKeys(boolean value)
146
- {
147
- this.ignoreMergeKeys = value;
148
- return this;
149
- }
150
- }
151
-
152
- protected void loadDriverJar(String glob)
153
- {
154
- synchronized (loadedJarGlobs) {
155
- if (!loadedJarGlobs.contains(glob)) {
156
- // TODO match glob
157
- PluginClassLoader loader = (PluginClassLoader) getClass().getClassLoader();
158
- loader.addPath(Paths.get(glob));
159
- loadedJarGlobs.add(glob);
160
- }
161
- }
162
- }
163
-
164
- // for subclasses to add @Config
165
- protected Class<? extends PluginTask> getTaskClass()
166
- {
167
- return PluginTask.class;
168
- }
169
-
170
- protected abstract Features getFeatures(PluginTask task);
171
-
172
- protected abstract JdbcOutputConnector getConnector(PluginTask task, boolean retryableMetadataOperation);
173
-
174
- protected abstract BatchInsert newBatchInsert(PluginTask task, Optional<List<String>> mergeKeys) throws IOException, SQLException;
175
-
176
- protected JdbcOutputConnection newConnection(PluginTask task, boolean retryableMetadataOperation,
177
- boolean autoCommit) throws SQLException
178
- {
179
- return getConnector(task, retryableMetadataOperation).connect(autoCommit);
180
- }
181
-
182
- public enum Mode {
183
- INSERT,
184
- INSERT_DIRECT,
185
- MERGE,
186
- MERGE_DIRECT,
187
- TRUNCATE_INSERT,
188
- REPLACE;
189
-
190
- @JsonValue
191
- @Override
192
- public String toString()
193
- {
194
- return name().toLowerCase(Locale.ENGLISH);
195
- }
196
-
197
- @JsonCreator
198
- public static Mode fromString(String value)
199
- {
200
- switch(value) {
201
- case "insert":
202
- return INSERT;
203
- case "insert_direct":
204
- return INSERT_DIRECT;
205
- case "merge":
206
- return MERGE;
207
- case "merge_direct":
208
- return MERGE_DIRECT;
209
- case "truncate_insert":
210
- return TRUNCATE_INSERT;
211
- case "replace":
212
- return REPLACE;
213
- default:
214
- throw new ConfigException(String.format("Unknown mode '%s'. Supported modes are insert, insert_direct, merge, merge_direct, truncate_insert, replace", value));
215
- }
216
- }
217
-
218
- /**
219
- * True if this mode directly modifies the target table without creating intermediate tables.
220
- */
221
- public boolean isDirectModify()
222
- {
223
- return this == INSERT_DIRECT || this == MERGE_DIRECT;
224
- }
225
-
226
- /**
227
- * True if this mode merges records on unique keys
228
- */
229
- public boolean isMerge()
230
- {
231
- return this == MERGE || this == MERGE_DIRECT;
232
- }
233
-
234
- /**
235
- * True if this mode creates intermediate table for each tasks.
236
- */
237
- public boolean tempTablePerTask()
238
- {
239
- return this == INSERT || this == MERGE || this == TRUNCATE_INSERT /*this == REPLACE_VIEW*/;
240
- }
241
-
242
- /**
243
- * True if this mode truncates the target table before committing intermediate tables
244
- */
245
- public boolean truncateBeforeCommit()
246
- {
247
- return this == TRUNCATE_INSERT;
248
- }
249
-
250
- /**
251
- * True if this mode uses MERGE statement to commit intermediate tables to the target table
252
- */
253
- public boolean commitByMerge()
254
- {
255
- return this == MERGE;
256
- }
257
-
258
- /**
259
- * True if this mode overwrites schema of the target tables
260
- */
261
- public boolean ignoreTargetTableSchema()
262
- {
263
- return this == REPLACE /*|| this == REPLACE_VIEW*/;
264
- }
265
-
266
- /**
267
- * True if this mode swaps the target tables with intermediate tables to commit
268
- */
269
- public boolean commitBySwapTable()
270
- {
271
- return this == REPLACE;
272
- }
273
- }
274
-
275
- public ConfigDiff transaction(ConfigSource config,
276
- Schema schema, int taskCount,
277
- OutputPlugin.Control control)
278
- {
279
- PluginTask task = config.loadConfig(getTaskClass());
280
- Features features = getFeatures(task);
281
- task.setFeatures(features);
282
-
283
- if (!features.getSupportedModes().contains(task.getMode())) {
284
- throw new ConfigException(String.format("This output type doesn't support '%s'. Supported modes are: %s", task.getMode(), features.getSupportedModes()));
285
- }
286
-
287
- task = begin(task, schema, taskCount);
288
- control.run(task.dump());
289
- return commit(task, schema, taskCount);
290
- }
291
-
292
- public ConfigDiff resume(TaskSource taskSource,
293
- Schema schema, int taskCount,
294
- OutputPlugin.Control control)
295
- {
296
- PluginTask task = taskSource.loadTask(getTaskClass());
297
-
298
- if (!task.getMode().tempTablePerTask()) {
299
- throw new UnsupportedOperationException("inplace mode is not resumable. You need to delete partially-loaded records from the database and restart the entire transaction.");
300
- }
301
-
302
- task = begin(task, schema, taskCount);
303
- control.run(task.dump());
304
- return commit(task, schema, taskCount);
305
- }
306
-
307
- protected String getTransactionUniqueName()
308
- {
309
- // TODO use uuid?
310
- Timestamp t = Exec.session().getTransactionTime();
311
- return String.format("%016x%08x", t.getEpochSecond(), t.getNano());
312
- }
313
-
314
- private PluginTask begin(final PluginTask task,
315
- final Schema schema, final int taskCount)
316
- {
317
- try {
318
- withRetry(new IdempotentSqlRunnable() { // no intermediate data if isDirectModify == true
319
- public void run() throws SQLException
320
- {
321
- JdbcOutputConnection con = newConnection(task, true, false);
322
- try {
323
- doBegin(con, task, schema, taskCount);
324
- } finally {
325
- con.close();
326
- }
327
- }
328
- });
329
- } catch (SQLException | InterruptedException ex) {
330
- throw new RuntimeException(ex);
331
- }
332
- return task;
333
- }
334
-
335
- private ConfigDiff commit(final PluginTask task,
336
- Schema schema, final int taskCount)
337
- {
338
- if (!task.getMode().isDirectModify()) { // no intermediate data if isDirectModify == true
339
- try {
340
- withRetry(new IdempotentSqlRunnable() {
341
- public void run() throws SQLException
342
- {
343
- JdbcOutputConnection con = newConnection(task, false, false);
344
- try {
345
- doCommit(con, task, taskCount);
346
- } finally {
347
- con.close();
348
- }
349
- }
350
- });
351
- } catch (SQLException | InterruptedException ex) {
352
- throw new RuntimeException(ex);
353
- }
354
- }
355
- return Exec.newConfigDiff();
356
- }
357
-
358
- public void cleanup(TaskSource taskSource,
359
- Schema schema, final int taskCount,
360
- final List<CommitReport> successCommitReports)
361
- {
362
- final PluginTask task = taskSource.loadTask(getTaskClass());
363
-
364
- if (!task.getMode().isDirectModify()) { // no intermediate data if isDirectModify == true
365
- try {
366
- withRetry(new IdempotentSqlRunnable() {
367
- public void run() throws SQLException
368
- {
369
- JdbcOutputConnection con = newConnection(task, true, true);
370
- try {
371
- doCleanup(con, task, taskCount, successCommitReports);
372
- } finally {
373
- con.close();
374
- }
375
- }
376
- });
377
- } catch (SQLException | InterruptedException ex) {
378
- throw new RuntimeException(ex);
379
- }
380
- }
381
- }
382
-
383
- protected void doBegin(JdbcOutputConnection con,
384
- PluginTask task, final Schema schema, int taskCount) throws SQLException
385
- {
386
- if (schema.getColumnCount() == 0) {
387
- throw new ConfigException("task count == 0 is not supported");
388
- }
389
-
390
- Mode mode = task.getMode();
391
- logger.info("Using {} mode", mode);
392
-
393
- Optional<JdbcSchema> initialTargetTableSchema =
394
- mode.ignoreTargetTableSchema() ?
395
- Optional.<JdbcSchema>absent() :
396
- newJdbcSchemaFromTableIfExists(con, task.getTable());
397
-
398
- // TODO get CREATE TABLE statement from task if set
399
- JdbcSchema newTableSchema = applyColumnOptionsToNewTableSchema(
400
- initialTargetTableSchema.or(new Supplier<JdbcSchema>() {
401
- public JdbcSchema get()
402
- {
403
- return newJdbcSchemaForNewTable(schema);
404
- }
405
- }),
406
- task.getColumnOptions());
407
-
408
- // create intermediate tables
409
- if (!mode.isDirectModify()) {
410
- // direct modify mode doesn't need intermediate tables.
411
- ImmutableList.Builder<String> intermTableNames = ImmutableList.builder();
412
- if (mode.tempTablePerTask()) {
413
- String namePrefix = generateIntermediateTableNamePrefix(task.getTable(), con, 3, task.getFeatures().getMaxTableNameLength());
414
- for (int i=0; i < taskCount; i++) {
415
- intermTableNames.add(namePrefix + String.format("%03d", i));
416
- }
417
- } else {
418
- String name = generateIntermediateTableNamePrefix(task.getTable(), con, 0, task.getFeatures().getMaxTableNameLength());
419
- intermTableNames.add(name);
420
- }
421
- // create the intermediate tables here
422
- task.setIntermediateTables(Optional.<List<String>>of(intermTableNames.build()));
423
- for (String name : task.getIntermediateTables().get()) {
424
- // DROP TABLE IF EXISTS xyz__0000000054d92dee1e452158_bulk_load_temp
425
- con.dropTableIfExists(name);
426
- // CREATE TABLE IF NOT EXISTS xyz__0000000054d92dee1e452158_bulk_load_temp
427
- con.createTableIfNotExists(name, newTableSchema);
428
- }
429
- } else {
430
- task.setIntermediateTables(Optional.<List<String>>absent());
431
- }
432
-
433
- // build JdbcSchema from a table
434
- JdbcSchema targetTableSchema;
435
- if (initialTargetTableSchema.isPresent()) {
436
- targetTableSchema = initialTargetTableSchema.get();
437
- } else if (task.getIntermediateTables().isPresent() && !task.getIntermediateTables().get().isEmpty()) {
438
- String firstItermTable = task.getIntermediateTables().get().get(0);
439
- targetTableSchema = newJdbcSchemaFromTableIfExists(con, firstItermTable).get();
440
- } else {
441
- // also create the target table if not exists
442
- // CREATE TABLE IF NOT EXISTS xyz
443
- con.createTableIfNotExists(task.getTable(), newTableSchema);
444
- targetTableSchema = newJdbcSchemaFromTableIfExists(con, task.getTable()).get();
445
- }
446
- task.setTargetTableSchema(matchSchemaByColumnNames(schema, targetTableSchema));
447
-
448
- // validate column_options
449
- newColumnSetters(
450
- new ColumnSetterFactory(null, task.getDefaultTimeZone()), // TODO create a dummy BatchInsert
451
- task.getTargetTableSchema(), schema,
452
- task.getColumnOptions());
453
-
454
- // normalize merge_key parameter for merge modes
455
- if (mode.isMerge()) {
456
- Optional<List<String>> mergeKeys = task.getMergeKeys();
457
- if (task.getFeatures().getIgnoreMergeKeys()) {
458
- if (mergeKeys.isPresent()) {
459
- throw new ConfigException("This output type does not accept 'merge_key' option.");
460
- }
461
- task.setMergeKeys(Optional.<List<String>>of(ImmutableList.<String>of()));
462
- } else if (mergeKeys.isPresent()) {
463
- if (task.getMergeKeys().get().isEmpty()) {
464
- throw new ConfigException("Empty 'merge_keys' option is invalid.");
465
- }
466
- for (String key : mergeKeys.get()) {
467
- if (!targetTableSchema.findColumn(key).isPresent()) {
468
- throw new ConfigException(String.format("Merge key '%s' does not exist in the target table.", key));
469
- }
470
- }
471
- } else {
472
- ImmutableList.Builder<String> builder = ImmutableList.builder();
473
- for (JdbcColumn column : targetTableSchema.getColumns()) {
474
- if (column.isUniqueKey()) {
475
- builder.add(column.getName());
476
- }
477
- }
478
- task.setMergeKeys(Optional.<List<String>>of(builder.build()));
479
- if (task.getMergeKeys().get().isEmpty()) {
480
- throw new ConfigException("Merging mode is used but the target table does not have primary keys. Please set merge_keys option.");
481
- }
482
- }
483
- logger.info("Using merge keys: {}", task.getMergeKeys().get());
484
- } else {
485
- task.setMergeKeys(Optional.<List<String>>absent());
486
- }
487
- }
488
-
489
- protected String generateIntermediateTableNamePrefix(String baseTableName, JdbcOutputConnection con, int suffixLength, int maxLength) throws SQLException
490
- {
491
- Charset tableNameCharset = con.getTableNameCharset();
492
- String tableName = baseTableName;
493
- String suffix = "_bl_tmp";
494
- String uniqueSuffix = getTransactionUniqueName() + suffix;
495
-
496
- // way to count length of table name varies by DBMSs (bytes or characters),
497
- // so truncate swap table name by one character.
498
- while (!checkTableNameLength(tableName + "_" + uniqueSuffix, tableNameCharset, suffixLength, maxLength)) {
499
- if (uniqueSuffix.length() > 8 + suffix.length()) {
500
- // truncate transaction unique name
501
- // (include 8 characters of the transaction name at least)
502
- uniqueSuffix = uniqueSuffix.substring(1);
503
- } else {
504
- if (tableName.isEmpty()) {
505
- throw new ConfigException("Table name is too long to generate temporary table name");
506
- }
507
- // truncate table name
508
- tableName = tableName.substring(0, tableName.length() - 1);
509
- //if (!connection.tableExists(tableName)) {
510
- // TODO this doesn't help. Rather than truncating more characters,
511
- // here needs to replace characters with random characters. But
512
- // to make the result deterministic. So, an idea is replacing
513
- // the last character to the first (second, third, ... for each loop)
514
- // of md5(original table name).
515
- //}
516
- }
517
-
518
- }
519
- return tableName + "_" + uniqueSuffix;
520
- }
521
-
522
- private static JdbcSchema applyColumnOptionsToNewTableSchema(JdbcSchema schema, final Map<String, JdbcColumnOption> columnOptions)
523
- {
524
- return new JdbcSchema(Lists.transform(schema.getColumns(), new Function<JdbcColumn, JdbcColumn>() {
525
- public JdbcColumn apply(JdbcColumn c)
526
- {
527
- JdbcColumnOption option = columnOptionOf(columnOptions, c);
528
- if (option.getType().isPresent()) {
529
- return JdbcColumn.newTypeDeclaredColumn(
530
- c.getName(), Types.OTHER, // sqlType, isNotNull, and isUniqueKey are ignored
531
- option.getType().get(), false, false);
532
- }
533
- return c;
534
- }
535
- }));
536
- }
537
-
538
- protected static List<ColumnSetter> newColumnSetters(ColumnSetterFactory factory,
539
- JdbcSchema targetTableSchema, Schema inputValueSchema,
540
- Map<String, JdbcColumnOption> columnOptions)
541
- {
542
- ImmutableList.Builder<ColumnSetter> builder = ImmutableList.builder();
543
- int schemaColumnIndex = 0;
544
- for (JdbcColumn targetColumn : targetTableSchema.getColumns()) {
545
- if (targetColumn.isSkipColumn()) {
546
- builder.add(factory.newSkipColumnSetter());
547
- } else {
548
- //String columnOptionKey = inputValueSchema.getColumn(schemaColumnIndex).getName();
549
- JdbcColumnOption option = columnOptionOf(columnOptions, targetColumn);
550
- builder.add(factory.newColumnSetter(targetColumn, option));
551
- schemaColumnIndex++;
552
- }
553
- }
554
- return builder.build();
555
- }
556
-
557
- private static JdbcColumnOption columnOptionOf(Map<String, JdbcColumnOption> columnOptions, JdbcColumn targetColumn)
558
- {
559
- return Optional.fromNullable(columnOptions.get(targetColumn.getName())).or(
560
- // default column option
561
- new Supplier<JdbcColumnOption>()
562
- {
563
- public JdbcColumnOption get()
564
- {
565
- return Exec.newConfigSource().loadConfig(JdbcColumnOption.class);
566
- }
567
- });
568
- }
569
-
570
- private boolean checkTableNameLength(String tableName, Charset tableNameCharset, int suffixLength, int maxLength)
571
- {
572
- return tableNameCharset.encode(tableName).remaining() + suffixLength <= maxLength;
573
- }
574
-
575
- protected void doCommit(JdbcOutputConnection con, PluginTask task, int taskCount)
576
- throws SQLException
577
- {
578
- if (task.getIntermediateTables().get().isEmpty()) {
579
- return;
580
- }
581
-
582
- JdbcSchema schema = filterSkipColumns(task.getTargetTableSchema());
583
-
584
- switch (task.getMode()) {
585
- case INSERT_DIRECT:
586
- case MERGE_DIRECT:
587
- // already done
588
- break;
589
-
590
- case INSERT:
591
- // aggregate insert into target
592
- con.collectInsert(task.getIntermediateTables().get(), schema, task.getTable(), false);
593
- break;
594
-
595
- case TRUNCATE_INSERT:
596
- // truncate & aggregate insert into target
597
- con.collectInsert(task.getIntermediateTables().get(), schema, task.getTable(), true);
598
- break;
599
-
600
- case MERGE:
601
- // aggregate merge into target
602
- con.collectMerge(task.getIntermediateTables().get(), schema, task.getTable(), task.getMergeKeys().get());
603
- break;
604
-
605
- case REPLACE:
606
- // swap table
607
- con.replaceTable(task.getIntermediateTables().get().get(0), schema, task.getTable());
608
- break;
609
- }
610
- }
611
-
612
- protected void doCleanup(JdbcOutputConnection con, PluginTask task, int taskCount,
613
- List<CommitReport> successCommitReports)
614
- throws SQLException
615
- {
616
- if (task.getIntermediateTables().isPresent()) {
617
- for (String intermTable : task.getIntermediateTables().get()) {
618
- con.dropTableIfExists(intermTable);
619
- }
620
- }
621
- }
622
-
623
- protected JdbcSchema newJdbcSchemaForNewTable(Schema schema)
624
- {
625
- final ImmutableList.Builder<JdbcColumn> columns = ImmutableList.builder();
626
- for (Column c : schema.getColumns()) {
627
- final String columnName = c.getName();
628
- c.visit(new ColumnVisitor() {
629
- public void booleanColumn(Column column)
630
- {
631
- columns.add(JdbcColumn.newGenericTypeColumn(
632
- columnName, Types.BOOLEAN, "BOOLEAN",
633
- 1, 0, false, false));
634
- }
635
-
636
- public void longColumn(Column column)
637
- {
638
- columns.add(JdbcColumn.newGenericTypeColumn(
639
- columnName, Types.BIGINT, "BIGINT",
640
- 22, 0, false, false));
641
- }
642
-
643
- public void doubleColumn(Column column)
644
- {
645
- columns.add(JdbcColumn.newGenericTypeColumn(
646
- columnName, Types.FLOAT, "DOUBLE PRECISION",
647
- 24, 0, false, false));
648
- }
649
-
650
- public void stringColumn(Column column)
651
- {
652
- columns.add(JdbcColumn.newGenericTypeColumn(
653
- columnName, Types.CLOB, "CLOB",
654
- 4000, 0, false, false)); // TODO size type param
655
- }
656
-
657
- public void timestampColumn(Column column)
658
- {
659
- columns.add(JdbcColumn.newGenericTypeColumn(
660
- columnName, Types.TIMESTAMP, "TIMESTAMP",
661
- 26, 0, false, false)); // size type param is from postgresql
662
- }
663
- });
664
- }
665
- return new JdbcSchema(columns.build());
666
- }
667
-
668
- public Optional<JdbcSchema> newJdbcSchemaFromTableIfExists(JdbcOutputConnection connection,
669
- String tableName) throws SQLException
670
- {
671
- if (!connection.tableExists(tableName)) {
672
- // DatabaseMetaData.getPrimaryKeys fails if table does not exist
673
- return Optional.absent();
674
- }
675
-
676
- DatabaseMetaData dbm = connection.getMetaData();
677
- String escape = dbm.getSearchStringEscape();
678
-
679
- ResultSet rs = dbm.getPrimaryKeys(null, connection.getSchemaName(), tableName);
680
- ImmutableSet.Builder<String> primaryKeysBuilder = ImmutableSet.builder();
681
- try {
682
- while(rs.next()) {
683
- primaryKeysBuilder.add(rs.getString("COLUMN_NAME"));
684
- }
685
- } finally {
686
- rs.close();
687
- }
688
- ImmutableSet<String> primaryKeys = primaryKeysBuilder.build();
689
-
690
- ImmutableList.Builder<JdbcColumn> builder = ImmutableList.builder();
691
- rs = dbm.getColumns(null,
692
- JdbcUtils.escapeSearchString(connection.getSchemaName(), escape),
693
- JdbcUtils.escapeSearchString(tableName, escape),
694
- null);
695
- try {
696
- while (rs.next()) {
697
- String columnName = rs.getString("COLUMN_NAME");
698
- String simpleTypeName = rs.getString("TYPE_NAME").toUpperCase(Locale.ENGLISH);
699
- boolean isUniqueKey = primaryKeys.contains(columnName);
700
- int sqlType = rs.getInt("DATA_TYPE");
701
- int colSize = rs.getInt("COLUMN_SIZE");
702
- int decDigit = rs.getInt("DECIMAL_DIGITS");
703
- if (rs.wasNull()) {
704
- decDigit = -1;
705
- }
706
- boolean isNotNull = "NO".equals(rs.getString("IS_NULLABLE"));
707
- //rs.getString("COLUMN_DEF") // or null // TODO
708
- builder.add(JdbcColumn.newGenericTypeColumn(
709
- columnName, sqlType, simpleTypeName, colSize, decDigit, isNotNull, isUniqueKey));
710
- // We can't get declared column name using JDBC API.
711
- // Subclasses need to overwrite it.
712
- }
713
- } finally {
714
- rs.close();
715
- }
716
- List<JdbcColumn> columns = builder.build();
717
- if (columns.isEmpty()) {
718
- return Optional.absent();
719
- } else {
720
- return Optional.of(new JdbcSchema(columns));
721
- }
722
- }
723
-
724
- private JdbcSchema matchSchemaByColumnNames(Schema inputSchema, JdbcSchema targetTableSchema)
725
- {
726
- ImmutableList.Builder<JdbcColumn> jdbcColumns = ImmutableList.builder();
727
-
728
- for (Column column : inputSchema.getColumns()) {
729
- Optional<JdbcColumn> c = targetTableSchema.findColumn(column.getName());
730
- jdbcColumns.add(c.or(JdbcColumn.skipColumn()));
731
- }
732
-
733
- return new JdbcSchema(jdbcColumns.build());
734
- }
735
-
736
- public TransactionalPageOutput open(TaskSource taskSource, Schema schema, final int taskIndex)
737
- {
738
- final PluginTask task = taskSource.loadTask(getTaskClass());
739
- final Mode mode = task.getMode();
740
-
741
- // instantiate BatchInsert without table name
742
- BatchInsert batch = null;
743
- try {
744
- batch = newBatchInsert(task,
745
- task.getMode() == Mode.MERGE_DIRECT ?
746
- task.getMergeKeys() :
747
- Optional.<List<String>>absent());
748
- } catch (IOException | SQLException ex) {
749
- throw new RuntimeException(ex);
750
- }
751
-
752
- try {
753
- // configure PageReader -> BatchInsert
754
- PageReader reader = new PageReader(schema);
755
-
756
- List<ColumnSetter> columnSetters = newColumnSetters(
757
- new ColumnSetterFactory(batch, task.getDefaultTimeZone()),
758
- task.getTargetTableSchema(), schema,
759
- task.getColumnOptions());
760
- JdbcSchema insertIntoSchema = filterSkipColumns(task.getTargetTableSchema());
761
-
762
- // configure BatchInsert -> an intermediate table (!isDirectModify) or the target table (isDirectModify)
763
- String destTable;
764
- if (mode.tempTablePerTask()) {
765
- destTable = task.getIntermediateTables().get().get(taskIndex);
766
- } else if (mode.isDirectModify()) {
767
- destTable = task.getTable();
768
- } else {
769
- destTable = task.getIntermediateTables().get().get(0);
770
- }
771
- batch.prepare(destTable, insertIntoSchema);
772
-
773
- PluginPageOutput output = new PluginPageOutput(reader, batch, columnSetters, task.getBatchSize());
774
- batch = null;
775
- return output;
776
-
777
- } catch (SQLException ex) {
778
- throw new RuntimeException(ex);
779
-
780
- } finally {
781
- if (batch != null) {
782
- try {
783
- batch.close();
784
- } catch (IOException | SQLException ex) {
785
- throw new RuntimeException(ex);
786
- }
787
- }
788
- }
789
- }
790
-
791
- public static class PluginPageOutput
792
- implements TransactionalPageOutput
793
- {
794
- protected final List<Column> columns;
795
- protected final List<ColumnSetterVisitor> columnVisitors;
796
- private final PageReader pageReader;
797
- private final BatchInsert batch;
798
- private final int batchSize;
799
- private final int forceBatchFlushSize;
800
-
801
- public PluginPageOutput(final PageReader pageReader,
802
- BatchInsert batch, List<ColumnSetter> columnSetters,
803
- int batchSize)
804
- {
805
- this.pageReader = pageReader;
806
- this.batch = batch;
807
- this.columns = pageReader.getSchema().getColumns();
808
- this.columnVisitors = ImmutableList.copyOf(Lists.transform(
809
- columnSetters, new Function<ColumnSetter, ColumnSetterVisitor>() {
810
- public ColumnSetterVisitor apply(ColumnSetter setter)
811
- {
812
- return new ColumnSetterVisitor(pageReader, setter);
813
- }
814
- }));
815
- this.batchSize = batchSize;
816
- this.forceBatchFlushSize = batchSize * 2;
817
- }
818
-
819
- @Override
820
- public void add(Page page)
821
- {
822
- try {
823
- pageReader.setPage(page);
824
- while (pageReader.nextRecord()) {
825
- if (batch.getBatchWeight() > forceBatchFlushSize) {
826
- batch.flush();
827
- }
828
- handleColumnsSetters();
829
- batch.add();
830
- }
831
- if (batch.getBatchWeight() > batchSize) {
832
- batch.flush();
833
- }
834
- } catch (IOException | SQLException ex) {
835
- throw new RuntimeException(ex);
836
- }
837
- }
838
-
839
- @Override
840
- public void finish()
841
- {
842
- try {
843
- batch.finish();
844
- } catch (IOException | SQLException ex) {
845
- throw new RuntimeException(ex);
846
- }
847
- }
848
-
849
- @Override
850
- public void close()
851
- {
852
- try {
853
- batch.close();
854
- } catch (IOException | SQLException ex) {
855
- throw new RuntimeException(ex);
856
- }
857
- }
858
-
859
- @Override
860
- public void abort()
861
- {
862
- }
863
-
864
- @Override
865
- public CommitReport commit()
866
- {
867
- return Exec.newCommitReport();
868
- }
869
-
870
- protected void handleColumnsSetters()
871
- {
872
- int size = columnVisitors.size();
873
- for (int i=0; i < size; i++) {
874
- columns.get(i).visit(columnVisitors.get(i));
875
- }
876
- }
877
- }
878
-
879
- public static interface IdempotentSqlRunnable
880
- {
881
- public void run() throws SQLException;
882
- }
883
-
884
- protected void withRetry(IdempotentSqlRunnable op)
885
- throws SQLException, InterruptedException
886
- {
887
- withRetry(op, "Operation failed");
888
- }
889
-
890
- protected void withRetry(final IdempotentSqlRunnable op, final String errorMessage)
891
- throws SQLException, InterruptedException
892
- {
893
- try {
894
- retryExecutor()
895
- .withRetryLimit(12)
896
- .withInitialRetryWait(1000)
897
- .withMaxRetryWait(30 * 60 * 1000)
898
- .runInterruptible(new Retryable<Void>() {
899
- public Void call() throws Exception
900
- {
901
- op.run();
902
- return null;
903
- }
904
-
905
- public void onRetry(Exception exception, int retryCount, int retryLimit, int retryWait)
906
- {
907
- if (exception instanceof SQLException) {
908
- SQLException ex = (SQLException) exception;
909
- String sqlState = ex.getSQLState();
910
- int errorCode = ex.getErrorCode();
911
- logger.warn("{} ({}:{}), retrying {}/{} after {} seconds. Message: {}",
912
- errorMessage, errorCode, sqlState, retryCount, retryLimit, retryWait/1000,
913
- buildExceptionMessage(exception));
914
- } else {
915
- logger.warn("{}, retrying {}/{} after {} seconds. Message: {}",
916
- errorMessage, retryCount, retryLimit, retryWait/1000,
917
- buildExceptionMessage(exception));
918
- }
919
- if (retryCount % 3 == 0) {
920
- logger.info("Error details:", exception);
921
- }
922
- }
923
-
924
- public void onGiveup(Exception firstException, Exception lastException)
925
- {
926
- if (firstException instanceof SQLException) {
927
- SQLException ex = (SQLException) firstException;
928
- String sqlState = ex.getSQLState();
929
- int errorCode = ex.getErrorCode();
930
- logger.error("{} ({}:{})", errorMessage, errorCode, sqlState);
931
- }
932
- }
933
-
934
- public boolean isRetryableException(Exception exception)
935
- {
936
- //if (exception instanceof SQLException) {
937
- // SQLException ex = (SQLException) exception;
938
- // String sqlState = ex.getSQLState();
939
- // int errorCode = ex.getErrorCode();
940
- // return isRetryableSQLException(ex);
941
- //}
942
- return false; // TODO
943
- }
944
- });
945
-
946
- } catch (ExecutionException ex) {
947
- Throwable cause = ex.getCause();
948
- Throwables.propagateIfInstanceOf(cause, SQLException.class);
949
- throw Throwables.propagate(cause);
950
- }
951
- }
952
-
953
- private String buildExceptionMessage(Throwable ex) {
954
- StringBuilder sb = new StringBuilder();
955
- sb.append(ex.getMessage());
956
- if (ex.getCause() != null) {
957
- buildExceptionMessageCont(sb, ex.getCause(), ex.getMessage());
958
- }
959
- return sb.toString();
960
- }
961
-
962
- private void buildExceptionMessageCont(StringBuilder sb, Throwable ex, String lastMessage) {
963
- if (!lastMessage.equals(ex.getMessage())) {
964
- // suppress same messages
965
- sb.append(" < ");
966
- sb.append(ex.getMessage());
967
- }
968
- if (ex.getCause() == null) {
969
- return;
970
- }
971
- buildExceptionMessageCont(sb, ex.getCause(), ex.getMessage());
972
- }
973
- }
1
+ package org.embulk.output.jdbc;
2
+
3
+ import java.util.HashSet;
4
+ import java.util.List;
5
+ import java.util.Map;
6
+ import java.util.Locale;
7
+ import java.util.Set;
8
+ import java.util.concurrent.ExecutionException;
9
+ import java.io.IOException;
10
+ import java.nio.charset.Charset;
11
+ import java.nio.file.Paths;
12
+ import java.sql.Types;
13
+ import java.sql.ResultSet;
14
+ import java.sql.DatabaseMetaData;
15
+ import java.sql.SQLException;
16
+ import org.slf4j.Logger;
17
+ import org.joda.time.DateTimeZone;
18
+ import com.fasterxml.jackson.annotation.JsonCreator;
19
+ import com.fasterxml.jackson.annotation.JsonValue;
20
+ import com.fasterxml.jackson.annotation.JsonProperty;
21
+ import com.google.common.base.Optional;
22
+ import com.google.common.base.Function;
23
+ import com.google.common.base.Supplier;
24
+ import com.google.common.base.Throwables;
25
+ import com.google.common.collect.Lists;
26
+ import com.google.common.collect.ImmutableList;
27
+ import com.google.common.collect.ImmutableSet;
28
+ import org.embulk.config.Config;
29
+ import org.embulk.config.ConfigDefault;
30
+ import org.embulk.config.ConfigDiff;
31
+ import org.embulk.config.ConfigException;
32
+ import org.embulk.config.ConfigSource;
33
+ import org.embulk.config.Task;
34
+ import org.embulk.config.TaskReport;
35
+ import org.embulk.config.TaskSource;
36
+ import org.embulk.plugin.PluginClassLoader;
37
+ import org.embulk.spi.Exec;
38
+ import org.embulk.spi.Column;
39
+ import org.embulk.spi.ColumnVisitor;
40
+ import org.embulk.spi.OutputPlugin;
41
+ import org.embulk.spi.Schema;
42
+ import org.embulk.spi.TransactionalPageOutput;
43
+ import org.embulk.spi.Page;
44
+ import org.embulk.spi.PageReader;
45
+ import org.embulk.spi.time.Timestamp;
46
+ import org.embulk.output.jdbc.setter.ColumnSetter;
47
+ import org.embulk.output.jdbc.setter.ColumnSetterFactory;
48
+ import org.embulk.output.jdbc.setter.ColumnSetterVisitor;
49
+ import org.embulk.spi.util.RetryExecutor.Retryable;
50
+ import static org.embulk.spi.util.RetryExecutor.retryExecutor;
51
+ import static org.embulk.output.jdbc.JdbcSchema.filterSkipColumns;
52
+
53
+ public abstract class AbstractJdbcOutputPlugin
54
+ implements OutputPlugin
55
+ {
56
+ private final static Set<String> loadedJarGlobs = new HashSet<String>();
57
+
58
+ protected final Logger logger = Exec.getLogger(getClass());
59
+
60
+ public interface PluginTask
61
+ extends Task
62
+ {
63
+ @Config("options")
64
+ @ConfigDefault("{}")
65
+ public ToStringMap getOptions();
66
+
67
+ @Config("table")
68
+ public String getTable();
69
+
70
+ @Config("mode")
71
+ public Mode getMode();
72
+
73
+ @Config("batch_size")
74
+ @ConfigDefault("16777216")
75
+ // TODO set minimum number
76
+ public int getBatchSize();
77
+
78
+ @Config("merge_keys")
79
+ @ConfigDefault("null")
80
+ public Optional<List<String>> getMergeKeys();
81
+
82
+ @Config("column_options")
83
+ @ConfigDefault("{}")
84
+ public Map<String, JdbcColumnOption> getColumnOptions();
85
+
86
+ @Config("default_timezone")
87
+ @ConfigDefault("\"UTC\"")
88
+ public DateTimeZone getDefaultTimeZone();
89
+
90
+ public void setMergeKeys(Optional<List<String>> keys);
91
+
92
+ public void setFeatures(Features features);
93
+ public Features getFeatures();
94
+
95
+ public JdbcSchema getTargetTableSchema();
96
+ public void setTargetTableSchema(JdbcSchema schema);
97
+
98
+ public Optional<List<String>> getIntermediateTables();
99
+ public void setIntermediateTables(Optional<List<String>> names);
100
+ }
101
+
102
+ public static class Features
103
+ {
104
+ private int maxTableNameLength = 64;
105
+ private Set<Mode> supportedModes = ImmutableSet.copyOf(Mode.values());
106
+ private boolean ignoreMergeKeys = false;
107
+
108
+ public Features()
109
+ { }
110
+
111
+ @JsonProperty
112
+ public int getMaxTableNameLength()
113
+ {
114
+ return maxTableNameLength;
115
+ }
116
+
117
+ @JsonProperty
118
+ public Features setMaxTableNameLength(int bytes)
119
+ {
120
+ this.maxTableNameLength = bytes;
121
+ return this;
122
+ }
123
+
124
+ @JsonProperty
125
+ public Set<Mode> getSupportedModes()
126
+ {
127
+ return supportedModes;
128
+ }
129
+
130
+ @JsonProperty
131
+ public Features setSupportedModes(Set<Mode> modes)
132
+ {
133
+ this.supportedModes = modes;
134
+ return this;
135
+ }
136
+
137
+ @JsonProperty
138
+ public boolean getIgnoreMergeKeys()
139
+ {
140
+ return ignoreMergeKeys;
141
+ }
142
+
143
+ @JsonProperty
144
+ public Features setIgnoreMergeKeys(boolean value)
145
+ {
146
+ this.ignoreMergeKeys = value;
147
+ return this;
148
+ }
149
+ }
150
+
151
+ protected void loadDriverJar(String glob)
152
+ {
153
+ synchronized (loadedJarGlobs) {
154
+ if (!loadedJarGlobs.contains(glob)) {
155
+ // TODO match glob
156
+ PluginClassLoader loader = (PluginClassLoader) getClass().getClassLoader();
157
+ loader.addPath(Paths.get(glob));
158
+ loadedJarGlobs.add(glob);
159
+ }
160
+ }
161
+ }
162
+
163
+ // for subclasses to add @Config
164
+ protected Class<? extends PluginTask> getTaskClass()
165
+ {
166
+ return PluginTask.class;
167
+ }
168
+
169
+ protected abstract Features getFeatures(PluginTask task);
170
+
171
+ protected abstract JdbcOutputConnector getConnector(PluginTask task, boolean retryableMetadataOperation);
172
+
173
+ protected abstract BatchInsert newBatchInsert(PluginTask task, Optional<List<String>> mergeKeys) throws IOException, SQLException;
174
+
175
+ protected JdbcOutputConnection newConnection(PluginTask task, boolean retryableMetadataOperation,
176
+ boolean autoCommit) throws SQLException
177
+ {
178
+ return getConnector(task, retryableMetadataOperation).connect(autoCommit);
179
+ }
180
+
181
+ public enum Mode {
182
+ INSERT,
183
+ INSERT_DIRECT,
184
+ MERGE,
185
+ MERGE_DIRECT,
186
+ TRUNCATE_INSERT,
187
+ REPLACE;
188
+
189
+ @JsonValue
190
+ @Override
191
+ public String toString()
192
+ {
193
+ return name().toLowerCase(Locale.ENGLISH);
194
+ }
195
+
196
+ @JsonCreator
197
+ public static Mode fromString(String value)
198
+ {
199
+ switch(value) {
200
+ case "insert":
201
+ return INSERT;
202
+ case "insert_direct":
203
+ return INSERT_DIRECT;
204
+ case "merge":
205
+ return MERGE;
206
+ case "merge_direct":
207
+ return MERGE_DIRECT;
208
+ case "truncate_insert":
209
+ return TRUNCATE_INSERT;
210
+ case "replace":
211
+ return REPLACE;
212
+ default:
213
+ throw new ConfigException(String.format("Unknown mode '%s'. Supported modes are insert, insert_direct, merge, merge_direct, truncate_insert, replace", value));
214
+ }
215
+ }
216
+
217
+ /**
218
+ * True if this mode directly modifies the target table without creating intermediate tables.
219
+ */
220
+ public boolean isDirectModify()
221
+ {
222
+ return this == INSERT_DIRECT || this == MERGE_DIRECT;
223
+ }
224
+
225
+ /**
226
+ * True if this mode merges records on unique keys
227
+ */
228
+ public boolean isMerge()
229
+ {
230
+ return this == MERGE || this == MERGE_DIRECT;
231
+ }
232
+
233
+ /**
234
+ * True if this mode creates intermediate table for each tasks.
235
+ */
236
+ public boolean tempTablePerTask()
237
+ {
238
+ return this == INSERT || this == MERGE || this == TRUNCATE_INSERT /*this == REPLACE_VIEW*/;
239
+ }
240
+
241
+ /**
242
+ * True if this mode truncates the target table before committing intermediate tables
243
+ */
244
+ public boolean truncateBeforeCommit()
245
+ {
246
+ return this == TRUNCATE_INSERT;
247
+ }
248
+
249
+ /**
250
+ * True if this mode uses MERGE statement to commit intermediate tables to the target table
251
+ */
252
+ public boolean commitByMerge()
253
+ {
254
+ return this == MERGE;
255
+ }
256
+
257
+ /**
258
+ * True if this mode overwrites schema of the target tables
259
+ */
260
+ public boolean ignoreTargetTableSchema()
261
+ {
262
+ return this == REPLACE /*|| this == REPLACE_VIEW*/;
263
+ }
264
+
265
+ /**
266
+ * True if this mode swaps the target tables with intermediate tables to commit
267
+ */
268
+ public boolean commitBySwapTable()
269
+ {
270
+ return this == REPLACE;
271
+ }
272
+ }
273
+
274
+ public ConfigDiff transaction(ConfigSource config,
275
+ Schema schema, int taskCount,
276
+ OutputPlugin.Control control)
277
+ {
278
+ PluginTask task = config.loadConfig(getTaskClass());
279
+ Features features = getFeatures(task);
280
+ task.setFeatures(features);
281
+
282
+ if (!features.getSupportedModes().contains(task.getMode())) {
283
+ throw new ConfigException(String.format("This output type doesn't support '%s'. Supported modes are: %s", task.getMode(), features.getSupportedModes()));
284
+ }
285
+
286
+ task = begin(task, schema, taskCount);
287
+ control.run(task.dump());
288
+ return commit(task, schema, taskCount);
289
+ }
290
+
291
+ public ConfigDiff resume(TaskSource taskSource,
292
+ Schema schema, int taskCount,
293
+ OutputPlugin.Control control)
294
+ {
295
+ PluginTask task = taskSource.loadTask(getTaskClass());
296
+
297
+ if (!task.getMode().tempTablePerTask()) {
298
+ throw new UnsupportedOperationException("inplace mode is not resumable. You need to delete partially-loaded records from the database and restart the entire transaction.");
299
+ }
300
+
301
+ task = begin(task, schema, taskCount);
302
+ control.run(task.dump());
303
+ return commit(task, schema, taskCount);
304
+ }
305
+
306
+ protected String getTransactionUniqueName()
307
+ {
308
+ // TODO use uuid?
309
+ Timestamp t = Exec.session().getTransactionTime();
310
+ return String.format("%016x%08x", t.getEpochSecond(), t.getNano());
311
+ }
312
+
313
+ private PluginTask begin(final PluginTask task,
314
+ final Schema schema, final int taskCount)
315
+ {
316
+ try {
317
+ withRetry(new IdempotentSqlRunnable() { // no intermediate data if isDirectModify == true
318
+ public void run() throws SQLException
319
+ {
320
+ JdbcOutputConnection con = newConnection(task, true, false);
321
+ try {
322
+ doBegin(con, task, schema, taskCount);
323
+ } finally {
324
+ con.close();
325
+ }
326
+ }
327
+ });
328
+ } catch (SQLException | InterruptedException ex) {
329
+ throw new RuntimeException(ex);
330
+ }
331
+ return task;
332
+ }
333
+
334
+ private ConfigDiff commit(final PluginTask task,
335
+ Schema schema, final int taskCount)
336
+ {
337
+ if (!task.getMode().isDirectModify()) { // no intermediate data if isDirectModify == true
338
+ try {
339
+ withRetry(new IdempotentSqlRunnable() {
340
+ public void run() throws SQLException
341
+ {
342
+ JdbcOutputConnection con = newConnection(task, false, false);
343
+ try {
344
+ doCommit(con, task, taskCount);
345
+ } finally {
346
+ con.close();
347
+ }
348
+ }
349
+ });
350
+ } catch (SQLException | InterruptedException ex) {
351
+ throw new RuntimeException(ex);
352
+ }
353
+ }
354
+ return Exec.newConfigDiff();
355
+ }
356
+
357
+ public void cleanup(TaskSource taskSource,
358
+ Schema schema, final int taskCount,
359
+ final List<TaskReport> successTaskReports)
360
+ {
361
+ final PluginTask task = taskSource.loadTask(getTaskClass());
362
+
363
+ if (!task.getMode().isDirectModify()) { // no intermediate data if isDirectModify == true
364
+ try {
365
+ withRetry(new IdempotentSqlRunnable() {
366
+ public void run() throws SQLException
367
+ {
368
+ JdbcOutputConnection con = newConnection(task, true, true);
369
+ try {
370
+ doCleanup(con, task, taskCount, successTaskReports);
371
+ } finally {
372
+ con.close();
373
+ }
374
+ }
375
+ });
376
+ } catch (SQLException | InterruptedException ex) {
377
+ throw new RuntimeException(ex);
378
+ }
379
+ }
380
+ }
381
+
382
+ protected void doBegin(JdbcOutputConnection con,
383
+ PluginTask task, final Schema schema, int taskCount) throws SQLException
384
+ {
385
+ if (schema.getColumnCount() == 0) {
386
+ throw new ConfigException("task count == 0 is not supported");
387
+ }
388
+
389
+ Mode mode = task.getMode();
390
+ logger.info("Using {} mode", mode);
391
+
392
+ Optional<JdbcSchema> initialTargetTableSchema =
393
+ mode.ignoreTargetTableSchema() ?
394
+ Optional.<JdbcSchema>absent() :
395
+ newJdbcSchemaFromTableIfExists(con, task.getTable());
396
+
397
+ // TODO get CREATE TABLE statement from task if set
398
+ JdbcSchema newTableSchema = applyColumnOptionsToNewTableSchema(
399
+ initialTargetTableSchema.or(new Supplier<JdbcSchema>() {
400
+ public JdbcSchema get()
401
+ {
402
+ return newJdbcSchemaForNewTable(schema);
403
+ }
404
+ }),
405
+ task.getColumnOptions());
406
+
407
+ // create intermediate tables
408
+ if (!mode.isDirectModify()) {
409
+ // direct modify mode doesn't need intermediate tables.
410
+ ImmutableList.Builder<String> intermTableNames = ImmutableList.builder();
411
+ if (mode.tempTablePerTask()) {
412
+ String namePrefix = generateIntermediateTableNamePrefix(task.getTable(), con, 3, task.getFeatures().getMaxTableNameLength());
413
+ for (int i=0; i < taskCount; i++) {
414
+ intermTableNames.add(namePrefix + String.format("%03d", i));
415
+ }
416
+ } else {
417
+ String name = generateIntermediateTableNamePrefix(task.getTable(), con, 0, task.getFeatures().getMaxTableNameLength());
418
+ intermTableNames.add(name);
419
+ }
420
+ // create the intermediate tables here
421
+ task.setIntermediateTables(Optional.<List<String>>of(intermTableNames.build()));
422
+ for (String name : task.getIntermediateTables().get()) {
423
+ // DROP TABLE IF EXISTS xyz__0000000054d92dee1e452158_bulk_load_temp
424
+ con.dropTableIfExists(name);
425
+ // CREATE TABLE IF NOT EXISTS xyz__0000000054d92dee1e452158_bulk_load_temp
426
+ con.createTableIfNotExists(name, newTableSchema);
427
+ }
428
+ } else {
429
+ task.setIntermediateTables(Optional.<List<String>>absent());
430
+ }
431
+
432
+ // build JdbcSchema from a table
433
+ JdbcSchema targetTableSchema;
434
+ if (initialTargetTableSchema.isPresent()) {
435
+ targetTableSchema = initialTargetTableSchema.get();
436
+ } else if (task.getIntermediateTables().isPresent() && !task.getIntermediateTables().get().isEmpty()) {
437
+ String firstItermTable = task.getIntermediateTables().get().get(0);
438
+ targetTableSchema = newJdbcSchemaFromTableIfExists(con, firstItermTable).get();
439
+ } else {
440
+ // also create the target table if not exists
441
+ // CREATE TABLE IF NOT EXISTS xyz
442
+ con.createTableIfNotExists(task.getTable(), newTableSchema);
443
+ targetTableSchema = newJdbcSchemaFromTableIfExists(con, task.getTable()).get();
444
+ }
445
+ task.setTargetTableSchema(matchSchemaByColumnNames(schema, targetTableSchema));
446
+
447
+ // validate column_options
448
+ newColumnSetters(
449
+ new ColumnSetterFactory(null, task.getDefaultTimeZone()), // TODO create a dummy BatchInsert
450
+ task.getTargetTableSchema(), schema,
451
+ task.getColumnOptions());
452
+
453
+ // normalize merge_key parameter for merge modes
454
+ if (mode.isMerge()) {
455
+ Optional<List<String>> mergeKeys = task.getMergeKeys();
456
+ if (task.getFeatures().getIgnoreMergeKeys()) {
457
+ if (mergeKeys.isPresent()) {
458
+ throw new ConfigException("This output type does not accept 'merge_key' option.");
459
+ }
460
+ task.setMergeKeys(Optional.<List<String>>of(ImmutableList.<String>of()));
461
+ } else if (mergeKeys.isPresent()) {
462
+ if (task.getMergeKeys().get().isEmpty()) {
463
+ throw new ConfigException("Empty 'merge_keys' option is invalid.");
464
+ }
465
+ for (String key : mergeKeys.get()) {
466
+ if (!targetTableSchema.findColumn(key).isPresent()) {
467
+ throw new ConfigException(String.format("Merge key '%s' does not exist in the target table.", key));
468
+ }
469
+ }
470
+ } else {
471
+ ImmutableList.Builder<String> builder = ImmutableList.builder();
472
+ for (JdbcColumn column : targetTableSchema.getColumns()) {
473
+ if (column.isUniqueKey()) {
474
+ builder.add(column.getName());
475
+ }
476
+ }
477
+ task.setMergeKeys(Optional.<List<String>>of(builder.build()));
478
+ if (task.getMergeKeys().get().isEmpty()) {
479
+ throw new ConfigException("Merging mode is used but the target table does not have primary keys. Please set merge_keys option.");
480
+ }
481
+ }
482
+ logger.info("Using merge keys: {}", task.getMergeKeys().get());
483
+ } else {
484
+ task.setMergeKeys(Optional.<List<String>>absent());
485
+ }
486
+ }
487
+
488
+ protected String generateIntermediateTableNamePrefix(String baseTableName, JdbcOutputConnection con, int suffixLength, int maxLength) throws SQLException
489
+ {
490
+ Charset tableNameCharset = con.getTableNameCharset();
491
+ String tableName = baseTableName;
492
+ String suffix = "_bl_tmp";
493
+ String uniqueSuffix = getTransactionUniqueName() + suffix;
494
+
495
+ // way to count length of table name varies by DBMSs (bytes or characters),
496
+ // so truncate swap table name by one character.
497
+ while (!checkTableNameLength(tableName + "_" + uniqueSuffix, tableNameCharset, suffixLength, maxLength)) {
498
+ if (uniqueSuffix.length() > 8 + suffix.length()) {
499
+ // truncate transaction unique name
500
+ // (include 8 characters of the transaction name at least)
501
+ uniqueSuffix = uniqueSuffix.substring(1);
502
+ } else {
503
+ if (tableName.isEmpty()) {
504
+ throw new ConfigException("Table name is too long to generate temporary table name");
505
+ }
506
+ // truncate table name
507
+ tableName = tableName.substring(0, tableName.length() - 1);
508
+ //if (!connection.tableExists(tableName)) {
509
+ // TODO this doesn't help. Rather than truncating more characters,
510
+ // here needs to replace characters with random characters. But
511
+ // to make the result deterministic. So, an idea is replacing
512
+ // the last character to the first (second, third, ... for each loop)
513
+ // of md5(original table name).
514
+ //}
515
+ }
516
+
517
+ }
518
+ return tableName + "_" + uniqueSuffix;
519
+ }
520
+
521
+ private static JdbcSchema applyColumnOptionsToNewTableSchema(JdbcSchema schema, final Map<String, JdbcColumnOption> columnOptions)
522
+ {
523
+ return new JdbcSchema(Lists.transform(schema.getColumns(), new Function<JdbcColumn, JdbcColumn>() {
524
+ public JdbcColumn apply(JdbcColumn c)
525
+ {
526
+ JdbcColumnOption option = columnOptionOf(columnOptions, c);
527
+ if (option.getType().isPresent()) {
528
+ return JdbcColumn.newTypeDeclaredColumn(
529
+ c.getName(), Types.OTHER, // sqlType, isNotNull, and isUniqueKey are ignored
530
+ option.getType().get(), false, false);
531
+ }
532
+ return c;
533
+ }
534
+ }));
535
+ }
536
+
537
+ protected static List<ColumnSetter> newColumnSetters(ColumnSetterFactory factory,
538
+ JdbcSchema targetTableSchema, Schema inputValueSchema,
539
+ Map<String, JdbcColumnOption> columnOptions)
540
+ {
541
+ ImmutableList.Builder<ColumnSetter> builder = ImmutableList.builder();
542
+ int schemaColumnIndex = 0;
543
+ for (JdbcColumn targetColumn : targetTableSchema.getColumns()) {
544
+ if (targetColumn.isSkipColumn()) {
545
+ builder.add(factory.newSkipColumnSetter());
546
+ } else {
547
+ //String columnOptionKey = inputValueSchema.getColumn(schemaColumnIndex).getName();
548
+ JdbcColumnOption option = columnOptionOf(columnOptions, targetColumn);
549
+ builder.add(factory.newColumnSetter(targetColumn, option));
550
+ schemaColumnIndex++;
551
+ }
552
+ }
553
+ return builder.build();
554
+ }
555
+
556
+ private static JdbcColumnOption columnOptionOf(Map<String, JdbcColumnOption> columnOptions, JdbcColumn targetColumn)
557
+ {
558
+ return Optional.fromNullable(columnOptions.get(targetColumn.getName())).or(
559
+ // default column option
560
+ new Supplier<JdbcColumnOption>()
561
+ {
562
+ public JdbcColumnOption get()
563
+ {
564
+ return Exec.newConfigSource().loadConfig(JdbcColumnOption.class);
565
+ }
566
+ });
567
+ }
568
+
569
+ private boolean checkTableNameLength(String tableName, Charset tableNameCharset, int suffixLength, int maxLength)
570
+ {
571
+ return tableNameCharset.encode(tableName).remaining() + suffixLength <= maxLength;
572
+ }
573
+
574
+ protected void doCommit(JdbcOutputConnection con, PluginTask task, int taskCount)
575
+ throws SQLException
576
+ {
577
+ if (task.getIntermediateTables().get().isEmpty()) {
578
+ return;
579
+ }
580
+
581
+ JdbcSchema schema = filterSkipColumns(task.getTargetTableSchema());
582
+
583
+ switch (task.getMode()) {
584
+ case INSERT_DIRECT:
585
+ case MERGE_DIRECT:
586
+ // already done
587
+ break;
588
+
589
+ case INSERT:
590
+ // aggregate insert into target
591
+ con.collectInsert(task.getIntermediateTables().get(), schema, task.getTable(), false);
592
+ break;
593
+
594
+ case TRUNCATE_INSERT:
595
+ // truncate & aggregate insert into target
596
+ con.collectInsert(task.getIntermediateTables().get(), schema, task.getTable(), true);
597
+ break;
598
+
599
+ case MERGE:
600
+ // aggregate merge into target
601
+ con.collectMerge(task.getIntermediateTables().get(), schema, task.getTable(), task.getMergeKeys().get());
602
+ break;
603
+
604
+ case REPLACE:
605
+ // swap table
606
+ con.replaceTable(task.getIntermediateTables().get().get(0), schema, task.getTable());
607
+ break;
608
+ }
609
+ }
610
+
611
+ protected void doCleanup(JdbcOutputConnection con, PluginTask task, int taskCount,
612
+ List<TaskReport> successTaskReports)
613
+ throws SQLException
614
+ {
615
+ if (task.getIntermediateTables().isPresent()) {
616
+ for (String intermTable : task.getIntermediateTables().get()) {
617
+ con.dropTableIfExists(intermTable);
618
+ }
619
+ }
620
+ }
621
+
622
+ protected JdbcSchema newJdbcSchemaForNewTable(Schema schema)
623
+ {
624
+ final ImmutableList.Builder<JdbcColumn> columns = ImmutableList.builder();
625
+ for (Column c : schema.getColumns()) {
626
+ final String columnName = c.getName();
627
+ c.visit(new ColumnVisitor() {
628
+ public void booleanColumn(Column column)
629
+ {
630
+ columns.add(JdbcColumn.newGenericTypeColumn(
631
+ columnName, Types.BOOLEAN, "BOOLEAN",
632
+ 1, 0, false, false));
633
+ }
634
+
635
+ public void longColumn(Column column)
636
+ {
637
+ columns.add(JdbcColumn.newGenericTypeColumn(
638
+ columnName, Types.BIGINT, "BIGINT",
639
+ 22, 0, false, false));
640
+ }
641
+
642
+ public void doubleColumn(Column column)
643
+ {
644
+ columns.add(JdbcColumn.newGenericTypeColumn(
645
+ columnName, Types.FLOAT, "DOUBLE PRECISION",
646
+ 24, 0, false, false));
647
+ }
648
+
649
+ public void stringColumn(Column column)
650
+ {
651
+ columns.add(JdbcColumn.newGenericTypeColumn(
652
+ columnName, Types.CLOB, "CLOB",
653
+ 4000, 0, false, false)); // TODO size type param
654
+ }
655
+
656
+ public void timestampColumn(Column column)
657
+ {
658
+ columns.add(JdbcColumn.newGenericTypeColumn(
659
+ columnName, Types.TIMESTAMP, "TIMESTAMP",
660
+ 26, 0, false, false)); // size type param is from postgresql
661
+ }
662
+ });
663
+ }
664
+ return new JdbcSchema(columns.build());
665
+ }
666
+
667
+ public Optional<JdbcSchema> newJdbcSchemaFromTableIfExists(JdbcOutputConnection connection,
668
+ String tableName) throws SQLException
669
+ {
670
+ if (!connection.tableExists(tableName)) {
671
+ // DatabaseMetaData.getPrimaryKeys fails if table does not exist
672
+ return Optional.absent();
673
+ }
674
+
675
+ DatabaseMetaData dbm = connection.getMetaData();
676
+ String escape = dbm.getSearchStringEscape();
677
+
678
+ ResultSet rs = dbm.getPrimaryKeys(null, connection.getSchemaName(), tableName);
679
+ ImmutableSet.Builder<String> primaryKeysBuilder = ImmutableSet.builder();
680
+ try {
681
+ while(rs.next()) {
682
+ primaryKeysBuilder.add(rs.getString("COLUMN_NAME"));
683
+ }
684
+ } finally {
685
+ rs.close();
686
+ }
687
+ ImmutableSet<String> primaryKeys = primaryKeysBuilder.build();
688
+
689
+ ImmutableList.Builder<JdbcColumn> builder = ImmutableList.builder();
690
+ rs = dbm.getColumns(null,
691
+ JdbcUtils.escapeSearchString(connection.getSchemaName(), escape),
692
+ JdbcUtils.escapeSearchString(tableName, escape),
693
+ null);
694
+ try {
695
+ while (rs.next()) {
696
+ String columnName = rs.getString("COLUMN_NAME");
697
+ String simpleTypeName = rs.getString("TYPE_NAME").toUpperCase(Locale.ENGLISH);
698
+ boolean isUniqueKey = primaryKeys.contains(columnName);
699
+ int sqlType = rs.getInt("DATA_TYPE");
700
+ int colSize = rs.getInt("COLUMN_SIZE");
701
+ int decDigit = rs.getInt("DECIMAL_DIGITS");
702
+ if (rs.wasNull()) {
703
+ decDigit = -1;
704
+ }
705
+ boolean isNotNull = "NO".equals(rs.getString("IS_NULLABLE"));
706
+ //rs.getString("COLUMN_DEF") // or null // TODO
707
+ builder.add(JdbcColumn.newGenericTypeColumn(
708
+ columnName, sqlType, simpleTypeName, colSize, decDigit, isNotNull, isUniqueKey));
709
+ // We can't get declared column name using JDBC API.
710
+ // Subclasses need to overwrite it.
711
+ }
712
+ } finally {
713
+ rs.close();
714
+ }
715
+ List<JdbcColumn> columns = builder.build();
716
+ if (columns.isEmpty()) {
717
+ return Optional.absent();
718
+ } else {
719
+ return Optional.of(new JdbcSchema(columns));
720
+ }
721
+ }
722
+
723
+ private JdbcSchema matchSchemaByColumnNames(Schema inputSchema, JdbcSchema targetTableSchema)
724
+ {
725
+ ImmutableList.Builder<JdbcColumn> jdbcColumns = ImmutableList.builder();
726
+
727
+ for (Column column : inputSchema.getColumns()) {
728
+ Optional<JdbcColumn> c = targetTableSchema.findColumn(column.getName());
729
+ jdbcColumns.add(c.or(JdbcColumn.skipColumn()));
730
+ }
731
+
732
+ return new JdbcSchema(jdbcColumns.build());
733
+ }
734
+
735
+ public TransactionalPageOutput open(TaskSource taskSource, Schema schema, final int taskIndex)
736
+ {
737
+ final PluginTask task = taskSource.loadTask(getTaskClass());
738
+ final Mode mode = task.getMode();
739
+
740
+ // instantiate BatchInsert without table name
741
+ BatchInsert batch = null;
742
+ try {
743
+ batch = newBatchInsert(task,
744
+ task.getMode() == Mode.MERGE_DIRECT ?
745
+ task.getMergeKeys() :
746
+ Optional.<List<String>>absent());
747
+ } catch (IOException | SQLException ex) {
748
+ throw new RuntimeException(ex);
749
+ }
750
+
751
+ try {
752
+ // configure PageReader -> BatchInsert
753
+ PageReader reader = new PageReader(schema);
754
+
755
+ List<ColumnSetter> columnSetters = newColumnSetters(
756
+ new ColumnSetterFactory(batch, task.getDefaultTimeZone()),
757
+ task.getTargetTableSchema(), schema,
758
+ task.getColumnOptions());
759
+ JdbcSchema insertIntoSchema = filterSkipColumns(task.getTargetTableSchema());
760
+
761
+ // configure BatchInsert -> an intermediate table (!isDirectModify) or the target table (isDirectModify)
762
+ String destTable;
763
+ if (mode.tempTablePerTask()) {
764
+ destTable = task.getIntermediateTables().get().get(taskIndex);
765
+ } else if (mode.isDirectModify()) {
766
+ destTable = task.getTable();
767
+ } else {
768
+ destTable = task.getIntermediateTables().get().get(0);
769
+ }
770
+ batch.prepare(destTable, insertIntoSchema);
771
+
772
+ PluginPageOutput output = new PluginPageOutput(reader, batch, columnSetters, task.getBatchSize());
773
+ batch = null;
774
+ return output;
775
+
776
+ } catch (SQLException ex) {
777
+ throw new RuntimeException(ex);
778
+
779
+ } finally {
780
+ if (batch != null) {
781
+ try {
782
+ batch.close();
783
+ } catch (IOException | SQLException ex) {
784
+ throw new RuntimeException(ex);
785
+ }
786
+ }
787
+ }
788
+ }
789
+
790
+ public static class PluginPageOutput
791
+ implements TransactionalPageOutput
792
+ {
793
+ protected final List<Column> columns;
794
+ protected final List<ColumnSetterVisitor> columnVisitors;
795
+ private final PageReader pageReader;
796
+ private final BatchInsert batch;
797
+ private final int batchSize;
798
+ private final int forceBatchFlushSize;
799
+
800
+ public PluginPageOutput(final PageReader pageReader,
801
+ BatchInsert batch, List<ColumnSetter> columnSetters,
802
+ int batchSize)
803
+ {
804
+ this.pageReader = pageReader;
805
+ this.batch = batch;
806
+ this.columns = pageReader.getSchema().getColumns();
807
+ this.columnVisitors = ImmutableList.copyOf(Lists.transform(
808
+ columnSetters, new Function<ColumnSetter, ColumnSetterVisitor>() {
809
+ public ColumnSetterVisitor apply(ColumnSetter setter)
810
+ {
811
+ return new ColumnSetterVisitor(pageReader, setter);
812
+ }
813
+ }));
814
+ this.batchSize = batchSize;
815
+ this.forceBatchFlushSize = batchSize * 2;
816
+ }
817
+
818
+ @Override
819
+ public void add(Page page)
820
+ {
821
+ try {
822
+ pageReader.setPage(page);
823
+ while (pageReader.nextRecord()) {
824
+ if (batch.getBatchWeight() > forceBatchFlushSize) {
825
+ batch.flush();
826
+ }
827
+ handleColumnsSetters();
828
+ batch.add();
829
+ }
830
+ if (batch.getBatchWeight() > batchSize) {
831
+ batch.flush();
832
+ }
833
+ } catch (IOException | SQLException ex) {
834
+ throw new RuntimeException(ex);
835
+ }
836
+ }
837
+
838
+ @Override
839
+ public void finish()
840
+ {
841
+ try {
842
+ batch.finish();
843
+ } catch (IOException | SQLException ex) {
844
+ throw new RuntimeException(ex);
845
+ }
846
+ }
847
+
848
+ @Override
849
+ public void close()
850
+ {
851
+ try {
852
+ batch.close();
853
+ } catch (IOException | SQLException ex) {
854
+ throw new RuntimeException(ex);
855
+ }
856
+ }
857
+
858
+ @Override
859
+ public void abort()
860
+ {
861
+ }
862
+
863
+ @Override
864
+ public TaskReport commit()
865
+ {
866
+ return Exec.newTaskReport();
867
+ }
868
+
869
+ protected void handleColumnsSetters()
870
+ {
871
+ int size = columnVisitors.size();
872
+ for (int i=0; i < size; i++) {
873
+ columns.get(i).visit(columnVisitors.get(i));
874
+ }
875
+ }
876
+ }
877
+
878
+ public static interface IdempotentSqlRunnable
879
+ {
880
+ public void run() throws SQLException;
881
+ }
882
+
883
+ protected void withRetry(IdempotentSqlRunnable op)
884
+ throws SQLException, InterruptedException
885
+ {
886
+ withRetry(op, "Operation failed");
887
+ }
888
+
889
+ protected void withRetry(final IdempotentSqlRunnable op, final String errorMessage)
890
+ throws SQLException, InterruptedException
891
+ {
892
+ try {
893
+ retryExecutor()
894
+ .withRetryLimit(12)
895
+ .withInitialRetryWait(1000)
896
+ .withMaxRetryWait(30 * 60 * 1000)
897
+ .runInterruptible(new Retryable<Void>() {
898
+ public Void call() throws Exception
899
+ {
900
+ op.run();
901
+ return null;
902
+ }
903
+
904
+ public void onRetry(Exception exception, int retryCount, int retryLimit, int retryWait)
905
+ {
906
+ if (exception instanceof SQLException) {
907
+ SQLException ex = (SQLException) exception;
908
+ String sqlState = ex.getSQLState();
909
+ int errorCode = ex.getErrorCode();
910
+ logger.warn("{} ({}:{}), retrying {}/{} after {} seconds. Message: {}",
911
+ errorMessage, errorCode, sqlState, retryCount, retryLimit, retryWait/1000,
912
+ buildExceptionMessage(exception));
913
+ } else {
914
+ logger.warn("{}, retrying {}/{} after {} seconds. Message: {}",
915
+ errorMessage, retryCount, retryLimit, retryWait/1000,
916
+ buildExceptionMessage(exception));
917
+ }
918
+ if (retryCount % 3 == 0) {
919
+ logger.info("Error details:", exception);
920
+ }
921
+ }
922
+
923
+ public void onGiveup(Exception firstException, Exception lastException)
924
+ {
925
+ if (firstException instanceof SQLException) {
926
+ SQLException ex = (SQLException) firstException;
927
+ String sqlState = ex.getSQLState();
928
+ int errorCode = ex.getErrorCode();
929
+ logger.error("{} ({}:{})", errorMessage, errorCode, sqlState);
930
+ }
931
+ }
932
+
933
+ public boolean isRetryableException(Exception exception)
934
+ {
935
+ //if (exception instanceof SQLException) {
936
+ // SQLException ex = (SQLException) exception;
937
+ // String sqlState = ex.getSQLState();
938
+ // int errorCode = ex.getErrorCode();
939
+ // return isRetryableSQLException(ex);
940
+ //}
941
+ return false; // TODO
942
+ }
943
+ });
944
+
945
+ } catch (ExecutionException ex) {
946
+ Throwable cause = ex.getCause();
947
+ Throwables.propagateIfInstanceOf(cause, SQLException.class);
948
+ throw Throwables.propagate(cause);
949
+ }
950
+ }
951
+
952
+ private String buildExceptionMessage(Throwable ex) {
953
+ StringBuilder sb = new StringBuilder();
954
+ sb.append(ex.getMessage());
955
+ if (ex.getCause() != null) {
956
+ buildExceptionMessageCont(sb, ex.getCause(), ex.getMessage());
957
+ }
958
+ return sb.toString();
959
+ }
960
+
961
+ private void buildExceptionMessageCont(StringBuilder sb, Throwable ex, String lastMessage) {
962
+ if (!lastMessage.equals(ex.getMessage())) {
963
+ // suppress same messages
964
+ sb.append(" < ");
965
+ sb.append(ex.getMessage());
966
+ }
967
+ if (ex.getCause() == null) {
968
+ return;
969
+ }
970
+ buildExceptionMessageCont(sb, ex.getCause(), ex.getMessage());
971
+ }
972
+ }