embulk-input-jdbc 0.7.2 → 0.7.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9a672095825353519696e26638bf7b750406a720
4
- data.tar.gz: f90651ce7041f7435cc07da9435dafe593b027cc
3
+ metadata.gz: bab76f99601161e312ae588c0f5d1c6ed7a5ca7c
4
+ data.tar.gz: 3394307ce36d292c46de24aafa14a79859cd6b13
5
5
  SHA512:
6
- metadata.gz: 5dbd93a8b413d1ff2b3d53d61c04706a1696cc8a544805c094d9fdef05a8bd11072a98b39edf602b08af9ab679684220bd6699df2d0963ded92d514c7d6e7b2e
7
- data.tar.gz: 9886411e1b6d7358669827291f53a4994b1a116123d724253c0f39ce9c5041413e8207b8b84ab02b4686c8775be6594f6e70a0e0d74a79190d013f06d0514b23
6
+ metadata.gz: ae4abde050d7ff724733096a16e0461a5d5b31d0877e0ab76ebe4b763c00507b9bca116ad19f7b27e9ce3870814b843589133f4aca0c2a530680594cc81261b4
7
+ data.tar.gz: a7b72823dd55164287445e38a2d3d5b2c18b76c638d1c8d8c38aa0d6cd5af987a55f818c43b8a3627c06653a3bbec6503855740a005fb97e6dafc647035cc2ca
@@ -59,7 +59,7 @@ public class JdbcInputPlugin
59
59
  synchronized (loadedJarGlobs) {
60
60
  String glob = t.getDriverPath().get();
61
61
  if (!loadedJarGlobs.contains(glob)) {
62
- loadDriverJar(glob);
62
+ addDriverJarToClasspath(glob);
63
63
  loadedJarGlobs.add(glob);
64
64
  }
65
65
  }
@@ -1,5 +1,6 @@
1
1
  package org.embulk.input.jdbc;
2
2
 
3
+ import java.nio.file.Path;
3
4
  import java.util.List;
4
5
  import java.util.Map;
5
6
  import java.nio.file.Paths;
@@ -8,10 +9,12 @@ import java.sql.SQLException;
8
9
 
9
10
  import org.slf4j.Logger;
10
11
 
12
+ import com.fasterxml.jackson.databind.JsonNode;
11
13
  import com.google.common.base.Optional;
12
14
  import com.google.common.base.Supplier;
13
15
  import com.google.common.base.Throwables;
14
16
  import com.google.common.collect.ImmutableList;
17
+ import com.google.common.collect.ImmutableMap;
15
18
 
16
19
  import org.embulk.config.Config;
17
20
  import org.embulk.config.ConfigException;
@@ -25,6 +28,7 @@ import org.embulk.config.TaskSource;
25
28
  import org.embulk.plugin.PluginClassLoader;
26
29
  import org.embulk.spi.BufferAllocator;
27
30
  import org.embulk.spi.Column;
31
+ import org.embulk.spi.DataException;
28
32
  import org.embulk.spi.PageBuilder;
29
33
  import org.embulk.spi.InputPlugin;
30
34
  import org.embulk.spi.PageOutput;
@@ -33,8 +37,11 @@ import org.embulk.spi.Exec;
33
37
  import org.embulk.input.jdbc.getter.ColumnGetter;
34
38
  import org.embulk.input.jdbc.getter.ColumnGetterFactory;
35
39
  import org.embulk.input.jdbc.JdbcInputConnection.BatchSelect;
40
+ import org.embulk.input.jdbc.JdbcInputConnection.PreparedQuery;
36
41
  import org.joda.time.DateTimeZone;
37
42
 
43
+ import static java.util.Locale.ENGLISH;
44
+
38
45
  public abstract class AbstractJdbcInputPlugin
39
46
  implements InputPlugin
40
47
  {
@@ -49,6 +56,7 @@ public abstract class AbstractJdbcInputPlugin
49
56
  @Config("table")
50
57
  @ConfigDefault("null")
51
58
  public Optional<String> getTable();
59
+ public void setTable(Optional<String> normalizedTableName);
52
60
 
53
61
  @Config("query")
54
62
  @ConfigDefault("null")
@@ -66,10 +74,18 @@ public abstract class AbstractJdbcInputPlugin
66
74
  @ConfigDefault("null")
67
75
  public Optional<String> getOrderBy();
68
76
 
69
- //// TODO See bellow.
70
- //@Config("last_value")
71
- //@ConfigDefault("null")
72
- //public Optional<String> getLastValue();
77
+ @Config("incremental")
78
+ @ConfigDefault("false")
79
+ public boolean getIncremental();
80
+
81
+ @Config("incremental_columns")
82
+ @ConfigDefault("[]")
83
+ public List<String> getIncrementalColumns();
84
+ public void setIncrementalColumns(List<String> indexes);
85
+
86
+ @Config("last_record")
87
+ @ConfigDefault("null")
88
+ public Optional<List<JsonNode>> getLastRecord();
73
89
 
74
90
  // TODO limit_value is necessary to make sure repeated bulk load transactions
75
91
  // don't a same record twice or miss records when the column
@@ -128,9 +144,15 @@ public abstract class AbstractJdbcInputPlugin
128
144
  @ConfigDefault("null")
129
145
  public Optional<String> getAfterSelect();
130
146
 
147
+ public PreparedQuery getBuiltQuery();
148
+ public void setBuiltQuery(PreparedQuery query);
149
+
131
150
  public JdbcSchema getQuerySchema();
132
151
  public void setQuerySchema(JdbcSchema schema);
133
152
 
153
+ public List<Integer> getIncrementalColumnIndexes();
154
+ public void setIncrementalColumnIndexes(List<Integer> indexes);
155
+
134
156
  @ConfigInject
135
157
  public BufferAllocator getBufferAllocator();
136
158
  }
