Skip to content

Commit a50218d

Browse files
committed
Add support for COPY FROM special CQL command
1 parent 4535551 commit a50218d

13 files changed

+668
-125
lines changed

Diff for: CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
88
### Added
99
- Add support for the following special CQL commands in `CassandraStatement`:
1010
- `SOURCE <filename>`
11-
- `COPY <tableName>[(<colums>)] TO <target>[ WITH <options>[ AND <options>...]]`
11+
- `COPY <tableName>[(<colums>)] TO|FROM <target>[ WITH <options>[ AND <options>...]]`
1212
- Add a method `CassandraConnection.setOptionSet(OptionSet)` to programmatically define a custom compliance mode option
1313
set on a pre-existing connection.
1414

Diff for: src/main/java/com/ing/data/cassandra/jdbc/CassandraStatement.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import java.util.Objects;
5050
import java.util.concurrent.CompletionStage;
5151

52+
import static com.ing.data.cassandra.jdbc.utils.DriverUtil.SINGLE_QUOTE;
5253
import static com.ing.data.cassandra.jdbc.utils.ErrorConstants.BAD_AUTO_GEN;
5354
import static com.ing.data.cassandra.jdbc.utils.ErrorConstants.BAD_CONCURRENCY_RS;
5455
import static com.ing.data.cassandra.jdbc.utils.ErrorConstants.BAD_FETCH_DIR;
@@ -310,16 +311,15 @@ private List<String> splitStatements(final String cql) {
310311

311312
// If a query contains a semicolon character in a string value (for example 'abc;xyz'), re-merge queries
312313
// wrongly split.
313-
final String singleQuote = "'";
314314
StringBuilder prevCqlQuery = new StringBuilder();
315315
for (final String cqlQuery : cqlQueries) {
316-
final boolean hasStringValues = cqlQuery.contains(singleQuote);
316+
final boolean hasStringValues = cqlQuery.contains(SINGLE_QUOTE);
317317
final boolean isFirstQueryPartWithIncompleteStringValue =
318-
countMatches(cqlQuery, singleQuote) % 2 == 1 && prevCqlQuery.length() == 0;
318+
countMatches(cqlQuery, SINGLE_QUOTE) % 2 == 1 && prevCqlQuery.length() == 0;
319319
final boolean isNotFirstQueryPartWithCompleteStringValue =
320-
countMatches(cqlQuery, singleQuote) % 2 == 0 && prevCqlQuery.length() > 0;
320+
countMatches(cqlQuery, SINGLE_QUOTE) % 2 == 0 && prevCqlQuery.length() > 0;
321321
final boolean isNotFirstQueryPartWithoutStringValue =
322-
!prevCqlQuery.toString().isEmpty() && !cqlQuery.contains(singleQuote);
322+
!prevCqlQuery.toString().isEmpty() && !cqlQuery.contains(SINGLE_QUOTE);
323323

324324
if ((hasStringValues && (isFirstQueryPartWithIncompleteStringValue
325325
|| isNotFirstQueryPartWithCompleteStringValue)) || isNotFirstQueryPartWithoutStringValue) {

Diff for: src/main/java/com/ing/data/cassandra/jdbc/SessionHolder.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import static com.ing.data.cassandra.jdbc.utils.DriverUtil.JSSE_KEYSTORE_PROPERTY;
6969
import static com.ing.data.cassandra.jdbc.utils.DriverUtil.JSSE_TRUSTSTORE_PASSWORD_PROPERTY;
7070
import static com.ing.data.cassandra.jdbc.utils.DriverUtil.JSSE_TRUSTSTORE_PROPERTY;
71+
import static com.ing.data.cassandra.jdbc.utils.DriverUtil.SINGLE_QUOTE;
7172
import static com.ing.data.cassandra.jdbc.utils.DriverUtil.redactSensitiveValuesInJdbcUrl;
7273
import static com.ing.data.cassandra.jdbc.utils.DriverUtil.toStringWithoutSensitiveValues;
7374
import static com.ing.data.cassandra.jdbc.utils.ErrorConstants.SSL_CONFIG_FAILED;
@@ -134,7 +135,7 @@ class SessionHolder {
134135

135136
// Parse the URL into a set of Properties and replace double quote marks (") by simple quotes (') to handle the
136137
// fact that double quotes (") are not valid characters in URIs.
137-
this.properties = parseURL(url.replace("\"", "'"));
138+
this.properties = parseURL(url.replace("\"", SINGLE_QUOTE));
138139

139140
// Other properties in parameters come from the initial call to connect(), they take priority.
140141
params.keySet().stream()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
package com.ing.data.cassandra.jdbc.commands;
17+
18+
import com.datastax.oss.driver.api.core.cql.ResultSet;
19+
import com.ing.data.cassandra.jdbc.CassandraStatement;
20+
import org.apache.commons.lang3.StringUtils;
21+
22+
import java.sql.SQLException;
23+
import java.sql.SQLSyntaxErrorException;
24+
import java.text.DecimalFormat;
25+
import java.text.DecimalFormatSymbols;
26+
import java.text.NumberFormat;
27+
import java.util.HashSet;
28+
import java.util.Locale;
29+
import java.util.Properties;
30+
import java.util.Set;
31+
32+
import static com.ing.data.cassandra.jdbc.commands.SpecialCommandsUtil.LOG;
33+
import static com.ing.data.cassandra.jdbc.utils.ErrorConstants.UNSUPPORTED_COPY_OPTIONS;
34+
35+
/**
36+
* Executor abstraction for common parts of the special commands {@code COPY}.
37+
*
38+
* @see CopyFromCommandExecutor
39+
* @see CopyToCommandExecutor
40+
*/
41+
public abstract class AbstractCopyCommandExecutor implements SpecialCommandExecutor {
42+
43+
static final char DEFAULT_DECIMAL_SEPARATOR = '.';
44+
static final String DEFAULT_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ssZ";
45+
static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
46+
static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
47+
static final String DEFAULT_NULL_FORMAT = "null";
48+
static final char DEFAULT_QUOTE_CHAR = '"';
49+
static final char DEFAULT_DELIMITER_CHAR = ',';
50+
static final char DEFAULT_ESCAPE_CHAR = '\\';
51+
52+
// Common supported options
53+
static final String OPTION_DECIMALSEP = "DECIMALSEP";
54+
static final String OPTION_DELIMITER = "DELIMITER";
55+
static final String OPTION_ESCAPE = "ESCAPE";
56+
static final String OPTION_HEADER = "HEADER";
57+
static final String OPTION_NULLVAL = "NULLVAL";
58+
static final String OPTION_QUOTE = "QUOTE";
59+
static final String OPTION_THOUSANDSSEP = "THOUSANDSSEP";
60+
static final Set<String> SUPPORTED_OPTIONS = new HashSet<String>() {
61+
{
62+
add(OPTION_DECIMALSEP);
63+
add(OPTION_DELIMITER);
64+
add(OPTION_ESCAPE);
65+
add(OPTION_HEADER);
66+
add(OPTION_NULLVAL);
67+
add(OPTION_QUOTE);
68+
add(OPTION_THOUSANDSSEP);
69+
}
70+
};
71+
72+
String tableName = null;
73+
String columns = null;
74+
Properties options = new Properties();
75+
DecimalFormat decimalFormat;
76+
String dateTimeFormat;
77+
String dateFormat;
78+
String timeFormat;
79+
80+
/**
81+
* {@inheritDoc}
82+
*/
83+
@Override
84+
public abstract ResultSet execute(CassandraStatement statement, String cql) throws SQLException;
85+
86+
void checkOptions() throws SQLSyntaxErrorException {
87+
// Remove the supported options from the set of options found in the command, if the result set is not empty,
88+
// this means there are unsupported options.
89+
final Set<String> invalidKeys = new HashSet<>(this.options.stringPropertyNames());
90+
invalidKeys.removeAll(SUPPORTED_OPTIONS);
91+
if (!invalidKeys.isEmpty()) {
92+
throw new SQLSyntaxErrorException(String.format(UNSUPPORTED_COPY_OPTIONS, invalidKeys));
93+
}
94+
}
95+
96+
void configureFormatters() {
97+
this.dateTimeFormat = DEFAULT_DATETIME_FORMAT;
98+
this.dateFormat = DEFAULT_DATE_FORMAT;
99+
this.timeFormat = DEFAULT_TIME_FORMAT;
100+
101+
final char thousandsSeparator = getOptionValueAsChar(OPTION_THOUSANDSSEP, Character.MIN_VALUE);
102+
final DecimalFormatSymbols decimalSymbols = new DecimalFormatSymbols(Locale.getDefault());
103+
decimalSymbols.setDecimalSeparator(getOptionValueAsChar(OPTION_DECIMALSEP, DEFAULT_DECIMAL_SEPARATOR));
104+
if (thousandsSeparator != Character.MIN_VALUE) {
105+
decimalSymbols.setGroupingSeparator(thousandsSeparator);
106+
}
107+
this.decimalFormat = (DecimalFormat) NumberFormat.getNumberInstance();
108+
this.decimalFormat.setGroupingUsed(thousandsSeparator != Character.MIN_VALUE);
109+
this.decimalFormat.setDecimalFormatSymbols(decimalSymbols);
110+
}
111+
112+
char getOptionValueAsChar(final String optionName, final char defaultValue) {
113+
final String optionValue = this.options.getProperty(optionName);
114+
if (StringUtils.isNotEmpty(optionValue)) {
115+
return optionValue.charAt(0);
116+
}
117+
return defaultValue;
118+
}
119+
120+
int getOptionValueAsInt(final String optionName, final int defaultValue) {
121+
final String optionValue = this.options.getProperty(optionName);
122+
if (optionValue != null) {
123+
try {
124+
return Integer.parseInt(optionValue);
125+
} catch (final NumberFormatException e) {
126+
LOG.warn("Invalid value for option {}: {}. Will use the default value: {}.",
127+
optionName, optionValue, defaultValue);
128+
}
129+
}
130+
return defaultValue;
131+
}
132+
133+
}

0 commit comments

Comments
 (0)