Skip to content

Commit b170cf1

Browse files
authored
Support eval returns decimal division result instead of integer (opensearch-project#4440)
--------- Signed-off-by: Peng Huo <penghuo@gmail.com>
1 parent 095e8cf commit b170cf1

6 files changed

Lines changed: 201 additions & 60 deletions

File tree

core/src/main/java/org/opensearch/sql/calcite/CalcitePlanContext.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.apache.calcite.tools.RelBuilder;
2424
import org.opensearch.sql.ast.expression.UnresolvedExpression;
2525
import org.opensearch.sql.calcite.utils.CalciteToolsHelper;
26+
import org.opensearch.sql.common.setting.Settings;
2627
import org.opensearch.sql.executor.QueryType;
2728
import org.opensearch.sql.expression.function.FunctionProperties;
2829

@@ -39,6 +40,10 @@ public class CalcitePlanContext {
3940
/** This thread local variable is only used to skip script encoding in script pushdown. */
4041
public static final ThreadLocal<Boolean> skipEncoding = ThreadLocal.withInitial(() -> false);
4142

43+
/** Thread-local switch that tells whether the current query prefers legacy behavior. */
44+
private static final ThreadLocal<Boolean> legacyPreferredFlag =
45+
ThreadLocal.withInitial(() -> true);
46+
4247
@Getter @Setter private boolean isResolvingJoinCondition = false;
4348
@Getter @Setter private boolean isResolvingSubquery = false;
4449
@Getter @Setter private boolean inCoalesceFunction = false;
@@ -105,6 +110,27 @@ public static CalcitePlanContext create(
105110
return new CalcitePlanContext(config, querySizeLimit, queryType);
106111
}
107112

113+
/**
114+
* Executes {@code action} with the thread-local legacy flag set according to the supplied
115+
* settings.
116+
*/
117+
public static void run(Runnable action, Settings settings) {
118+
Boolean preferred = settings.getSettingValue(Settings.Key.PPL_SYNTAX_LEGACY_PREFERRED);
119+
legacyPreferredFlag.set(preferred);
120+
try {
121+
action.run();
122+
} finally {
123+
legacyPreferredFlag.remove();
124+
}
125+
}
126+
127+
/**
128+
* @return {@code true} when the current planning prefer legacy behavior.
129+
*/
130+
public static boolean isLegacyPreferred() {
131+
return legacyPreferredFlag.get();
132+
}
133+
108134
public void putRexLambdaRefMap(Map<String, RexLambdaRef> candidateMap) {
109135
this.rexLambdaRefMap.putAll(candidateMap);
110136
}

core/src/main/java/org/opensearch/sql/executor/QueryService.java

Lines changed: 67 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -90,68 +90,80 @@ public void executeWithCalcite(
9090
UnresolvedPlan plan,
9191
QueryType queryType,
9292
ResponseListener<ExecutionEngine.QueryResponse> listener) {
93-
try {
94-
AccessController.doPrivileged(
95-
(PrivilegedAction<Void>)
96-
() -> {
97-
CalcitePlanContext context =
98-
CalcitePlanContext.create(
99-
buildFrameworkConfig(), getQuerySizeLimit(), queryType);
100-
RelNode relNode = analyze(plan, context);
101-
RelNode optimized = optimize(relNode, context);
102-
RelNode calcitePlan = convertToCalcitePlan(optimized);
103-
executionEngine.execute(calcitePlan, context, listener);
104-
return null;
105-
});
106-
} catch (Throwable t) {
107-
if (isCalciteFallbackAllowed(t) && !(t instanceof NonFallbackCalciteException)) {
108-
log.warn("Fallback to V2 query engine since got exception", t);
109-
executeWithLegacy(plan, queryType, listener, Optional.of(t));
110-
} else {
111-
if (t instanceof Exception) {
112-
listener.onFailure((Exception) t);
113-
} else if (t instanceof VirtualMachineError) {
114-
// throw and fast fail the VM errors such as OOM (same with v2).
115-
throw t;
116-
} else {
117-
// Calcite may throw AssertError during query execution.
118-
listener.onFailure(new CalciteUnsupportedException(t.getMessage(), t));
119-
}
120-
}
121-
}
93+
CalcitePlanContext.run(
94+
() -> {
95+
try {
96+
AccessController.doPrivileged(
97+
(PrivilegedAction<Void>)
98+
() -> {
99+
CalcitePlanContext context =
100+
CalcitePlanContext.create(
101+
buildFrameworkConfig(), getQuerySizeLimit(), queryType);
102+
RelNode relNode = analyze(plan, context);
103+
RelNode optimized = optimize(relNode, context);
104+
RelNode calcitePlan = convertToCalcitePlan(optimized);
105+
executionEngine.execute(calcitePlan, context, listener);
106+
return null;
107+
});
108+
} catch (Throwable t) {
109+
if (isCalciteFallbackAllowed(t) && !(t instanceof NonFallbackCalciteException)) {
110+
log.warn("Fallback to V2 query engine since got exception", t);
111+
executeWithLegacy(plan, queryType, listener, Optional.of(t));
112+
} else {
113+
if (t instanceof Exception) {
114+
listener.onFailure((Exception) t);
115+
} else if (t instanceof VirtualMachineError) {
116+
// throw and fast fail the VM errors such as OOM (same with v2).
117+
throw t;
118+
} else {
119+
// Calcite may throw AssertError during query execution.
120+
listener.onFailure(new CalciteUnsupportedException(t.getMessage(), t));
121+
}
122+
}
123+
}
124+
},
125+
settings);
122126
}
123127

124128
public void explainWithCalcite(
125129
UnresolvedPlan plan,
126130
QueryType queryType,
127131
ResponseListener<ExecutionEngine.ExplainResponse> listener,
128132
Explain.ExplainFormat format) {
129-
try {
130-
AccessController.doPrivileged(
131-
(PrivilegedAction<Void>)
132-
() -> {
133-
CalcitePlanContext context =
134-
CalcitePlanContext.create(
135-
buildFrameworkConfig(), getQuerySizeLimit(), queryType);
136-
RelNode relNode = analyze(plan, context);
137-
RelNode optimized = optimize(relNode, context);
138-
RelNode calcitePlan = convertToCalcitePlan(optimized);
139-
executionEngine.explain(calcitePlan, format, context, listener);
140-
return null;
141-
});
142-
} catch (Throwable t) {
143-
if (isCalciteFallbackAllowed(t)) {
144-
log.warn("Fallback to V2 query engine since got exception", t);
145-
explainWithLegacy(plan, queryType, listener, format, Optional.of(t));
146-
} else {
147-
if (t instanceof Error) {
148-
// Calcite may throw AssertError during query execution.
149-
listener.onFailure(new CalciteUnsupportedException(t.getMessage()));
150-
} else {
151-
listener.onFailure((Exception) t);
152-
}
153-
}
154-
}
133+
CalcitePlanContext.run(
134+
() -> {
135+
try {
136+
AccessController.doPrivileged(
137+
(PrivilegedAction<Void>)
138+
() -> {
139+
CalcitePlanContext context =
140+
CalcitePlanContext.create(
141+
buildFrameworkConfig(), getQuerySizeLimit(), queryType);
142+
context.run(
143+
() -> {
144+
RelNode relNode = analyze(plan, context);
145+
RelNode optimized = optimize(relNode, context);
146+
RelNode calcitePlan = convertToCalcitePlan(optimized);
147+
executionEngine.explain(calcitePlan, format, context, listener);
148+
},
149+
settings);
150+
return null;
151+
});
152+
} catch (Throwable t) {
153+
if (isCalciteFallbackAllowed(t)) {
154+
log.warn("Fallback to V2 query engine since got exception", t);
155+
explainWithLegacy(plan, queryType, listener, format, Optional.of(t));
156+
} else {
157+
if (t instanceof Error) {
158+
// Calcite may throw AssertError during query execution.
159+
listener.onFailure(new CalciteUnsupportedException(t.getMessage()));
160+
} else {
161+
listener.onFailure((Exception) t);
162+
}
163+
}
164+
}
165+
},
166+
settings);
155167
}
156168

157169
public void executeWithLegacy(

core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,20 @@ protected void registerOperator(
643643
typeChecker);
644644
}
645645

646+
protected void registerDivideFunction(BuiltinFunctionName functionName) {
647+
register(
648+
functionName,
649+
(FunctionImp2)
650+
(builder, left, right) -> {
651+
SqlOperator operator =
652+
CalcitePlanContext.isLegacyPreferred()
653+
? PPLBuiltinOperators.DIVIDE
654+
: SqlLibraryOperators.SAFE_DIVIDE;
655+
return builder.makeCall(operator, left, right);
656+
},
657+
PPLTypeChecker.family(SqlTypeFamily.NUMERIC, SqlTypeFamily.NUMERIC));
658+
}
659+
646660
void populate() {
647661
// register operators for comparison
648662
registerOperator(NOTEQUAL, PPLBuiltinOperators.NOT_EQUALS_IP, SqlStdOperatorTable.NOT_EQUALS);
@@ -734,8 +748,8 @@ void populate() {
734748
registerOperator(MODULUS, PPLBuiltinOperators.MOD);
735749
registerOperator(MODULUSFUNCTION, PPLBuiltinOperators.MOD);
736750
registerOperator(CRC32, PPLBuiltinOperators.CRC32);
737-
registerOperator(DIVIDE, PPLBuiltinOperators.DIVIDE);
738-
registerOperator(DIVIDEFUNCTION, PPLBuiltinOperators.DIVIDE);
751+
registerDivideFunction(DIVIDE);
752+
registerDivideFunction(DIVIDEFUNCTION);
739753
registerOperator(SHA2, PPLBuiltinOperators.SHA2);
740754
registerOperator(CIDRMATCH, PPLBuiltinOperators.CIDRMATCH);
741755
registerOperator(INTERNAL_GROK, PPLBuiltinOperators.GROK);

docs/user/ppl/admin/settings.rst

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,9 @@ This configuration is introduced since 3.3.0 which is used to switch some behavi
173173
The behaviours it controlled includes:
174174

175175
- The default value of argument ``bucket_nullable`` in ``stats`` command. Check `stats command <../cmd/stats.rst>`_ for details.
176+
- The return value of ``divide`` and ``/`` operator. Check `expressions <../functions/expressions.rst>`_ for details.
176177

177-
Example
178+
Example 1
178179
-------
179180

180181
You can update the setting with a new value like this.
@@ -200,6 +201,22 @@ PPL query::
200201
}
201202
}
202203

204+
Example 2
205+
---------
206+
207+
Reset to default (true) by setting to null:
208+
209+
PPL query::
210+
211+
sh$ curl -sS -H 'Content-Type: application/json' \
212+
... -X PUT localhost:9200/_plugins/_query/settings \
213+
... -d '{"transient" : {"plugins.ppl.syntax.legacy.preferred" : null}}'
214+
{
215+
"acknowledged": true,
216+
"persistent": {},
217+
"transient": {}
218+
}
219+
203220
plugins.ppl.values.max.limit
204221
============================
205222

docs/user/ppl/functions/expressions.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ Arithmetic expression is an expression formed by numeric literals and binary ari
2828
1. ``+``: Add.
2929
2. ``-``: Subtract.
3030
3. ``*``: Multiply.
31-
4. ``/``: Divide. For integers, the result is an integer with fractional part discarded. Returns NULL when dividing by zero.
31+
4. ``/``: Divide. Integer operands follow the legacy truncating result when
32+
`plugins.ppl.syntax.legacy.preferred <../admin/settings.rst>`_ is ``true`` (default). When the
33+
setting is ``false`` the operands are promoted to floating point, preserving
34+
the fractional part. Division by zero still returns ``NULL``.
3235
5. ``%``: Modulo. This can be used with integers only with remainder of the division as result.
3336

3437
Precedence
@@ -172,4 +175,3 @@ NOT operator ::
172175
| 36 |
173176
| 28 |
174177
+-----+
175-
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
setup:
2+
- do:
3+
query.settings:
4+
body:
5+
transient:
6+
plugins.calcite.enabled: true
7+
plugins.ppl.syntax.legacy.preferred: true
8+
- do:
9+
indices.create:
10+
index: test_divide_settings
11+
body:
12+
settings:
13+
number_of_shards: 1
14+
number_of_replicas: 0
15+
- do:
16+
bulk:
17+
index: test_divide_settings
18+
refresh: true
19+
body:
20+
- '{"index": {}}'
21+
- '{"id": 1}'
22+
23+
---
24+
teardown:
25+
- do:
26+
query.settings:
27+
body:
28+
transient:
29+
plugins.calcite.enabled: false
30+
plugins.ppl.syntax.legacy.preferred: true
31+
- do:
32+
indices.delete:
33+
index: test_divide_settings
34+
35+
---
36+
"legacy division retains integer truncation":
37+
- skip:
38+
features:
39+
- headers
40+
- allowed_warnings
41+
- do:
42+
allowed_warnings: []
43+
headers:
44+
Content-Type: 'application/json'
45+
ppl:
46+
body:
47+
query: source=test_divide_settings | eval a=4/2 | eval b=2/4 | eval c=2/40 | fields a,b,c
48+
- match: { total: 1 }
49+
- match: { datarows: [[2,0,0]] }
50+
51+
---
52+
"non-legacy division returns floating values":
53+
- skip:
54+
features:
55+
- headers
56+
- allowed_warnings
57+
- do:
58+
query.settings:
59+
body:
60+
transient:
61+
plugins.ppl.syntax.legacy.preferred: false
62+
- do:
63+
allowed_warnings: []
64+
headers:
65+
Content-Type: 'application/json'
66+
ppl:
67+
body:
68+
query: source=test_divide_settings | eval a=4/2 | eval b=2/4 | eval c=2/40 | fields a,b,c
69+
- match: { total: 1 }
70+
- match: { datarows: [[2.0,0.5,0.05]] }

0 commit comments

Comments
 (0)