@@ -149,12 +171,20 @@ public abstract class AbstractJdbcInputPlugin
149
171
  {
150
172
  PluginTask task = config.loadConfig(getTaskClass());
151
173
 
152
- //if (task.getLastValue().isPresent() && !task.getOrderBy().isPresent()) {
153
- // throw new ConfigException("order_by parameter must be set if last_value parameter is set");
154
- //}
174
+ if (task.getIncremental()) {
175
+ if (task.getOrderBy().isPresent()) {
176
+ throw new ConfigException("order_by option must not be set if incremental is true");
177
+ }
178
+ }
179
+ else {
180
+ if (!task.getIncrementalColumns().isEmpty()) {
181
+ throw new ConfigException("'incremental: true' must be set if incremental_columns is set");
182
+ }
183
+ }
155
184
 
156
185
  Schema schema;
157
186
  try (JdbcInputConnection con = newConnection(task)) {
187
+ // TODO incremental_columns is not set => get primary key
158
188
  schema = setupTask(con, task);
159
189
  } catch (SQLException ex) {
160
190
  throw Throwables.propagate(ex);
@@ -165,11 +195,60 @@ public abstract class AbstractJdbcInputPlugin
165
195
 
166
196
  private Schema setupTask(JdbcInputConnection con, PluginTask task) throws SQLException
167
197
  {
198
+ if (task.getTable().isPresent()) {
199
+ String actualTableName = normalizeTableNameCase(con, task.getTable().get());
200
+ task.setTable(Optional.of(actualTableName));
201
+ }
202
+
168
203
  // build SELECT query and gets schema of its result
169
204
  String query = getQuery(task, con);
170
- logger.info("SQL: " + query);
205
+
171
206
  JdbcSchema querySchema = con.getSchemaOfQuery(query);
172
207
  task.setQuerySchema(querySchema);
208
+ // query schema should not change after incremental query
209
+
210
+ PreparedQuery preparedQuery;
211
+ if (task.getIncremental()) {
212
+ // build incremental query
213
+
214
+ List<String> incrementalColumns = task.getIncrementalColumns();
215
+ if (incrementalColumns.isEmpty()) {
216
+ // incremental_columns is not set
217
+ if (!task.getTable().isPresent()) {
218
+ throw new ConfigException("incremental_columns option must be set if incremental is true and custom query option is set");
219
+ }
220
+ // get primary keys from the target table to use them as incremental_columns
221
+ List<String> primaryKeys = con.getPrimaryKeys(task.getTable().get());
222
+ if (primaryKeys.isEmpty()) {
223
+ throw new ConfigException(String.format(ENGLISH,
224
+ "Primary key is not available at the table '%s'. incremental_columns option must be set",
225
+ task.getTable().get()));
226
+ }
227
+ logger.info("Using primary keys as incremental_columns: {}", primaryKeys);
228
+ task.setIncrementalColumns(primaryKeys);
229
+ incrementalColumns = primaryKeys;
230
+ }
231
+
232
+ List<Integer> incrementalColumnIndexes = findIncrementalColumnIndexes(querySchema, incrementalColumns);
233
+ task.setIncrementalColumnIndexes(incrementalColumnIndexes);
234
+
235
+ if (task.getLastRecord().isPresent()) {
236
+ List<JsonNode> lastRecord = task.getLastRecord().get();
237
+ if (lastRecord.size() != incrementalColumnIndexes.size()) {
238
+ throw new ConfigException("Number of values set at last_record must be same with number of columns set at incremental_columns");
239
+ }
240
+ preparedQuery = con.buildIncrementalQuery(query, querySchema, incrementalColumnIndexes, lastRecord);
241
+ }
242
+ else {
243
+ preparedQuery = con.buildIncrementalQuery(query, querySchema, incrementalColumnIndexes, null);
244
+ }
245
+ }
246
+ else {
247
+ task.setIncrementalColumnIndexes(ImmutableList.<Integer>of());
248
+ preparedQuery = new PreparedQuery(query, ImmutableList.<JdbcLiteral>of());
249
+ }
250
+
251
+ task.setBuiltQuery(preparedQuery);
173
252
 
174
253
  // validate column_options
175
254
  newColumnGetters(task, querySchema, null);
@@ -186,19 +265,63 @@ public abstract class AbstractJdbcInputPlugin
186
265
  return new Schema(columns.build());
187
266
  }
188
267
 
268
+ private String normalizeTableNameCase(JdbcInputConnection con, String tableName)
269
+ throws SQLException
270
+ {
271
+ if (con.tableExists(tableName)) {
272
+ return tableName;
273
+ } else {
274
+ String upperTableName = tableName.toUpperCase();
275
+ String lowerTableName = tableName.toLowerCase();
276
+ boolean upperExists = con.tableExists(upperTableName);
277
+ boolean lowerExists = con.tableExists(upperTableName);
278
+ if (upperExists && lowerExists) {
279
+ throw new ConfigException(String.format("Cannot specify table '%s' because both '%s' and '%s' exist.",
280
+ tableName, upperTableName, lowerTableName));
281
+ } else if (upperExists) {
282
+ return upperTableName;
283
+ } else if (lowerExists) {
284
+ return lowerTableName;
285
+ } else {
286
+ // fallback to the given table name. this may throw error later at getSchemaOfQuery
287
+ return tableName;
288
+ }
289
+ }
290
+ }
291
+
292
+ private List<Integer> findIncrementalColumnIndexes(JdbcSchema schema, List<String> incrementalColumns)
293
+ throws SQLException
294
+ {
295
+ ImmutableList.Builder<Integer> builder = ImmutableList.builder();
296
+ for (String name : incrementalColumns) {
297
+ Optional<Integer> index = schema.findColumn(name);
298
+ if (index.isPresent()) {
299
+ builder.add(index.get());
300
+ }
301
+ else {
302
+ throw new ConfigException(String.format(ENGLISH,
303
+ "Column name '%s' is in incremental_columns option does not exist",
304
+ name));
305
+ }
306
+ }
307
+ return builder.build();
308
+ }
309
+
189
310
  private String getQuery(PluginTask task, JdbcInputConnection con) throws SQLException
190
311
  {
191
312
  if (task.getQuery().isPresent()) {
192
313
  if (task.getTable().isPresent() || task.getSelect().isPresent() ||
193
314
  task.getWhere().isPresent() || task.getOrderBy().isPresent()) {
194
315
  throw new ConfigException("'table', 'select', 'where' and 'order_by' parameters are unnecessary if 'query' parameter is set.");
316
+ } else if (!task.getIncrementalColumns().isEmpty() || task.getLastRecord().isPresent()) {
317
+ throw new ConfigException("'incremental_columns' and 'last_record' parameters are not supported if 'query' parameter is set.");
195
318
  }
196
319
  return task.getQuery().get();
197
320
  } else if (task.getTable().isPresent()) {
198
321
  return con.buildSelectQuery(task.getTable().get(), task.getSelect(),
199
322
  task.getWhere(), task.getOrderBy());
200
323
  } else {
201
- throw new ConfigException("'table' parameter is required (if 'query' parameter is not set)");
324
+ throw new ConfigException("'table' or 'query' parameter is required");
202
325
  }
203
326
  }
204
327
 
@@ -224,12 +347,11 @@ public abstract class AbstractJdbcInputPlugin
224
347
  protected ConfigDiff buildNextConfigDiff(PluginTask task, List<TaskReport> reports)
225
348
  {
226
349
  ConfigDiff next = Exec.newConfigDiff();
227
- // TODO
228
- //if (task.getOrderBy().isPresent()) {
229
- // // TODO when parallel execution is implemented, calculate the max last_value
230
- // // from the all commit reports.
231
- // next.set("last_value", reports.get(0).get(JsonNode.class, "last_value"));
232
- //}
350
+ if (reports.size() > 0 && reports.get(0).has("last_record")) {
351
+ next.set("last_record", reports.get(0).get(JsonNode.class, "last_record"));
352
+ } else if (task.getLastRecord().isPresent()) {
353
+ next.set("last_record", task.getLastRecord().get());
354
+ }
233
355
  return next;
234
356
  }
235
357
 
@@ -241,6 +363,42 @@ public abstract class AbstractJdbcInputPlugin
241
363
  // do nothing
242
364
  }
243
365
 
366
+ private static class LastRecordStore
367
+ {
368
+ private final List<Integer> columnIndexes;
369
+ private final JsonNode[] lastValues;
370
+ private final List<String> columnNames;
371
+
372
+ public LastRecordStore(List<Integer> columnIndexes, List<String> columnNames)
373
+ {
374
+ this.columnIndexes = columnIndexes;
375
+ this.lastValues = new JsonNode[columnIndexes.size()];
376
+ this.columnNames = columnNames;
377
+ }
378
+
379
+ public void accept(List<ColumnGetter> getters)
380
+ throws SQLException
381
+ {
382
+ for (int i = 0; i < columnIndexes.size(); i++) {
383
+ lastValues[i] = getters.get(columnIndexes.get(i)).encodeToJson();
384
+ }
385
+ }
386
+
387
+ public List<JsonNode> getList()
388
+ {
389
+ ImmutableList.Builder<JsonNode> builder = ImmutableList.builder();
390
+ for (int i = 0; i < lastValues.length; i++) {
391
+ if (lastValues[i] == null || lastValues[i].isNull()) {
392
+ throw new DataException(String.format(ENGLISH,
393
+ "incremental_columns can't include null values but the last row is null at column '%s'",
394
+ columnNames.get(i)));
395
+ }
396
+ builder.add(lastValues[i]);
397
+ }
398
+ return builder.build();
399
+ }
400
+ }
401
+
244
402
  @Override
245
403
  public TaskReport run(TaskSource taskSource,
246
404
  Schema schema, int taskIndex,
@@ -248,22 +406,32 @@ public abstract class AbstractJdbcInputPlugin
248
406
  {
249
407
  PluginTask task = taskSource.loadTask(getTaskClass());
250
408
 
409
+ PreparedQuery builtQuery = task.getBuiltQuery();
251
410
  JdbcSchema querySchema = task.getQuerySchema();
252
411
  BufferAllocator allocator = task.getBufferAllocator();
253
412
  PageBuilder pageBuilder = new PageBuilder(allocator, schema, output);
254
413
 
414
+ long totalRows = 0;
415
+
416
+ LastRecordStore lastRecordStore = null;
417
+
255
418
  try (JdbcInputConnection con = newConnection(task)) {
256
- try (BatchSelect cursor = con.newSelectCursor(getQuery(task, con), task.getFetchRows(), task.getSocketTimeout())) {
257
- List<ColumnGetter> getters = newColumnGetters(task, querySchema, pageBuilder);
419
+ List<ColumnGetter> getters = newColumnGetters(task, querySchema, pageBuilder);
420
+ try (BatchSelect cursor = con.newSelectCursor(builtQuery, getters, task.getFetchRows(), task.getSocketTimeout())) {
258
421
  while (true) {
259
- // TODO run fetch() in another thread asynchronously
260
- // TODO retry fetch() if it failed (maybe order_by is required and unique_column(s) option is also required)
261
- boolean cont = fetch(cursor, getters, pageBuilder);
262
- if (!cont) {
422
+ long rows = fetch(cursor, getters, pageBuilder);
423
+ if (rows <= 0L) {
263
424
  break;
264
425
  }
426
+ totalRows += rows;
265
427
  }
266
428
  }
429
+
430
+ if (task.getIncremental() && totalRows > 0) {
431
+ lastRecordStore = new LastRecordStore(task.getIncrementalColumnIndexes(), task.getIncrementalColumns());
432
+ lastRecordStore.accept(getters);
433
+ }
434
+
267
435
  pageBuilder.finish();
268
436
 
269
437
  // after_select runs after pageBuilder.finish because pageBuilder.finish may fail.
@@ -284,10 +452,10 @@ public abstract class AbstractJdbcInputPlugin
284
452
  }
285
453
 
286
454
  TaskReport report = Exec.newTaskReport();
287
- // TODO
288
- //if (orderByColumn != null) {
289
- // report.set("last_value", lastValue);
290
- //}
455
+ if (lastRecordStore != null) {
456
+ report.set("last_record", lastRecordStore.getList());
457
+ }
458
+
291
459
  return report;
292
460
  }
293
461
 
@@ -339,12 +507,12 @@ public abstract class AbstractJdbcInputPlugin
339
507
  });
340
508
  }
341
509
 
342
- private boolean fetch(BatchSelect cursor,
510
+ private long fetch(BatchSelect cursor,
343
511
  List<ColumnGetter> getters, PageBuilder pageBuilder) throws SQLException
344
512
  {
345
513
  ResultSet result = cursor.fetch();
346
514
  if (result == null || !result.next()) {
347
- return false;
515
+ return 0;
348
516
  }
349
517
 
350
518
  List<Column> columns = pageBuilder.getSchema().getColumns();
@@ -362,7 +530,8 @@ public abstract class AbstractJdbcInputPlugin
362
530
  reportRows *= 2;
363
531
  }
364
532
  } while (result.next());
365
- return true;
533
+
534
+ return rows;
366
535
  }
