Skip to content

Commit 3b3c2ad

Browse files
Bruce Irschickbirschick-bq
Bruce Irschick
andauthored
[AD-980] Handle in-driver date add calculation for literals. (#428)
* [AD-980] Handle in-driver date add calculation for literals. * Commit Code Coverage Badge * [AD-980] Add support for CURRENT_TIMESTAMP and remove support for handling unused data types in the literal conversion. * [AD-980] Fix file size. * Commit Code Coverage Badge * [AD-980] Test TIMESTAMPADD on left and DATE literal. * Commit Code Coverage Badge Co-authored-by: birschick-bq <[email protected]>
1 parent 724fdda commit 3b3c2ad

File tree

5 files changed

+318
-7
lines changed

5 files changed

+318
-7
lines changed

.github/badges/branches.svg

+1-1
Loading

src/main/java/software/amazon/documentdb/jdbc/calcite/adapter/DocumentDbRules.java

+54-5
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@
5353
import org.apache.calcite.util.TimeString;
5454
import org.apache.calcite.util.Util;
5555
import org.apache.calcite.util.trace.CalciteTrace;
56+
import org.bson.BsonDocument;
5657
import org.bson.BsonType;
58+
import org.bson.BsonValue;
5759
import org.checkerframework.checker.nullness.qual.NonNull;
5860
import org.slf4j.Logger;
5961
import software.amazon.documentdb.jdbc.common.utilities.SqlError;
@@ -204,7 +206,7 @@ private static boolean needsQuote(final String s) {
204206
* @param fieldName The non-normalized string
205207
* @return The input string with '$' replaced by '_'
206208
*/
207-
protected static String getNormalizedIdentifier(final String fieldName) {
209+
static String getNormalizedIdentifier(final String fieldName) {
208210
return fieldName.startsWith("$") ? "_" + fieldName.substring(1) : fieldName;
209211
}
210212

@@ -757,6 +759,8 @@ private static Operand reformatObjectIdLiteral(
757759

758760
private static class DateFunctionTranslator {
759761

762+
private static final String CURRENT_DATE = "CURRENT_DATE";
763+
private static final String CURRENT_TIMESTAMP = "CURRENT_TIMESTAMP";
760764
private static final Map<TimeUnitRange, String> DATE_PART_OPERATORS =
761765
new HashMap<>();
762766
private static final Instant FIRST_DAY_OF_WEEK_AFTER_EPOCH =
@@ -786,9 +790,54 @@ private static Operand translateCurrentTimestamp(final Instant currentTime) {
786790
@SneakyThrows
787791
private static Operand translateDateAdd(final RexCall call, final List<Operand> strings) {
788792
verifySupportedDateAddType(call.getOperands().get(1));
793+
794+
// Is date addition between literals (including CURRENT_DATE)?
795+
final boolean isLiteralCandidate = isDateLiteralCandidate(call, strings);
796+
if (isLiteralCandidate) {
797+
// Perform in-memory calculation before sending to server.
798+
return getDateAddLiteralOperand(strings);
799+
}
800+
// Otherwise, perform addition on server.
789801
return new Operand("{ \"$add\":" + "[" + Util.commaList(strings) + "]}");
790802
}
791803

804+
private static boolean isDateLiteralCandidate(final RexCall call, final List<Operand> strings) {
805+
final boolean allLiterals = call.getOperands().stream()
806+
.allMatch(op -> {
807+
final SqlKind opKind = op.getKind();
808+
final String opName = op.toString();
809+
return opKind == SqlKind.LITERAL
810+
|| opName.equalsIgnoreCase(CURRENT_DATE)
811+
|| opName.equalsIgnoreCase(CURRENT_TIMESTAMP);
812+
});
813+
final boolean allHaveQueryValue = strings.stream().allMatch(op -> op.getQueryValue() != null);
814+
return allLiterals && allHaveQueryValue;
815+
}
816+
817+
private static Operand getDateAddLiteralOperand(final List<Operand> strings) {
818+
final String queryValue0 = strings.get(0).getQueryValue();
819+
final String queryValue1 = strings.get(1).getQueryValue();
820+
final BsonDocument document0 = BsonDocument.parse("{field: " + queryValue0 + "}");
821+
final BsonDocument document1 = BsonDocument.parse("{field: " + queryValue1 + "}");
822+
823+
long sum = 0L;
824+
for (BsonValue v : new BsonValue[]{document0.get("field"), document1.get("field")}) {
825+
switch (v.getBsonType()) {
826+
case DATE_TIME:
827+
sum += v.asDateTime().getValue();
828+
break;
829+
case INT64:
830+
sum += v.asInt64().getValue();
831+
break;
832+
default:
833+
throw new UnsupportedOperationException(
834+
"Unsupported data type '" + v.getBsonType().name() + "'");
835+
}
836+
}
837+
final String query = "{\"$date\": {\"$numberLong\": \"" + sum + "\"}}";
838+
return new Operand(query, query, true);
839+
}
840+
792841
private static void verifySupportedDateAddType(final RexNode node)
793842
throws SQLFeatureNotSupportedException {
794843
if (node.getType().getSqlTypeName() == SqlTypeName.INTERVAL_MONTH
@@ -1169,7 +1218,7 @@ private static class StringFunctionTranslator {
11691218
new HashMap<>();
11701219

11711220
static {
1172-
STRING_OPERATORS.put(SqlStdOperatorTable.CONCAT, "$concat");;
1221+
STRING_OPERATORS.put(SqlStdOperatorTable.CONCAT, "$concat");
11731222
STRING_OPERATORS.put(SqlStdOperatorTable.LOWER, "$toLower");
11741223
STRING_OPERATORS.put(SqlStdOperatorTable.UPPER, "$toUpper");
11751224
STRING_OPERATORS.put(SqlStdOperatorTable.CHAR_LENGTH, "$strLenCP");
@@ -1215,15 +1264,15 @@ private static Operand getMongoAggregateForPositionStringOperator(
12151264
args.add("{" + STRING_OPERATORS.get(SqlStdOperatorTable.LOWER) + ":" + strings.get(1) + "}");
12161265
args.add("{" + STRING_OPERATORS.get(SqlStdOperatorTable.LOWER) + ":" + strings.get(0) + "}");
12171266
// Check if either string is null.
1218-
operand.append("{\"$cond\": [" + RexToMongoTranslator.getNullCheckExpr(strings) + ", ");
1267+
operand.append("{\"$cond\": [").append(RexToMongoTranslator.getNullCheckExpr(strings)).append(", ");
12191268
// Add starting index if any.
12201269
if (strings.size() == 3) {
12211270
args.add("{\"$subtract\": [" + strings.get(2) + ", 1]}"); // Convert to 0-based.
1222-
operand.append("{\"$cond\": [{\"$lte\": [" + strings.get(2) + ", 0]}, 0, "); // Check if 1-based index > 0.
1271+
operand.append("{\"$cond\": [{\"$lte\": [").append(strings.get(2)).append(", 0]}, 0, "); // Check if 1-based index > 0.
12231272
finish.append("]}");
12241273
}
12251274
// Convert 0-based index to 1-based.
1226-
operand.append("{\"$add\": [{" + STRING_OPERATORS.get(SqlStdOperatorTable.POSITION) + ": [" + Util.commaList(args) + "]}, 1]}");
1275+
operand.append("{\"$add\": [{").append(STRING_OPERATORS.get(SqlStdOperatorTable.POSITION)).append(": [").append(Util.commaList(args)).append("]}, 1]}");
12271276
operand.append(finish);
12281277
operand.append(", null ]}");
12291278
// Return 1-based index when string is found.

src/markdown/support/troubleshooting-guide.md

+76-1
Original file line numberDiff line numberDiff line change
@@ -262,4 +262,79 @@ For example:
262262
- In Tableau, a parameter must be used in the command line or terminal:
263263
- In Windows: `start "" "c:\program files\Tableau\Tableau [version]\bin\tableau.exe" -DLogLevel=DEBUG`
264264
- In MacOS: `/Applications/Tableau\ Desktop\[version].app/Contents/MacOS/Tableau -DLogLevel=DEBUG`
265-
- Tableau logs are located at: `{user.home}/Documents/My Tableau Repository/Logs`
265+
- Tableau logs are located at: `{user.home}/Documents/My Tableau Repository/Logs`
266+
267+
## Permanently Setting Environment Variables
268+
269+
### Windows
270+
271+
- From the start menu (or press the `Windows` key), type '***Edit environment variables for your account***' and launch
272+
the settings application.
273+
- If an environment variable is already listed, click the '***Edit...***' button. Otherwise, click the
274+
"***New...***" button.
275+
- Enter the name of the variable (e.g., `JAVA_TOOL_OPTIONS`) in the '***Variable name***' field and then enter the value
276+
in the '***Variable value***' field (e.g., `-Ddocumentdb.jdbc.log.level=DEBUG`. Click the '***Ok***' button to save the value.
277+
- Restart the application or command window for the change to take effect.
278+
279+
### MacOS
280+
281+
- Create the file `~/Library/LaunchAgents/environment.plist` if it doesn't exist
282+
- Edit the file `~/Library/LaunchAgents/environment.plist`
283+
- If the contents do not exist, enter the following template:
284+
```xml
285+
?xml version="1.0" encoding="UTF-8"?>
286+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
287+
<plist version="1.0">
288+
<dict>
289+
<key>Label</key>
290+
<string>my.startup</string>
291+
<key>ProgramArguments</key>
292+
<array>
293+
<string>sh</string>
294+
<string>-c</string>
295+
<string>
296+
<!-- Add more 'launchctl setenv ...' commands here -->
297+
</string>
298+
</array>
299+
<key>RunAtLoad</key>
300+
<true/>
301+
</dict>
302+
</plist>
303+
```
304+
305+
- Add environment variables in the path `/dict/array/string` after the `<string>-c</string>` node.
306+
- In the example below, we add two environment variables `JAVA_TOOL_OPTIONS` and `DOCUMENTDB_CUSTOM_OPTIONS`
307+
308+
```xml
309+
?xml version="1.0" encoding="UTF-8"?>
310+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
311+
<plist version="1.0">
312+
<dict>
313+
<key>Label</key>
314+
<string>my.startup</string>
315+
<key>ProgramArguments</key>
316+
<array>
317+
<string>sh</string>
318+
<string>-c</string>
319+
<string>
320+
<!-- Add more 'launchctl setenv ...' commands here -->
321+
launchctl setenv JAVA_TOOL_OPTIONS -Ddocumentdb.jdbc.log.level=DEBUG
322+
launchctl setenv DOCUMENTDB_CUSTOM_OPTIONS allowDiskUse=enable
323+
</string>
324+
</array>
325+
<key>RunAtLoad</key>
326+
<true/>
327+
</dict>
328+
</plist>
329+
```
330+
331+
- You can reboot your machine or in a terminal windows type the following commands to load your changes.
332+
333+
```shell
334+
launchctl stop ~/Library/LaunchAgents/environment.plist
335+
launchctl unload ~/Library/LaunchAgents/environment.plist
336+
launchctl load ~/Library/LaunchAgents/environment.plist
337+
launchctl start ~/Library/LaunchAgents/environment.plist
338+
```
339+
340+
- Restart your application for the changes to take effect.

src/test/java/software/amazon/documentdb/jdbc/DocumentDbStatementFilterTest.java

+69
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.bson.BsonDouble;
2323
import org.bson.BsonInt64;
2424
import org.bson.BsonMinKey;
25+
import org.bson.BsonNull;
2526
import org.bson.BsonObjectId;
2627
import org.bson.BsonString;
2728
import org.bson.types.ObjectId;
@@ -37,6 +38,7 @@
3738
import java.sql.Statement;
3839
import java.sql.Timestamp;
3940
import java.time.Instant;
41+
import java.time.temporal.ChronoUnit;
4042

4143
public class DocumentDbStatementFilterTest extends DocumentDbStatementTest {
4244

@@ -602,6 +604,73 @@ void testQueryWhereTimestampAdd(final DocumentDbTestEnvironment testEnvironment)
602604
}
603605
}
604606

607+
/**
608+
* Tests where condition of field compared to CURRENT_DATE.
609+
*
610+
* @throws SQLException occurs if query fails.
611+
*/
612+
@DisplayName("Tests where condition of field compared to CURRENT_DATE.")
613+
@ParameterizedTest(name = "testWhereFieldComparedToCurrentDate - [{index}] - {arguments}")
614+
@MethodSource({"getTestEnvironments"})
615+
void testWhereFieldComparedToCurrentDate(final DocumentDbTestEnvironment testEnvironment) throws SQLException {
616+
setTestEnvironment(testEnvironment);
617+
final String tableName = "testWhereFieldComparedToCurrentDate";
618+
final long dateTimePast = Instant.now().minus(2, ChronoUnit.DAYS).toEpochMilli();
619+
final long dateTimeFuture = Instant.now().plus(2, ChronoUnit.DAYS).toEpochMilli();
620+
final BsonDocument doc1 = BsonDocument.parse("{\"_id\": 101}");
621+
doc1.append("field", new BsonDateTime(dateTimePast));
622+
final BsonDocument doc2 = BsonDocument.parse("{\"_id\": 102}");
623+
doc2.append("field", new BsonDateTime(dateTimeFuture));
624+
final BsonDocument doc3 = BsonDocument.parse("{\"_id\": 103}");
625+
doc3.append("field", new BsonNull());
626+
insertBsonDocuments(tableName, new BsonDocument[]{doc1, doc2, doc3});
627+
628+
try (Connection connection = getConnection()) {
629+
final Statement statement = getDocumentDbStatement(connection);
630+
631+
for (final String currentFunc : new String[]{"CURRENT_DATE", "CURRENT_TIMESTAMP"}) {
632+
// Find condition that does exist.
633+
final ResultSet resultSet1 = statement.executeQuery(
634+
String.format(
635+
"SELECT \"field\"%n" +
636+
" FROM \"%s\".\"%s\"%n" +
637+
" WHERE \"field\" < TIMESTAMPADD(DAY, 1, %s)",
638+
getDatabaseName(), tableName, currentFunc));
639+
Assertions.assertNotNull(resultSet1);
640+
Assertions.assertTrue(resultSet1.next());
641+
Assertions.assertFalse(resultSet1.next());
642+
// Find condition that does exist.
643+
final ResultSet resultSet2 = statement.executeQuery(
644+
String.format(
645+
"SELECT \"field\"%n" +
646+
" FROM \"%s\".\"%s\"%n" +
647+
" WHERE \"field\" > TIMESTAMPADD(DAY, 1, %s)",
648+
getDatabaseName(), tableName, currentFunc));
649+
Assertions.assertNotNull(resultSet2);
650+
Assertions.assertTrue(resultSet2.next());
651+
Assertions.assertFalse(resultSet2.next());
652+
// Find condition that does NOT exist.
653+
final ResultSet resultSet3 = statement.executeQuery(
654+
String.format(
655+
"SELECT \"field\"%n" +
656+
" FROM \"%s\".\"%s\"%n" +
657+
" WHERE \"field\" > TIMESTAMPADD(DAY, 10, %s)",
658+
getDatabaseName(), tableName, currentFunc));
659+
Assertions.assertNotNull(resultSet3);
660+
Assertions.assertFalse(resultSet3.next());
661+
// Find condition that does NOT exist.
662+
final ResultSet resultSet4 = statement.executeQuery(
663+
String.format(
664+
"SELECT \"field\"%n" +
665+
" FROM \"%s\".\"%s\"%n" +
666+
" WHERE \"field\" < TIMESTAMPADD(DAY, -10, %s)",
667+
getDatabaseName(), tableName, currentFunc));
668+
Assertions.assertNotNull(resultSet4);
669+
Assertions.assertFalse(resultSet4.next());
670+
}
671+
}
672+
}
673+
605674
/**
606675
* Tests for queries filtering by IS NULL.
607676
*

0 commit comments

Comments
 (0)