embulk-input-jdbc 0.7.2 → 0.7.3

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