367
536
 
368
537
  //// TODO move to embulk.spi.util?
@@ -402,10 +571,14 @@ public abstract class AbstractJdbcInputPlugin
402
571
  // }
403
572
  //}
404
573
 
405
- protected void loadDriverJar(String glob)
574
+ protected void addDriverJarToClasspath(String glob)
406
575
  {
407
576
  // TODO match glob
408
577
  PluginClassLoader loader = (PluginClassLoader) getClass().getClassLoader();
578
+ Path path = Paths.get(glob);
579
+ if (!path.toFile().exists()) {
580
+ throw new ConfigException("The specified driver jar doesn't exist: " + glob);
581
+ }
409
582
  loader.addPath(Paths.get(glob));
410
583
  }
411
584
  }
@@ -44,7 +44,6 @@ public class JdbcColumn
44
44
  return sqlType;
45
45
  }
46
46
 
47
-
48
47
  @JsonProperty("precision")
49
48
  public int getPrecision()
50
49
  {
@@ -11,12 +11,19 @@ import java.util.Set;
11
11
 
12
12
  import org.embulk.config.ConfigException;
13
13
  import org.embulk.spi.Exec;
14
+ import org.embulk.input.jdbc.getter.ColumnGetter;
14
15
  import org.slf4j.Logger;
15
16
 
17
+ import java.util.List;
18
+ import java.util.ArrayList;
19
+ import static java.util.Locale.ENGLISH;
20
+
21
+ import com.fasterxml.jackson.annotation.JsonCreator;
22
+ import com.fasterxml.jackson.annotation.JsonProperty;
23
+ import com.fasterxml.jackson.databind.JsonNode;
16
24
  import com.google.common.base.Optional;
17
25
  import com.google.common.collect.ImmutableList;
18
26
  import com.google.common.collect.ImmutableSet;
19
- import com.google.common.collect.ImmutableSet.Builder;
20
27
 
21
28
  public class JdbcInputConnection
22
29
  implements AutoCloseable
@@ -57,6 +64,20 @@ public class JdbcInputConnection
57
64
  }
