Skip to content

Commit 1e42e2b

Browse files
Rework on now function implementation (#113)
Signed-off-by: Yury-Fridlyand <[email protected]>
1 parent d5e8a21 commit 1e42e2b

File tree

22 files changed

+360
-289
lines changed

22 files changed

+360
-289
lines changed

common/src/main/java/org/opensearch/sql/common/utils/QueryContext.java

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,6 @@ public class QueryContext {
2424
*/
2525
private static final String REQUEST_ID_KEY = "request_id";
2626

27-
/**
28-
* Timestamp when SQL plugin started to process current request.
29-
*/
30-
private static final String REQUEST_PROCESSING_STARTED = "request_processing_started";
31-
3227
/**
3328
* Generates a random UUID and adds to the {@link ThreadContext} as the request id.
3429
* <p>
@@ -56,22 +51,6 @@ public static String getRequestId() {
5651
return id;
5752
}
5853

59-
public static void recordProcessingStarted() {
60-
ThreadContext.put(REQUEST_PROCESSING_STARTED, LocalDateTime.now().toString());
61-
}
62-
63-
/**
64-
* Get recorded previously time indicating when processing started for the current query.
65-
* @return A LocalDateTime object
66-
*/
67-
public static LocalDateTime getProcessingStartedTime() {
68-
if (ThreadContext.containsKey(REQUEST_PROCESSING_STARTED)) {
69-
return LocalDateTime.parse(ThreadContext.get(REQUEST_PROCESSING_STARTED));
70-
}
71-
// This shouldn't happen outside of unit tests
72-
return LocalDateTime.now();
73-
}
74-
7554
/**
7655
* Wraps a given instance of {@link Runnable} into a new one which gets all the
7756
* entries from current ThreadContext map.

core/src/main/java/org/opensearch/sql/analysis/AnalysisContext.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
package org.opensearch.sql.analysis;
88

99
import java.util.ArrayList;
10+
import java.util.HashMap;
1011
import java.util.List;
12+
import java.util.Map;
1113
import java.util.Objects;
1214
import lombok.Getter;
15+
import org.opensearch.sql.expression.Expression;
1316
import org.opensearch.sql.expression.NamedExpression;
1417

1518
/**
@@ -23,13 +26,26 @@ public class AnalysisContext {
2326
@Getter
2427
private final List<NamedExpression> namedParseExpressions;
2528

29+
/**
30+
* Storage for values of functions which return a constant value.
31+
* We are storing the values there to use it in sequential calls to those functions.
32+
* For example, `now` function should the same value during processing a query.
33+
*/
34+
@Getter
35+
private final Map<String, Expression> constantFunctionValues;
36+
2637
public AnalysisContext() {
2738
this(new TypeEnvironment(null));
2839
}
2940

41+
/**
42+
* Class CTOR.
43+
* @param environment Env to set to a new instance.
44+
*/
3045
public AnalysisContext(TypeEnvironment environment) {
3146
this.environment = environment;
3247
this.namedParseExpressions = new ArrayList<>();
48+
this.constantFunctionValues = new HashMap<>();
3349
}
3450

3551
/**

core/src/main/java/org/opensearch/sql/analysis/ExpressionAnalyzer.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.opensearch.sql.ast.expression.Case;
2525
import org.opensearch.sql.ast.expression.Cast;
2626
import org.opensearch.sql.ast.expression.Compare;
27+
import org.opensearch.sql.ast.expression.ConstantFunction;
2728
import org.opensearch.sql.ast.expression.EqualTo;
2829
import org.opensearch.sql.ast.expression.Field;
2930
import org.opensearch.sql.ast.expression.Function;
@@ -169,6 +170,19 @@ public Expression visitRelevanceFieldList(RelevanceFieldList node, AnalysisConte
169170
ImmutableMap.copyOf(node.getFieldList())));
170171
}
171172

173+
@Override
174+
public Expression visitConstantFunction(ConstantFunction node, AnalysisContext context) {
175+
var valueName = node.getFuncName();
176+
if (context.getConstantFunctionValues().containsKey(valueName)) {
177+
return context.getConstantFunctionValues().get(valueName);
178+
}
179+
180+
var value = visitFunction(node, context);
181+
value = DSL.literal(value.valueOf(null));
182+
context.getConstantFunctionValues().put(valueName, value);
183+
return value;
184+
}
185+
172186
@Override
173187
public Expression visitFunction(Function node, AnalysisContext context) {
174188
FunctionName functionName = FunctionName.of(node.getFuncName());

core/src/main/java/org/opensearch/sql/ast/AbstractNodeVisitor.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.opensearch.sql.ast.expression.Case;
1616
import org.opensearch.sql.ast.expression.Cast;
1717
import org.opensearch.sql.ast.expression.Compare;
18+
import org.opensearch.sql.ast.expression.ConstantFunction;
1819
import org.opensearch.sql.ast.expression.EqualTo;
1920
import org.opensearch.sql.ast.expression.Field;
2021
import org.opensearch.sql.ast.expression.Function;
@@ -116,6 +117,10 @@ public T visitRelevanceFieldList(RelevanceFieldList node, C context) {
116117
return visitChildren(node, context);
117118
}
118119

120+
public T visitConstantFunction(ConstantFunction node, C context) {
121+
return visitChildren(node, context);
122+
}
123+
119124
public T visitUnresolvedAttribute(UnresolvedAttribute node, C context) {
120125
return visitChildren(node, context);
121126
}

core/src/main/java/org/opensearch/sql/ast/dsl/AstDSL.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.opensearch.sql.ast.expression.Case;
1919
import org.opensearch.sql.ast.expression.Cast;
2020
import org.opensearch.sql.ast.expression.Compare;
21+
import org.opensearch.sql.ast.expression.ConstantFunction;
2122
import org.opensearch.sql.ast.expression.DataType;
2223
import org.opensearch.sql.ast.expression.EqualTo;
2324
import org.opensearch.sql.ast.expression.Field;
@@ -229,6 +230,10 @@ public static Function function(String funcName, UnresolvedExpression... funcArg
229230
return new Function(funcName, Arrays.asList(funcArgs));
230231
}
231232

233+
public static Function constantFunction(String funcName, UnresolvedExpression... funcArgs) {
234+
return new ConstantFunction(funcName, Arrays.asList(funcArgs));
235+
}
236+
232237
/**
233238
* CASE
234239
* WHEN search_condition THEN result_expr
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
7+
package org.opensearch.sql.ast.expression;
8+
9+
import java.util.List;
10+
import lombok.EqualsAndHashCode;
11+
import org.opensearch.sql.ast.AbstractNodeVisitor;
12+
13+
/**
14+
* Expression node that holds a function which should be replaced by its constant[1] value.
15+
* [1] Constant at execution time.
16+
*/
17+
@EqualsAndHashCode(callSuper = false)
18+
public class ConstantFunction extends Function {
19+
20+
public ConstantFunction(String funcName, List<UnresolvedExpression> funcArgs) {
21+
super(funcName, funcArgs);
22+
}
23+
24+
@Override
25+
public <R, C> R accept(AbstractNodeVisitor<R, C> nodeVisitor, C context) {
26+
return nodeVisitor.visitConstantFunction(this, context);
27+
}
28+
}

core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFunction.java

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,8 @@
2828
import java.time.format.TextStyle;
2929
import java.util.Locale;
3030
import java.util.concurrent.TimeUnit;
31-
import java.util.function.Supplier;
3231
import javax.annotation.Nullable;
3332
import lombok.experimental.UtilityClass;
34-
import org.opensearch.sql.common.utils.QueryContext;
3533
import org.opensearch.sql.data.model.ExprDateValue;
3634
import org.opensearch.sql.data.model.ExprDatetimeValue;
3735
import org.opensearch.sql.data.model.ExprIntegerValue;
@@ -104,15 +102,12 @@ public void register(BuiltinFunctionRepository repository) {
104102

105103
/**
106104
* NOW() returns a constant time that indicates the time at which the statement began to execute.
105+
* `fsp` argument support is removed until refactoring to avoid bug where `now()`, `now(x)` and
106+
* `now(y) return different values.
107107
*/
108-
private LocalDateTime now(@Nullable Integer fsp) {
109-
return formatLocalDateTime(QueryContext::getProcessingStartedTime, fsp);
110-
}
111-
112108
private FunctionResolver now(FunctionName functionName) {
113109
return define(functionName,
114-
impl(() -> new ExprDatetimeValue(now((Integer)null)), DATETIME),
115-
impl((v) -> new ExprDatetimeValue(now(v.integerValue())), DATETIME, INTEGER)
110+
impl(() -> new ExprDatetimeValue(formatNow(null)), DATETIME)
116111
);
117112
}
118113

@@ -135,21 +130,19 @@ private FunctionResolver localtime() {
135130
/**
136131
* SYSDATE() returns the time at which it executes.
137132
*/
138-
private LocalDateTime sysDate(@Nullable Integer fsp) {
139-
return formatLocalDateTime(LocalDateTime::now, fsp);
140-
}
141-
142133
private FunctionResolver sysdate() {
143134
return define(BuiltinFunctionName.SYSDATE.getName(),
144-
impl(() -> new ExprDatetimeValue(sysDate(null)), DATETIME),
145-
impl((v) -> new ExprDatetimeValue(sysDate(v.integerValue())), DATETIME, INTEGER)
135+
impl(() -> new ExprDatetimeValue(formatNow(null)), DATETIME),
136+
impl((v) -> new ExprDatetimeValue(formatNow(v.integerValue())), DATETIME, INTEGER)
146137
);
147138
}
148139

140+
/**
141+
* Synonym for @see `now`.
142+
*/
149143
private FunctionResolver curtime(FunctionName functionName) {
150144
return define(functionName,
151-
impl(() -> new ExprTimeValue(sysDate(null).toLocalTime()), TIME),
152-
impl((v) -> new ExprTimeValue(sysDate(v.integerValue()).toLocalTime()), TIME, INTEGER)
145+
impl(() -> new ExprTimeValue(formatNow(null).toLocalTime()), TIME)
153146
);
154147
}
155148

@@ -163,7 +156,7 @@ private FunctionResolver current_time() {
163156

164157
private FunctionResolver curdate(FunctionName functionName) {
165158
return define(functionName,
166-
impl(() -> new ExprDateValue(sysDate(null).toLocalDate()), DATE)
159+
impl(() -> new ExprDateValue(formatNow(null).toLocalDate()), DATE)
167160
);
168161
}
169162

@@ -829,17 +822,15 @@ private ExprValue exprYear(ExprValue date) {
829822
}
830823

831824
/**
832-
* Prepare LocalDateTime value.
833-
* @param supplier A function which returns LocalDateTime to format.
825+
* Prepare LocalDateTime value. Truncate fractional second part according to the argument.
834826
* @param fsp argument is given to specify a fractional seconds precision from 0 to 6,
835827
* the return value includes a fractional seconds part of that many digits.
836828
* @return LocalDateTime object.
837829
*/
838-
private LocalDateTime formatLocalDateTime(Supplier<LocalDateTime> supplier,
839-
@Nullable Integer fsp) {
840-
var res = supplier.get();
830+
private LocalDateTime formatNow(@Nullable Integer fsp) {
831+
var res = LocalDateTime.now();
841832
if (fsp == null) {
842-
return res;
833+
fsp = 0;
843834
}
844835
var defaultPrecision = 9; // There are 10^9 nanoseconds in one second
845836
if (fsp < 0 || fsp > 6) { // Check that the argument is in the allowed range [0, 6]

core/src/test/java/org/opensearch/sql/analysis/ExpressionAnalyzerTest.java

Lines changed: 43 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
import static java.util.Collections.emptyList;
1010
import static org.junit.jupiter.api.Assertions.assertEquals;
11+
import static org.junit.jupiter.api.Assertions.assertSame;
1112
import static org.junit.jupiter.api.Assertions.assertThrows;
13+
import static org.junit.jupiter.api.Assertions.assertTrue;
1214
import static org.opensearch.sql.ast.dsl.AstDSL.field;
1315
import static org.opensearch.sql.ast.dsl.AstDSL.function;
1416
import static org.opensearch.sql.ast.dsl.AstDSL.intLiteral;
@@ -26,14 +28,10 @@
2628
import com.google.common.collect.ImmutableMap;
2729
import java.util.Collections;
2830
import java.util.LinkedHashMap;
31+
import java.util.List;
2932
import java.util.Map;
30-
import java.util.function.Function;
31-
import java.util.stream.Stream;
3233
import org.junit.jupiter.api.Test;
3334
import org.junit.jupiter.api.extension.ExtendWith;
34-
import org.junit.jupiter.params.ParameterizedTest;
35-
import org.junit.jupiter.params.provider.Arguments;
36-
import org.junit.jupiter.params.provider.MethodSource;
3735
import org.opensearch.sql.analysis.symbol.Namespace;
3836
import org.opensearch.sql.analysis.symbol.Symbol;
3937
import org.opensearch.sql.ast.dsl.AstDSL;
@@ -52,6 +50,7 @@
5250
import org.opensearch.sql.expression.Expression;
5351
import org.opensearch.sql.expression.FunctionExpression;
5452
import org.opensearch.sql.expression.HighlightExpression;
53+
import org.opensearch.sql.expression.LiteralExpression;
5554
import org.opensearch.sql.expression.config.ExpressionConfig;
5655
import org.opensearch.sql.expression.window.aggregation.AggregateWindowFunction;
5756
import org.springframework.context.annotation.Configuration;
@@ -543,47 +542,45 @@ public void match_phrase_prefix_all_params() {
543542
);
544543
}
545544

546-
private static Stream<Arguments> functionNames() {
547-
var dsl = new DSL(new ExpressionConfig().functionRepository());
548-
return Stream.of(
549-
Arguments.of((Function<Expression[], FunctionExpression>)dsl::now,
550-
"now", true),
551-
Arguments.of((Function<Expression[], FunctionExpression>)dsl::current_timestamp,
552-
"current_timestamp", true),
553-
Arguments.of((Function<Expression[], FunctionExpression>)dsl::localtimestamp,
554-
"localtimestamp", true),
555-
Arguments.of((Function<Expression[], FunctionExpression>)dsl::localtime,
556-
"localtime", true),
557-
Arguments.of((Function<Expression[], FunctionExpression>)dsl::sysdate,
558-
"sysdate", true),
559-
Arguments.of((Function<Expression[], FunctionExpression>)dsl::curtime,
560-
"curtime", true),
561-
Arguments.of((Function<Expression[], FunctionExpression>)dsl::current_time,
562-
"current_time", true),
563-
Arguments.of((Function<Expression[], FunctionExpression>)dsl::curdate,
564-
"curdate", false),
565-
Arguments.of((Function<Expression[], FunctionExpression>)dsl::current_date,
566-
"current_date", false));
567-
}
568-
569-
@ParameterizedTest(name = "{1}")
570-
@MethodSource("functionNames")
571-
public void now_like_functions(Function<Expression[], FunctionExpression> function,
572-
String name,
573-
Boolean hasFsp) {
574-
assertAnalyzeEqual(
575-
function.apply(new Expression[]{}),
576-
AstDSL.function(name));
577-
578-
if (hasFsp) {
579-
assertAnalyzeEqual(
580-
function.apply(new Expression[]{DSL.ref("integer_value", INTEGER)}),
581-
AstDSL.function(name, field("integer_value")));
582-
583-
assertAnalyzeEqual(
584-
function.apply(new Expression[]{DSL.literal(3)}),
585-
AstDSL.function(name, intLiteral(3)));
586-
}
545+
@Test
546+
public void constant_function_is_calculated_on_analyze() {
547+
// Actually, we can call any function as ConstantFunction to be calculated on analyze stage
548+
assertTrue(analyze(AstDSL.constantFunction("now")) instanceof LiteralExpression);
549+
assertTrue(analyze(AstDSL.constantFunction("localtime")) instanceof LiteralExpression);
550+
}
551+
552+
@Test
553+
public void function_isnt_calculated_on_analyze() {
554+
assertTrue(analyze(function("now")) instanceof FunctionExpression);
555+
assertTrue(analyze(AstDSL.function("localtime")) instanceof FunctionExpression);
556+
}
557+
558+
@Test
559+
public void constant_function_returns_constant_cached_value() {
560+
var values = List.of(analyze(AstDSL.constantFunction("now")),
561+
analyze(AstDSL.constantFunction("now")), analyze(AstDSL.constantFunction("now")));
562+
assertTrue(values.stream().allMatch(v ->
563+
v.valueOf(null) == analyze(AstDSL.constantFunction("now")).valueOf(null)));
564+
}
565+
566+
@Test
567+
public void function_returns_non_constant_value() {
568+
// Even a function returns the same values - they are calculated on each call
569+
// `sysdate()` which returns `LocalDateTime.now()` shouldn't be cached and should return always
570+
// different values
571+
var values = List.of(analyze(function("sysdate")), analyze(function("sysdate")),
572+
analyze(function("sysdate")), analyze(function("sysdate")));
573+
var referenceValue = analyze(function("sysdate")).valueOf(null);
574+
assertTrue(values.stream().noneMatch(v -> v.valueOf(null) == referenceValue));
575+
}
576+
577+
@Test
578+
public void now_as_a_function_not_cached() {
579+
// // We can call `now()` as a function, in that case nothing should be cached
580+
var values = List.of(analyze(function("now")), analyze(function("now")),
581+
analyze(function("now")), analyze(function("now")));
582+
var referenceValue = analyze(function("now")).valueOf(null);
583+
assertTrue(values.stream().noneMatch(v -> v.valueOf(null) == referenceValue));
587584
}
588585

589586
@Test

0 commit comments

Comments
 (0)