embulk-output-jdbc 0.2.4 → 0.3.0

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