58
65
  }
59
66
 
67
+ public List<String> getPrimaryKeys(String tableName) throws SQLException
68
+ {
69
+ ResultSet rs = databaseMetaData.getPrimaryKeys(null, schemaName, tableName);
70
+ ImmutableList.Builder<String> builder = ImmutableList.builder();
71
+ try {
72
+ while(rs.next()) {
73
+ builder.add(rs.getString("COLUMN_NAME"));
74
+ }
75
+ } finally {
76
+ rs.close();
77
+ }
78
+ return builder.build();
79
+ }
80
+
60
81
  protected JdbcSchema getSchemaOfResultMetadata(ResultSetMetaData metadata) throws SQLException
61
82
  {
62
83
  ImmutableList.Builder<JdbcColumn> columns = ImmutableList.builder();
@@ -72,19 +93,70 @@ public class JdbcInputConnection
72
93
  return new JdbcSchema(columns.build());
73
94
  }
74
95
 
75
- public BatchSelect newSelectCursor(String query, int fetchRows, int queryTimeout) throws SQLException
96
+ public static class PreparedQuery
76
97
  {
77
- return newBatchSelect(query, fetchRows, queryTimeout);
98
+ private final String query;
99
+ private final List<JdbcLiteral> parameters;
100
+
101
+ @JsonCreator
102
+ public PreparedQuery(
103
+ @JsonProperty("query") String query,
104
+ @JsonProperty("parameters") List<JdbcLiteral> parameters)
105
+ {
106
+ this.query = query;
107
+ this.parameters = parameters;
108
+ }
109
+
110
+ @JsonProperty("query")
111
+ public String getQuery()
112
+ {
113
+ return query;
114
+ }
115
+
116
+ @JsonProperty("parameters")
117
+ public List<JdbcLiteral> getParameters()
118
+ {
119
+ return parameters;
120
+ }
78
121
  }
