embulk-output-jdbc 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
+ }