embulk-output-jdbc 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/build.gradle +2 -2
  3. data/classpath/{embulk-output-jdbc-0.4.0.jar → embulk-output-jdbc-0.4.1.jar} +0 -0
  4. data/lib/embulk/output/jdbc.rb +3 -3
  5. data/src/main/java/org/embulk/output/JdbcOutputPlugin.java +138 -138
  6. data/src/main/java/org/embulk/output/jdbc/AbstractJdbcOutputPlugin.java +973 -972
  7. data/src/main/java/org/embulk/output/jdbc/BatchInsert.java +53 -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 +155 -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 +35 -35
  17. data/src/main/java/org/embulk/output/jdbc/setter/BigDecimalColumnSetter.java +70 -70
  18. data/src/main/java/org/embulk/output/jdbc/setter/BooleanColumnSetter.java +54 -54
  19. data/src/main/java/org/embulk/output/jdbc/setter/ByteColumnSetter.java +76 -76
  20. data/src/main/java/org/embulk/output/jdbc/setter/ColumnSetter.java +45 -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 +96 -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 +61 -61
  25. data/src/main/java/org/embulk/output/jdbc/setter/FloatColumnSetter.java +61 -61
  26. data/src/main/java/org/embulk/output/jdbc/setter/IntColumnSetter.java +76 -76
  27. data/src/main/java/org/embulk/output/jdbc/setter/LongColumnSetter.java +72 -72
  28. data/src/main/java/org/embulk/output/jdbc/setter/NStringColumnSetter.java +59 -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 +59 -59
  32. data/src/main/java/org/embulk/output/jdbc/setter/ShortColumnSetter.java +76 -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 +59 -59
  35. data/src/main/java/org/embulk/output/jdbc/setter/SqlTimeColumnSetter.java +59 -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 +59 -59
  38. data/src/test/java/org/embulk/output/TestJdbcOutputPlugin.java +5 -5
  39. metadata +3 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 693efe41999ffde2ffae134407fb1b38aedd65ad
4
- data.tar.gz: 61862ecaafebf03f884c0c3c1756d6a4381c14e4
3
+ metadata.gz: 011dc3e66d8ccb1d049311f46ffc83a49a1863c5
4
+ data.tar.gz: 1dd9e921ebdfaf9155c1d2eaedb2fbeabf77cb74
5
5
  SHA512:
6
- metadata.gz: ba4055e90152b59309eae136375e455f30b099cddc93d0470fb60fe0fcd3681143d61a5ab4424865e9dc3a68af3b8da0afbd0e764c4a64de89d2c0a51c400fce
7
- data.tar.gz: 6a8a60a53f2dae9f38ea0da0cc5a5b890232be4227a683f77a4374d43255a9e7bad4158b809bdf6a0e8b310dd01c283916aa41b6e90310c2df54fea541a0c81b
6
+ metadata.gz: 309a873c516a8e6ecdbcc1410854fd98eee6f1e999d415fc76a4c8343cbdfffdbd0e77a16d02fbb06b68da6592dbd6b4348639d92123895c0bf9fb081c4e30c8
7
+ data.tar.gz: 7c0a97867da33914850106b6b938788efcca67da609e5fd9ff2bd3cfa5f608b29d3bae849d73c44b0533695cad7ce7504c16610320d99990dece155e7d296b22
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,138 @@
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.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,972 +1,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.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.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.PluginClassLoader;
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.type.StringType;
47
- import org.embulk.spi.time.Timestamp;
48
- import org.embulk.spi.time.TimestampFormat;
49
- import org.embulk.spi.time.TimestampFormatter;
50
- import org.embulk.output.jdbc.setter.ColumnSetter;
51
- import org.embulk.output.jdbc.setter.ColumnSetterFactory;
52
- import org.embulk.output.jdbc.setter.ColumnSetterVisitor;
53
- import org.embulk.spi.util.RetryExecutor.Retryable;
54
- import static org.embulk.spi.util.RetryExecutor.retryExecutor;
55
- import static org.embulk.output.jdbc.JdbcSchema.filterSkipColumns;
56
-
57
- public abstract class AbstractJdbcOutputPlugin
58
- implements OutputPlugin
59
- {
60
- private final static Set<String> loadedJarGlobs = new HashSet<String>();
61
-
62
- protected final Logger logger = Exec.getLogger(getClass());
63
-
64
- public interface PluginTask
65
- extends Task
66
- {
67
- @Config("options")
68
- @ConfigDefault("{}")
69
- public ToStringMap getOptions();
70
-
71
- @Config("table")
72
- public String getTable();
73
-
74
- @Config("mode")
75
- public Mode getMode();
76
-
77
- @Config("batch_size")
78
- @ConfigDefault("16777216")
79
- // TODO set minimum number
80
- public int getBatchSize();
81
-
82
- @Config("merge_keys")
83
- @ConfigDefault("null")
84
- public Optional<List<String>> getMergeKeys();
85
-
86
- @Config("column_options")
87
- @ConfigDefault("{}")
88
- public Map<String, JdbcColumnOption> getColumnOptions();
89
-
90
- @Config("default_timezone")
91
- @ConfigDefault("\"UTC\"")
92
- public DateTimeZone getDefaultTimeZone();
93
-
94
- public void setMergeKeys(Optional<List<String>> keys);
95
-
96
- public void setFeatures(Features features);
97
- public Features getFeatures();
98
-
99
- public JdbcSchema getTargetTableSchema();
100
- public void setTargetTableSchema(JdbcSchema schema);
101
-
102
- public Optional<List<String>> getIntermediateTables();
103
- public void setIntermediateTables(Optional<List<String>> names);
104
- }
105
-
106
- public static class Features
107
- {
108
- private int maxTableNameLength = 64;
109
- private Set<Mode> supportedModes = ImmutableSet.copyOf(Mode.values());
110
- private boolean ignoreMergeKeys = false;
111
-
112
- public Features()
113
- { }
114
-
115
- @JsonProperty
116
- public int getMaxTableNameLength()
117
- {
118
- return maxTableNameLength;
119
- }
120
-
121
- @JsonProperty
122
- public Features setMaxTableNameLength(int bytes)
123
- {
124
- this.maxTableNameLength = bytes;
125
- return this;
126
- }
127
-
128
- @JsonProperty
129
- public Set<Mode> getSupportedModes()
130
- {
131
- return supportedModes;
132
- }
133
-
134
- @JsonProperty
135
- public Features setSupportedModes(Set<Mode> modes)
136
- {
137
- this.supportedModes = modes;
138
- return this;
139
- }
140
-
141
- @JsonProperty
142
- public boolean getIgnoreMergeKeys()
143
- {
144
- return ignoreMergeKeys;
145
- }
146
-
147
- @JsonProperty
148
- public Features setIgnoreMergeKeys(boolean value)
149
- {
150
- this.ignoreMergeKeys = value;
151
- return this;
152
- }
153
- }
154
-
155
- protected void loadDriverJar(String glob)
156
- {
157
- synchronized (loadedJarGlobs) {
158
- if (!loadedJarGlobs.contains(glob)) {
159
- // TODO match glob
160
- PluginClassLoader loader = (PluginClassLoader) getClass().getClassLoader();
161
- loader.addPath(Paths.get(glob));
162
- loadedJarGlobs.add(glob);
163
- }
164
- }
165
- }
166
-
167
- // for subclasses to add @Config
168
- protected Class<? extends PluginTask> getTaskClass()
169
- {
170
- return PluginTask.class;
171
- }
172
-
173
- protected abstract Features getFeatures(PluginTask task);
174
-
175
- protected abstract JdbcOutputConnector getConnector(PluginTask task, boolean retryableMetadataOperation);
176
-
177
- protected abstract BatchInsert newBatchInsert(PluginTask task, Optional<List<String>> mergeKeys) throws IOException, SQLException;
178
-
179
- protected JdbcOutputConnection newConnection(PluginTask task, boolean retryableMetadataOperation,
180
- boolean autoCommit) throws SQLException
181
- {
182
- return getConnector(task, retryableMetadataOperation).connect(autoCommit);
183
- }
184
-
185
- public enum Mode {
186
- INSERT,
187
- INSERT_DIRECT,
188
- MERGE,
189
- MERGE_DIRECT,
190
- TRUNCATE_INSERT,
191
- REPLACE;
192
-
193
- @JsonValue
194
- @Override
195
- public String toString()
196
- {
197
- return name().toLowerCase(Locale.ENGLISH);
198
- }
199
-
200
- @JsonCreator
201
- public static Mode fromString(String value)
202
- {
203
- switch(value) {
204
- case "insert":
205
- return INSERT;
206
- case "insert_direct":
207
- return INSERT_DIRECT;
208
- case "merge":
209
- return MERGE;
210
- case "merge_direct":
211
- return MERGE_DIRECT;
212
- case "truncate_insert":
213
- return TRUNCATE_INSERT;
214
- case "replace":
215
- return REPLACE;
216
- default:
217
- throw new ConfigException(String.format("Unknown mode '%s'. Supported modes are insert, insert_direct, merge, merge_direct, truncate_insert, replace", value));
218
- }
219
- }
220
-
221
- /**
222
- * True if this mode directly modifies the target table without creating intermediate tables.
223
- */
224
- public boolean isDirectModify()
225
- {
226
- return this == INSERT_DIRECT || this == MERGE_DIRECT;
227
- }
228
-
229
- /**
230
- * True if this mode merges records on unique keys
231
- */
232
- public boolean isMerge()
233
- {
234
- return this == MERGE || this == MERGE_DIRECT;
235
- }
236
-
237
- /**
238
- * True if this mode creates intermediate table for each tasks.
239
- */
240
- public boolean tempTablePerTask()
241
- {
242
- return this == INSERT || this == MERGE || this == TRUNCATE_INSERT /*this == REPLACE_VIEW*/;
243
- }
244
-
245
- /**
246
- * True if this mode truncates the target table before committing intermediate tables
247
- */
248
- public boolean truncateBeforeCommit()
249
- {
250
- return this == TRUNCATE_INSERT;
251
- }
252
-
253
- /**
254
- * True if this mode uses MERGE statement to commit intermediate tables to the target table
255
- */
256
- public boolean commitByMerge()
257
- {
258
- return this == MERGE;
259
- }
260
-
261
- /**
262
- * True if this mode overwrites schema of the target tables
263
- */
264
- public boolean ignoreTargetTableSchema()
265
- {
266
- return this == REPLACE /*|| this == REPLACE_VIEW*/;
267
- }
268
-
269
- /**
270
- * True if this mode swaps the target tables with intermediate tables to commit
271
- */
272
- public boolean commitBySwapTable()
273
- {
274
- return this == REPLACE;
275
- }
276
- }
277
-
278
- public ConfigDiff transaction(ConfigSource config,
279
- Schema schema, int taskCount,
280
- OutputPlugin.Control control)
281
- {
282
- PluginTask task = config.loadConfig(getTaskClass());
283
- Features features = getFeatures(task);
284
- task.setFeatures(features);
285
-
286
- if (!features.getSupportedModes().contains(task.getMode())) {
287
- throw new ConfigException(String.format("This output type doesn't support '%s'. Supported modes are: %s", task.getMode(), features.getSupportedModes()));
288
- }
289
-
290
- task = begin(task, schema, taskCount);
291
- control.run(task.dump());
292
- return commit(task, schema, taskCount);
293
- }
294
-
295
- public ConfigDiff resume(TaskSource taskSource,
296
- Schema schema, int taskCount,
297
- OutputPlugin.Control control)
298
- {
299
- PluginTask task = taskSource.loadTask(getTaskClass());
300
-
301
- if (!task.getMode().tempTablePerTask()) {
302
- throw new UnsupportedOperationException("inplace mode is not resumable. You need to delete partially-loaded records from the database and restart the entire transaction.");
303
- }
304
-
305
- task = begin(task, schema, taskCount);
306
- control.run(task.dump());
307
- return commit(task, schema, taskCount);
308
- }
309
-
310
- protected String getTransactionUniqueName()
311
- {
312
- // TODO use uuid?
313
- Timestamp t = Exec.session().getTransactionTime();
314
- return String.format("%016x%08x", t.getEpochSecond(), t.getNano());
315
- }
316
-
317
- private PluginTask begin(final PluginTask task,
318
- final Schema schema, final int taskCount)
319
- {
320
- try {
321
- withRetry(new IdempotentSqlRunnable() { // no intermediate data if isDirectModify == true
322
- public void run() throws SQLException
323
- {
324
- JdbcOutputConnection con = newConnection(task, true, false);
325
- try {
326
- doBegin(con, task, schema, taskCount);
327
- } finally {
328
- con.close();
329
- }
330
- }
331
- });
332
- } catch (SQLException | InterruptedException ex) {
333
- throw new RuntimeException(ex);
334
- }
335
- return task;
336
- }
337
-
338
- private ConfigDiff commit(final PluginTask task,
339
- Schema schema, final int taskCount)
340
- {
341
- if (!task.getMode().isDirectModify()) { // no intermediate data if isDirectModify == true
342
- try {
343
- withRetry(new IdempotentSqlRunnable() {
344
- public void run() throws SQLException
345
- {
346
- JdbcOutputConnection con = newConnection(task, false, false);
347
- try {
348
- doCommit(con, task, taskCount);
349
- } finally {
350
- con.close();
351
- }
352
- }
353
- });
354
- } catch (SQLException | InterruptedException ex) {
355
- throw new RuntimeException(ex);
356
- }
357
- }
358
- return Exec.newConfigDiff();
359
- }
360
-
361
- public void cleanup(TaskSource taskSource,
362
- Schema schema, final int taskCount,
363
- final List<CommitReport> successCommitReports)
364
- {
365
- final PluginTask task = taskSource.loadTask(getTaskClass());
366
-
367
- if (!task.getMode().isDirectModify()) { // no intermediate data if isDirectModify == true
368
- try {
369
- withRetry(new IdempotentSqlRunnable() {
370
- public void run() throws SQLException
371
- {
372
- JdbcOutputConnection con = newConnection(task, true, true);
373
- try {
374
- doCleanup(con, task, taskCount, successCommitReports);
375
- } finally {
376
- con.close();
377
- }
378
- }
379
- });
380
- } catch (SQLException | InterruptedException ex) {
381
- throw new RuntimeException(ex);
382
- }
383
- }
384
- }
385
-
386
- protected void doBegin(JdbcOutputConnection con,
387
- PluginTask task, final Schema schema, int taskCount) throws SQLException
388
- {
389
- if (schema.getColumnCount() == 0) {
390
- throw new ConfigException("task count == 0 is not supported");
391
- }
392
-
393
- Mode mode = task.getMode();
394
- logger.info("Using {} mode", mode);
395
-
396
- Optional<JdbcSchema> initialTargetTableSchema =
397
- mode.ignoreTargetTableSchema() ?
398
- Optional.<JdbcSchema>absent() :
399
- newJdbcSchemaFromTableIfExists(con, task.getTable());
400
-
401
- // TODO get CREATE TABLE statement from task if set
402
- JdbcSchema newTableSchema = applyColumnOptionsToNewTableSchema(
403
- initialTargetTableSchema.or(new Supplier<JdbcSchema>() {
404
- public JdbcSchema get()
405
- {
406
- return newJdbcSchemaForNewTable(schema);
407
- }
408
- }),
409
- task.getColumnOptions());
410
-
411
- // create intermediate tables
412
- if (!mode.isDirectModify()) {
413
- // direct modify mode doesn't need intermediate tables.
414
- ImmutableList.Builder<String> intermTableNames = ImmutableList.builder();
415
- if (mode.tempTablePerTask()) {
416
- String namePrefix = generateIntermediateTableNamePrefix(task.getTable(), con, 3, task.getFeatures().getMaxTableNameLength());
417
- for (int i=0; i < taskCount; i++) {
418
- intermTableNames.add(namePrefix + String.format("%03d", i));
419
- }
420
- } else {
421
- String name = generateIntermediateTableNamePrefix(task.getTable(), con, 0, task.getFeatures().getMaxTableNameLength());
422
- intermTableNames.add(name);
423
- }
424
- // create the intermediate tables here
425
- task.setIntermediateTables(Optional.<List<String>>of(intermTableNames.build()));
426
- for (String name : task.getIntermediateTables().get()) {
427
- // DROP TABLE IF EXISTS xyz__0000000054d92dee1e452158_bulk_load_temp
428
- con.dropTableIfExists(name);
429
- // CREATE TABLE IF NOT EXISTS xyz__0000000054d92dee1e452158_bulk_load_temp
430
- con.createTableIfNotExists(name, newTableSchema);
431
- }
432
- } else {
433
- task.setIntermediateTables(Optional.<List<String>>absent());
434
- }
435
-
436
- // build JdbcSchema from a table
437
- JdbcSchema targetTableSchema;
438
- if (initialTargetTableSchema.isPresent()) {
439
- targetTableSchema = initialTargetTableSchema.get();
440
- } else if (task.getIntermediateTables().isPresent() && !task.getIntermediateTables().get().isEmpty()) {
441
- String firstItermTable = task.getIntermediateTables().get().get(0);
442
- targetTableSchema = newJdbcSchemaFromTableIfExists(con, firstItermTable).get();
443
- } else {
444
- // also create the target table if not exists
445
- // CREATE TABLE IF NOT EXISTS xyz
446
- con.createTableIfNotExists(task.getTable(), newTableSchema);
447
- targetTableSchema = newJdbcSchemaFromTableIfExists(con, task.getTable()).get();
448
- }
449
- task.setTargetTableSchema(matchSchemaByColumnNames(schema, targetTableSchema));
450
-
451
- // validate column_options
452
- newColumnSetters(
453
- new ColumnSetterFactory(null, task.getDefaultTimeZone()), // TODO create a dummy BatchInsert
454
- task.getTargetTableSchema(), schema,
455
- task.getColumnOptions());
456
-
457
- // normalize merge_key parameter for merge modes
458
- if (mode.isMerge()) {
459
- Optional<List<String>> mergeKeys = task.getMergeKeys();
460
- if (task.getFeatures().getIgnoreMergeKeys()) {
461
- if (mergeKeys.isPresent()) {
462
- throw new ConfigException("This output type does not accept 'merge_key' option.");
463
- }
464
- task.setMergeKeys(Optional.<List<String>>of(ImmutableList.<String>of()));
465
- } else if (mergeKeys.isPresent()) {
466
- if (task.getMergeKeys().get().isEmpty()) {
467
- throw new ConfigException("Empty 'merge_keys' option is invalid.");
468
- }
469
- for (String key : mergeKeys.get()) {
470
- if (!targetTableSchema.findColumn(key).isPresent()) {
471
- throw new ConfigException(String.format("Merge key '%s' does not exist in the target table.", key));
472
- }
473
- }
474
- } else {
475
- ImmutableList.Builder<String> builder = ImmutableList.builder();
476
- for (JdbcColumn column : targetTableSchema.getColumns()) {
477
- if (column.isUniqueKey()) {
478
- builder.add(column.getName());
479
- }
480
- }
481
- task.setMergeKeys(Optional.<List<String>>of(builder.build()));
482
- if (task.getMergeKeys().get().isEmpty()) {
483
- throw new ConfigException("Merging mode is used but the target table does not have primary keys. Please set merge_keys option.");
484
- }
485
- }
486
- logger.info("Using merge keys: {}", task.getMergeKeys().get());
487
- } else {
488
- task.setMergeKeys(Optional.<List<String>>absent());
489
- }
490
- }
491
-
492
- protected String generateIntermediateTableNamePrefix(String baseTableName, JdbcOutputConnection con, int suffixLength, int maxLength) throws SQLException
493
- {
494
- Charset tableNameCharset = con.getTableNameCharset();
495
- String tableName = baseTableName;
496
- String suffix = "_bl_tmp";
497
- String uniqueSuffix = getTransactionUniqueName() + suffix;
498
-
499
- // way to count length of table name varies by DBMSs (bytes or characters),
500
- // so truncate swap table name by one character.
501
- while (!checkTableNameLength(tableName + "_" + uniqueSuffix, tableNameCharset, suffixLength, maxLength)) {
502
- if (uniqueSuffix.length() > 8 + suffix.length()) {
503
- // truncate transaction unique name
504
- // (include 8 characters of the transaction name at least)
505
- uniqueSuffix = uniqueSuffix.substring(1);
506
- } else {
507
- if (tableName.isEmpty()) {
508
- throw new ConfigException("Table name is too long to generate temporary table name");
509
- }
510
- // truncate table name
511
- tableName = tableName.substring(0, tableName.length() - 1);
512
- //if (!connection.tableExists(tableName)) {
513
- // TODO this doesn't help. Rather than truncating more characters,
514
- // here needs to replace characters with random characters. But
515
- // to make the result deterministic. So, an idea is replacing
516
- // the last character to the first (second, third, ... for each loop)
517
- // of md5(original table name).
518
- //}
519
- }
520
-
521
- }
522
- return tableName + "_" + uniqueSuffix;
523
- }
524
-
525
- private static JdbcSchema applyColumnOptionsToNewTableSchema(JdbcSchema schema, final Map<String, JdbcColumnOption> columnOptions)
526
- {
527
- return new JdbcSchema(Lists.transform(schema.getColumns(), new Function<JdbcColumn, JdbcColumn>() {
528
- public JdbcColumn apply(JdbcColumn c)
529
- {
530
- JdbcColumnOption option = columnOptionOf(columnOptions, c);
531
- if (option.getType().isPresent()) {
532
- return JdbcColumn.newTypeDeclaredColumn(
533
- c.getName(), Types.OTHER, // sqlType, isNotNull, and isUniqueKey are ignored
534
- option.getType().get(), false, false);
535
- }
536
- return c;
537
- }
538
- }));
539
- }
540
-
541
- protected static List<ColumnSetter> newColumnSetters(ColumnSetterFactory factory,
542
- JdbcSchema targetTableSchema, Schema inputValueSchema,
543
- Map<String, JdbcColumnOption> columnOptions)
544
- {
545
- ImmutableList.Builder<ColumnSetter> builder = ImmutableList.builder();
546
- int schemaColumnIndex = 0;
547
- for (JdbcColumn targetColumn : targetTableSchema.getColumns()) {
548
- if (targetColumn.isSkipColumn()) {
549
- builder.add(factory.newSkipColumnSetter());
550
- } else {
551
- //String columnOptionKey = inputValueSchema.getColumn(schemaColumnIndex).getName();
552
- JdbcColumnOption option = columnOptionOf(columnOptions, targetColumn);
553
- builder.add(factory.newColumnSetter(targetColumn, option));
554
- schemaColumnIndex++;
555
- }
556
- }
557
- return builder.build();
558
- }
559
-
560
- private static JdbcColumnOption columnOptionOf(Map<String, JdbcColumnOption> columnOptions, JdbcColumn targetColumn)
561
- {
562
- return Optional.fromNullable(columnOptions.get(targetColumn.getName())).or(
563
- // default column option
564
- new Supplier<JdbcColumnOption>()
565
- {
566
- public JdbcColumnOption get()
567
- {
568
- return Exec.newConfigSource().loadConfig(JdbcColumnOption.class);
569
- }
570
- });
571
- }
572
-
573
- private boolean checkTableNameLength(String tableName, Charset tableNameCharset, int suffixLength, int maxLength)
574
- {
575
- return tableNameCharset.encode(tableName).remaining() + suffixLength <= maxLength;
576
- }
577
-
578
- protected void doCommit(JdbcOutputConnection con, PluginTask task, int taskCount)
579
- throws SQLException
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<CommitReport> successCommitReports)
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 CommitReport commit()
865
- {
866
- return Exec.newCommitReport();
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
- }
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
+ }