79
122
 
80
- protected BatchSelect newBatchSelect(String query, int fetchRows, int queryTimeout) throws SQLException
123
+ public BatchSelect newSelectCursor(PreparedQuery preparedQuery,
124
+ List<ColumnGetter> getters,
125
+ int fetchRows, int queryTimeout) throws SQLException
81
126
  {
127
+ return newBatchSelect(preparedQuery, getters, fetchRows, queryTimeout);
128
+ }
129
+
130
+ protected BatchSelect newBatchSelect(PreparedQuery preparedQuery,
131
+ List<ColumnGetter> getters,
132
+ int fetchRows, int queryTimeout) throws SQLException
133
+ {
134
+ String query = preparedQuery.getQuery();
135
+ List<JdbcLiteral> params = preparedQuery.getParameters();
136
+
82
137
  PreparedStatement stmt = connection.prepareStatement(query);
83
138
  stmt.setFetchSize(fetchRows);
84
139
  stmt.setQueryTimeout(queryTimeout);
140
+ logger.info("SQL: " + query);
141
+ if (!params.isEmpty()) {
142
+ logger.info("Parameters: {}", params);
143
+ prepareParameters(stmt, getters, params);
144
+ }
85
145
  return new SingleSelect(stmt);
86
146
  }
87
147
 
148
+ protected void prepareParameters(PreparedStatement stmt, List<ColumnGetter> getters,
149
+ List<JdbcLiteral> parameters)
150
+ throws SQLException
151
+ {
152
+ for (int i = 0; i < parameters.size(); i++) {
153
+ JdbcLiteral literal = parameters.get(i);
154
+ ColumnGetter getter = getters.get(literal.getColumnIndex());
155
+ int index = i + 1; // JDBC column index begins from 1
156
+ getter.decodeFromJsonTo(stmt, index, literal.getValue());
157
+ }
158
+ }
159
+
88
160
  public interface BatchSelect
89
161
  extends AutoCloseable
90
162
  {
@@ -156,72 +228,87 @@ public class JdbcInputConnection
156
228
  }
157
229
 
158
230
  public String buildSelectQuery(String tableName,
159
- Optional<String> selectColumnList, Optional<String> whereCondition,
160
- Optional<String> orderByColumn) throws SQLException
161
- {
162
- String actualTableName;
163
- if (tableExists(tableName)) {
164
- actualTableName = tableName;
165
- } else {
166
- String upperTableName = tableName.toUpperCase();
167
- String lowerTableName = tableName.toLowerCase();
168
- if (tableExists(upperTableName)) {
169
- if (tableExists(lowerTableName)) {
170
- throw new ConfigException(String.format("Cannot specify table '%s' because both '%s' and '%s' exist.",
171
- tableName, upperTableName, lowerTableName));
172
- } else {
173
- actualTableName = upperTableName;
174
- }
175
- } else {
176
- if (tableExists(lowerTableName)) {
177
- actualTableName = lowerTableName;
178
- } else {
179
- actualTableName = tableName;
180
- }
181
- }
182
- }
183
-
231
+ Optional<String> selectExpression, Optional<String> whereCondition,
232
+ Optional<String> orderByExpression) throws SQLException
233
+ {
184
234
  StringBuilder sb = new StringBuilder();
185
235
 
186
236
  sb.append("SELECT ");
187
- sb.append(selectColumnList.or("*"));
188
- sb.append(" FROM ").append(buildTableName(actualTableName));
237
+ sb.append(selectExpression.or("*"));
238
+ sb.append(" FROM ").append(buildTableName(tableName));
189
239
 
190
240
  if (whereCondition.isPresent()) {
191
241
  sb.append(" WHERE ").append(whereCondition.get());
192
242
  }
193
243
 
194
- if (orderByColumn.isPresent()) {
195
- String actualOrderByColumn;
196
- Set<String> columnNames = getColumnNames(actualTableName);
197
- if (columnNames.contains(orderByColumn.get())) {
198
- actualOrderByColumn = orderByColumn.get();
199
- } else {
200
- String upperOrderByColumn = orderByColumn.get().toUpperCase();
201
- String lowerOrderByColumn = orderByColumn.get().toLowerCase();
202
- if (columnNames.contains(upperOrderByColumn)) {
203
- if (columnNames.contains(lowerOrderByColumn)) {
204
- throw new ConfigException(String.format("Cannot specify order-by colum '%s' because both '%s' and '%s' exist.",
205
- orderByColumn.get(), upperOrderByColumn, lowerOrderByColumn));
206
- } else {
207
- actualOrderByColumn = upperOrderByColumn;
208
- }
209
- } else {
210
- if (columnNames.contains(lowerOrderByColumn)) {
211
- actualOrderByColumn = lowerOrderByColumn;
212
- } else {
213
- actualOrderByColumn = orderByColumn.get();
214
- }
244
+ if (orderByExpression.isPresent()) {
245
+ sb.append(" ORDER BY ").append(orderByExpression.get());
246
+ }
247
+
248
+ return sb.toString();
249
+ }
250
+
251
+ public PreparedQuery buildIncrementalQuery(String rawQuery, JdbcSchema querySchema,
252
+ List<Integer> incrementalColumnIndexes, List<JsonNode> incrementalValues) throws SQLException
253
+ {
254
+ StringBuilder sb = new StringBuilder();
255
+ ImmutableList.Builder<JdbcLiteral> parameters = ImmutableList.builder();
256
+
257
+ sb.append("SELECT * FROM (");
258
+ sb.append(truncateStatementDelimiter(rawQuery));
259
+ sb.append(") embulk_incremental_");
260
+ if (incrementalValues != null) {
261
+ sb.append(" WHERE ");
262
+
263
+ List<String> leftColumnNames = new ArrayList<>();
264
+ List<JdbcLiteral> rightLiterals = new ArrayList<>();
265
+ for (int n = 0; n < incrementalColumnIndexes.size(); n++) {
266
+ int columnIndex = incrementalColumnIndexes.get(n);
267
+ JsonNode value = incrementalValues.get(n);
268
+ leftColumnNames.add(querySchema.getColumnName(columnIndex));
269
+ rightLiterals.add(new JdbcLiteral(columnIndex, value));
270
+ }
271
+
272
+ for (int n = 0; n < leftColumnNames.size(); n++) {
273
+ if (n > 0) {
274
+ sb.append(" OR ");
215
275
  }
276
+ sb.append("(");
277
+
278
+ for (int i = 0; i < n; i++) {
279
+ sb.append(quoteIdentifierString(leftColumnNames.get(i)));
280
+ sb.append(" = ?");
281
+ parameters.add(rightLiterals.get(i));
282
+ sb.append(" AND ");
283
+ }
284
+ sb.append(quoteIdentifierString(leftColumnNames.get(n)));
285
+ sb.append(" > ?");
286
+ parameters.add(rightLiterals.get(n));
287
+
288
+ sb.append(")");
216
289
  }
290
+ }
291
+ sb.append(" ORDER BY ");
217
292
 
218
- sb.append("ORDER BY ").append(quoteIdentifierString(actualOrderByColumn)).append(" ASC");
293
+ boolean first = true;
294
+ for (int i : incrementalColumnIndexes) {
295
+ if (first) {
296
+ first = false;
297
+ } else {
298
+ sb.append(", ");
299
+ }
300
+ sb.append(quoteIdentifierString(querySchema.getColumnName(i)));
219
301
  }
220
302
 
221
- return sb.toString();
303
+ return new PreparedQuery(sb.toString(), parameters.build());
304
+ }
305
+
306
+ protected String truncateStatementDelimiter(String rawQuery) throws SQLException
307
+ {
308
+ return rawQuery.replaceAll(";\\s*$", "");
222
309
  }
223
310
 
224
- private boolean tableExists(String tableName) throws SQLException
311
+ public boolean tableExists(String tableName) throws SQLException
225
312
  {
226
313
  try (ResultSet rs = connection.getMetaData().getTables(null, schemaName, tableName, null)) {
227
314
  return rs.next();
@@ -230,7 +317,7 @@ public class JdbcInputConnection
230
317
 
231
318
  private Set<String> getColumnNames(String tableName) throws SQLException
232
319
  {
233
- Builder<String> columnNamesBuilder = ImmutableSet.builder();
320
+ ImmutableSet.Builder<String> columnNamesBuilder = ImmutableSet.builder();
234
321
  try (ResultSet rs = connection.getMetaData().getColumns(null, schemaName, tableName, null)) {
235
322
  while (rs.next()) {
236
323
  columnNamesBuilder.add(rs.getString("COLUMN_NAME"));
@@ -0,0 +1,38 @@
1
+ package org.embulk.input.jdbc;
2
+
3
+ import com.fasterxml.jackson.annotation.JsonCreator;
4
+ import com.fasterxml.jackson.annotation.JsonProperty;
5
+ import com.fasterxml.jackson.databind.JsonNode;
6
+
7
+ public class JdbcLiteral
8
+ {
9
+ private final int columnIndex;
10
+ private final JsonNode value;
11
+
12
+ @JsonCreator
13
+ public JdbcLiteral(
14
+ @JsonProperty("columnIndex") int columnIndex,
15
+ @JsonProperty("value") JsonNode value)
16
+ {
17
+ this.columnIndex = columnIndex;
18
+ this.value = value;
19
+ }
20
+
21
+ @JsonProperty("columnIndex")
22
+ public int getColumnIndex()
23
+ {
24
+ return columnIndex;
25
+ }
26
+
27
+ @JsonProperty("value")
28
+ public JsonNode getValue()
29
+ {
30
+ return value;
31
+ }
32
+
33
+ @Override
34
+ public String toString()
35
+ {
36
+ return value.toString();
37
+ }
38
+ }
@@ -1,6 +1,7 @@
1
1
  package org.embulk.input.jdbc;
2
2
 
3
3
  import java.util.List;
4
+ import com.google.common.base.Optional;
4
5
  import com.fasterxml.jackson.annotation.JsonCreator;
5
6
  import com.fasterxml.jackson.annotation.JsonValue;
6
7
 
@@ -34,4 +35,21 @@ public class JdbcSchema
34
35
  {
35
36
  return columns.get(i).getName();
36
37
  }
38
+
39
+ public Optional<Integer> findColumn(String caseInsensitiveName)
40
+ {
41
+ // find by case sensitive first
42
+ for (int i = 0; i < columns.size(); i++) {
43
+ if (getColumn(i).getName().equals(caseInsensitiveName)) {
44
+ return Optional.of(i);
45
+ }
46
+ }
47
+ // find by case insensitive
48
+ for (int i = 0; i < columns.size(); i++) {
49
+ if (getColumn(i).getName().equalsIgnoreCase(caseInsensitiveName)) {
50
+ return Optional.of(i);
51
+ }
52
+ }
53
+ return Optional.absent();
54
+ }
37
55
  }
@@ -1,14 +1,21 @@
1
1
  package org.embulk.input.jdbc.getter;
2
2
 
3
3
  import java.sql.ResultSet;
4
+ import java.sql.PreparedStatement;
4
5
  import java.sql.SQLException;
6
+ import com.fasterxml.jackson.databind.JsonNode;
7
+ import com.fasterxml.jackson.databind.node.JsonNodeFactory;
5
8
  import org.embulk.spi.Column;
6
9
  import org.embulk.spi.ColumnVisitor;
7
10
  import org.embulk.spi.PageBuilder;
8
11
  import org.embulk.spi.type.Type;
12
+ import org.embulk.spi.DataException;
13
+ import static java.util.Locale.ENGLISH;
9
14
 
10
15
  public abstract class AbstractColumnGetter implements ColumnGetter, ColumnVisitor
11
16
  {
17
+ protected static final JsonNodeFactory jsonNodeFactory = JsonNodeFactory.instance;
18
+
12
19
  protected final PageBuilder to;
13
20
  private final Type toType;
14
21
 
@@ -20,8 +27,8 @@ public abstract class AbstractColumnGetter implements ColumnGetter, ColumnVisito
20
27
 
21
28
  @Override
22
29
  public void getAndSet(ResultSet from, int fromIndex,
23
- Column toColumn) throws SQLException {
24
-
30
+ Column toColumn) throws SQLException
31
+ {
25
32
  fetch(from, fromIndex);
26
33
  if (from.wasNull()) {
27
34
  to.setNull(toColumn);
@@ -79,4 +86,20 @@ public abstract class AbstractColumnGetter implements ColumnGetter, ColumnVisito
79
86
 
80
87
  protected abstract Type getDefaultToType();
81
88
 
89
+ @Override
90
+ public JsonNode encodeToJson()
91
+ {
92
+ throw new DataException(String.format(ENGLISH,
93
+ "Column type '%s' set at incremental_columns option is not supported",
94
+ getToType()));
95
+ }
96
+
97
+ @Override
98
+ public void decodeFromJsonTo(PreparedStatement toStatement, int toIndex, JsonNode fromValue)
99
+ throws SQLException
100
+ {
101
+ throw new DataException(String.format(ENGLISH,
102
+ "Converting last_record value %s to column index %d is not supported",
103
+ fromValue.toString(), toIndex));
104
+ }
82
105
  }
@@ -2,6 +2,8 @@ package org.embulk.input.jdbc.getter;
2
2
 
3
3
  import java.sql.ResultSet;
4
4
  import java.sql.SQLException;
5
+ import java.sql.PreparedStatement;
6
+ import com.fasterxml.jackson.databind.JsonNode;
5
7
  import org.embulk.spi.Column;
6
8
  import org.embulk.spi.type.Type;
7
9
 
@@ -11,4 +13,9 @@ public interface ColumnGetter
11
13
  Column toColumn) throws SQLException;
12
14
 
13
15
  public Type getToType();
16
+
17
+ public JsonNode encodeToJson();
18
+
19
+ public void decodeFromJsonTo(PreparedStatement toStatement, int toIndex, JsonNode fromValue)
20
+ throws SQLException;
14
21
  }
@@ -14,6 +14,8 @@ import org.embulk.spi.type.TimestampType;
14
14
  import org.embulk.spi.type.Type;
15
15
  import org.joda.time.DateTimeZone;
16
16
 
17
+ import static java.util.Locale.ENGLISH;
18
+
17
19
  public class ColumnGetterFactory
18
20
  {
19
21
  protected final PageBuilder to;
@@ -58,7 +60,8 @@ public class ColumnGetterFactory
58
60
  case "decimal":
59
61
  return new BigDecimalColumnGetter(to, toType);
60
62
  default:
61
- throw new ConfigException(String.format("Unknown value_type '%s' for column '%s'", option.getValueType(), column.getName()));
63
+ throw new ConfigException(String.format(ENGLISH,
64
+ "Unknown value_type '%s' for column '%s'", option.getValueType(), column.getName()));
62
65
  }
63
66
  }
64
67
 
@@ -185,7 +188,8 @@ public class ColumnGetterFactory
185
188
  private static UnsupportedOperationException unsupportedOperationException(JdbcColumn column)
186
189
  {
187
190
  throw new UnsupportedOperationException(
188
- String.format("Unsupported type %s (sqlType=%d) of '%s' column. Please exclude the column from 'select:' option.",
189
- column.getTypeName(), column.getSqlType(), column.getName()));
191
+ String.format(ENGLISH,
192
+ "Unsupported type %s (sqlType=%d) of '%s' column. Please add '%s: {type: string}' to 'column_options: {...}' option to convert the values to strings, or exclude the column from 'select:' option",
193
+ column.getTypeName(), column.getSqlType(), column.getName(), column.getName()));
190
194
  }
191
195
  }
@@ -1,7 +1,9 @@
1
1
  package org.embulk.input.jdbc.getter;
2
2
 
3
3
  import java.sql.ResultSet;
4
+ import java.sql.PreparedStatement;
4
5
  import java.sql.SQLException;
6
+ import com.fasterxml.jackson.databind.JsonNode;
5
7
  import org.embulk.spi.Column;
6
8
  import org.embulk.spi.PageBuilder;
7
9
  import org.embulk.spi.type.Type;
@@ -53,4 +55,16 @@ public class LongColumnGetter
53
55
  to.setString(column, Long.toString(value));
54
56
  }
55
57
 
58
+ @Override
59
+ public JsonNode encodeToJson()
60
+ {
61
+ return jsonNodeFactory.numberNode(value);
62
+ }
63
+
64
+ @Override
65
+ public void decodeFromJsonTo(PreparedStatement toStatement, int toIndex, JsonNode fromValue)
66
+ throws SQLException
67
+ {
68
+ toStatement.setLong(toIndex, fromValue.asLong());
69
+ }
56
70
  }
@@ -1,7 +1,9 @@
1
1
  package org.embulk.input.jdbc.getter;
2
2
 
3
3
  import java.sql.ResultSet;
4
+ import java.sql.PreparedStatement;
4
5
  import java.sql.SQLException;
6
+ import com.fasterxml.jackson.databind.JsonNode;
5
7
  import org.embulk.spi.Column;
6
8
  import org.embulk.spi.PageBuilder;
7
9
  import org.embulk.spi.json.JsonParseException;
@@ -79,4 +81,16 @@ public class StringColumnGetter
79
81
  to.setString(column, value);
80
82
  }
81
83
 
84
+ @Override
85
+ public JsonNode encodeToJson()
86
+ {
87
+ return jsonNodeFactory.textNode(value);
88
+ }
89
+
90
+ @Override
91
+ public void decodeFromJsonTo(PreparedStatement toStatement, int toIndex, JsonNode fromValue)
92
+ throws SQLException
93
+ {
94
+ toStatement.setString(toIndex, fromValue.asText());
95
+ }
82
96
  }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: embulk-input-jdbc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.7.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sadayuki Furuhashi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-06-24 00:00:00.000000000 Z
11
+ date: 2016-08-26 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Selects records from a table.
14
14
  email:
@@ -18,12 +18,14 @@ extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
20
  - build.gradle
21
+ - classpath/embulk-input-jdbc-0.7.3.jar
21
22
  - lib/embulk/input/jdbc.rb
22
23
  - src/main/java/org/embulk/input/JdbcInputPlugin.java
23
24
  - src/main/java/org/embulk/input/jdbc/AbstractJdbcInputPlugin.java
24
25
  - src/main/java/org/embulk/input/jdbc/JdbcColumn.java
25
26
  - src/main/java/org/embulk/input/jdbc/JdbcColumnOption.java
26
27
  - src/main/java/org/embulk/input/jdbc/JdbcInputConnection.java
28
+ - src/main/java/org/embulk/input/jdbc/JdbcLiteral.java
27
29
  - src/main/java/org/embulk/input/jdbc/JdbcSchema.java
28
30
  - src/main/java/org/embulk/input/jdbc/ToString.java
29
31
  - src/main/java/org/embulk/input/jdbc/ToStringMap.java
@@ -42,7 +44,6 @@ files:
42
44
  - src/main/java/org/embulk/input/jdbc/getter/TimeColumnGetter.java
43
45
  - src/main/java/org/embulk/input/jdbc/getter/TimestampColumnGetter.java
44
46
  - src/test/java/org/embulk/input/EmbulkPluginTester.java
45
- - classpath/embulk-input-jdbc-0.7.2.jar
46
47
  homepage: https://github.com/embulk/embulk-input-jdbc
47
48
  licenses:
48
49
  - Apache 2.0
@@ -53,17 +54,17 @@ require_paths:
53
54
  - lib
54
55
  required_ruby_version: !ruby/object:Gem::Requirement
55
56
  requirements:
56
- - - '>='
57
+ - - ">="
57
58
  - !ruby/object:Gem::Version
58
59
  version: '0'
59
60
  required_rubygems_version: !ruby/object:Gem::Requirement
60
61
  requirements:
61
- - - '>='
62
+ - - ">="
62
63
  - !ruby/object:Gem::Version
63
64
  version: '0'
64
65
  requirements: []
65
66
  rubyforge_project:
66
- rubygems_version: 2.1.9
67
+ rubygems_version: 2.4.8
67
68
  signing_key:
68
69
  specification_version: 4
69
70
  summary: JDBC input plugin for Embulk
Binary file