Skip to content

Commit 5481fbc

Browse files
Handle missing values in painless (#30975)
* Handle missing values in painless Throw an exception for `doc['field'].value` if this document is missing a value for the `field`. For 7.0: This is the default behaviour from 7.0 For 6.x: To enable this behavior from 6.x, a user can set a jvm.option: `-Des.script.exception_for_missing_value=true` on a node. If a user does not enable this behavior, a deprecation warning is logged on start up. Closes #29286
1 parent 584fa26 commit 5481fbc

File tree

11 files changed

+303
-17
lines changed

11 files changed

+303
-17
lines changed

buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy

+1
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,7 @@ class BuildPlugin implements Plugin<Project> {
691691
systemProperty 'tests.task', path
692692
systemProperty 'tests.security.manager', 'true'
693693
systemProperty 'jna.nosys', 'true'
694+
systemProperty 'es.scripting.exception_for_missing_value', 'true'
694695
// TODO: remove setting logging level via system property
695696
systemProperty 'tests.logger.level', 'WARN'
696697
for (Map.Entry<String, String> property : System.properties.entrySet()) {

docs/painless/painless-getting-started.asciidoc

+24
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,30 @@ GET hockey/_search
119119
----------------------------------------------------------------
120120
// CONSOLE
121121

122+
123+
[float]
124+
===== Missing values
125+
126+
If you request the value from a field `field` that isn’t in
127+
the document, `doc['field'].value` for this document returns:
128+
129+
- `0` if a `field` has a numeric datatype (long, double etc.)
130+
- `false` is a `field` has a boolean datatype
131+
- epoch date if a `field` has a date datatype
132+
- `null` if a `field` has a string datatype
133+
- `null` if a `field` has a geo datatype
134+
- `""` if a `field` has a binary datatype
135+
136+
IMPORTANT: Starting in 7.0, `doc['field'].value` throws an exception if
137+
the field is missing in a document. To enable this behavior now,
138+
set a <<jvm-options,`jvm.option`>>
139+
`-Des.scripting.exception_for_missing_value=true` on a node. If you do not enable
140+
this behavior, a deprecation warning is logged on start up.
141+
142+
To check if a document is missing a value, you can call
143+
`doc['field'].size() == 0`.
144+
145+
122146
[float]
123147
==== Updating Fields with Painless
124148

modules/lang-painless/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
* under the License.
1818
*/
1919

20-
import org.apache.tools.ant.types.Path
20+
2121

2222
esplugin {
2323
description 'An easy, safe and fast scripting language for Elasticsearch'

modules/lang-painless/src/test/resources/rest-api-spec/test/painless/20_scriptfield.yml

+1-2
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,13 @@ setup:
8888

8989
---
9090
"Scripted Field with a null safe dereference (null)":
91-
# Change this to ?: once we have it implemented
9291
- do:
9392
search:
9493
body:
9594
script_fields:
9695
bar:
9796
script:
98-
source: "(doc['missing'].value?.length() ?: 0) + params.x;"
97+
source: "(doc['missing'].size() == 0 ? 0 : doc['missing'].value.length()) + params.x;"
9998
params:
10099
x: 5
101100

server/build.gradle

+11-1
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ if (isEclipse == false || project.path == ":server-tests") {
329329
task integTest(type: RandomizedTestingTask,
330330
group: JavaBasePlugin.VERIFICATION_GROUP,
331331
description: 'Multi-node tests',
332-
dependsOn: test.dependsOn) {
332+
dependsOn: test.dependsOn.collect()) {
333333
configure(BuildPlugin.commonTestConfig(project))
334334
classpath = project.test.classpath
335335
testClassesDirs = project.test.testClassesDirs
@@ -338,3 +338,13 @@ if (isEclipse == false || project.path == ":server-tests") {
338338
check.dependsOn integTest
339339
integTest.mustRunAfter test
340340
}
341+
342+
// TODO: remove ScriptDocValuesMissingV6BehaviourTests in 7.0
343+
additionalTest('testScriptDocValuesMissingV6Behaviour'){
344+
include '**/ScriptDocValuesMissingV6BehaviourTests.class'
345+
systemProperty 'es.scripting.exception_for_missing_value', 'false'
346+
}
347+
test {
348+
// these are tested explicitly in separate test tasks
349+
exclude '**/*ScriptDocValuesMissingV6BehaviourTests.class'
350+
}

server/src/main/java/org/elasticsearch/index/fielddata/ScriptDocValues.java

+41-3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.elasticsearch.common.geo.GeoUtils;
3030
import org.elasticsearch.common.logging.DeprecationLogger;
3131
import org.elasticsearch.common.logging.ESLoggerFactory;
32+
import org.elasticsearch.script.ScriptModule;
3233
import org.joda.time.DateTime;
3334
import org.joda.time.DateTimeZone;
3435
import org.joda.time.MutableDateTime;
@@ -125,6 +126,10 @@ protected void resize(int newSize) {
125126

126127
public long getValue() {
127128
if (count == 0) {
129+
if (ScriptModule.EXCEPTION_FOR_MISSING_VALUE) {
130+
throw new IllegalStateException("A document doesn't have a value for a field! " +
131+
"Use doc[<field>].size()==0 to check if a document is missing a field!");
132+
}
128133
return 0L;
129134
}
130135
return values[0];
@@ -167,6 +172,10 @@ public Dates(SortedNumericDocValues in) {
167172
*/
168173
public ReadableDateTime getValue() {
169174
if (count == 0) {
175+
if (ScriptModule.EXCEPTION_FOR_MISSING_VALUE) {
176+
throw new IllegalStateException("A document doesn't have a value for a field! " +
177+
"Use doc[<field>].size()==0 to check if a document is missing a field!");
178+
}
170179
return EPOCH;
171180
}
172181
return get(0);
@@ -268,6 +277,10 @@ public SortedNumericDoubleValues getInternalValues() {
268277

269278
public double getValue() {
270279
if (count == 0) {
280+
if (ScriptModule.EXCEPTION_FOR_MISSING_VALUE) {
281+
throw new IllegalStateException("A document doesn't have a value for a field! " +
282+
"Use doc[<field>].size()==0 to check if a document is missing a field!");
283+
}
271284
return 0d;
272285
}
273286
return values[0];
@@ -324,6 +337,10 @@ protected void resize(int newSize) {
324337

325338
public GeoPoint getValue() {
326339
if (count == 0) {
340+
if (ScriptModule.EXCEPTION_FOR_MISSING_VALUE) {
341+
throw new IllegalStateException("A document doesn't have a value for a field! " +
342+
"Use doc[<field>].size()==0 to check if a document is missing a field!");
343+
}
327344
return null;
328345
}
329346
return values[0];
@@ -436,7 +453,14 @@ protected void resize(int newSize) {
436453
}
437454

438455
public boolean getValue() {
439-
return count != 0 && values[0];
456+
if (count == 0) {
457+
if (ScriptModule.EXCEPTION_FOR_MISSING_VALUE) {
458+
throw new IllegalStateException("A document doesn't have a value for a field! " +
459+
"Use doc[<field>].size()==0 to check if a document is missing a field!");
460+
}
461+
return false;
462+
}
463+
return values[0];
440464
}
441465

442466
@Override
@@ -519,7 +543,14 @@ public String get(int index) {
519543
}
520544

521545
public String getValue() {
522-
return count == 0 ? null : get(0);
546+
if (count == 0) {
547+
if (ScriptModule.EXCEPTION_FOR_MISSING_VALUE) {
548+
throw new IllegalStateException("A document doesn't have a value for a field! " +
549+
"Use doc[<field>].size()==0 to check if a document is missing a field!");
550+
}
551+
return null;
552+
}
553+
return get(0);
523554
}
524555
}
525556

@@ -540,7 +571,14 @@ public BytesRef get(int index) {
540571
}
541572

542573
public BytesRef getValue() {
543-
return count == 0 ? new BytesRef() : get(0);
574+
if (count == 0) {
575+
if (ScriptModule.EXCEPTION_FOR_MISSING_VALUE) {
576+
throw new IllegalStateException("A document doesn't have a value for a field! " +
577+
"Use doc[<field>].size()==0 to check if a document is missing a field!");
578+
}
579+
return new BytesRef();
580+
}
581+
return get(0);
544582
}
545583

546584
}

server/src/main/java/org/elasticsearch/script/ScriptModule.java

+12
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
import org.elasticsearch.common.settings.Settings;
3232
import org.elasticsearch.plugins.ScriptPlugin;
3333
import org.elasticsearch.search.aggregations.pipeline.movfn.MovingFunctionScript;
34+
import org.elasticsearch.common.Booleans;
35+
import org.elasticsearch.common.logging.DeprecationLogger;
36+
import org.elasticsearch.common.logging.Loggers;
3437

3538
/**
3639
* Manages building {@link ScriptService}.
@@ -61,6 +64,11 @@ public class ScriptModule {
6164
).collect(Collectors.toMap(c -> c.name, Function.identity()));
6265
}
6366

67+
public static final boolean EXCEPTION_FOR_MISSING_VALUE =
68+
Booleans.parseBoolean(System.getProperty("es.scripting.exception_for_missing_value", "false"));
69+
70+
private static final DeprecationLogger DEPRECATION_LOGGER = new DeprecationLogger(Loggers.getLogger(ScriptModule.class));
71+
6472
private final ScriptService scriptService;
6573

6674
public ScriptModule(Settings settings, List<ScriptPlugin> scriptPlugins) {
@@ -84,6 +92,10 @@ public ScriptModule(Settings settings, List<ScriptPlugin> scriptPlugins) {
8492
}
8593
}
8694
}
95+
if (EXCEPTION_FOR_MISSING_VALUE == false)
96+
DEPRECATION_LOGGER.deprecated("Script: returning default values for missing document values is deprecated. " +
97+
"Set system property '-Des.scripting.exception_for_missing_value=true' " +
98+
"to make behaviour compatible with future major versions.");
8799
scriptService = new ScriptService(settings, Collections.unmodifiableMap(engines), Collections.unmodifiableMap(contexts));
88100
}
89101

server/src/test/java/org/elasticsearch/index/fielddata/ScriptDocValuesDatesTests.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,13 @@ public void test() throws IOException {
4444
for (int round = 0; round < 10; round++) {
4545
int d = between(0, values.length - 1);
4646
dates.setNextDocId(d);
47-
assertEquals(expectedDates[d].length > 0 ? expectedDates[d][0] : new DateTime(0, DateTimeZone.UTC), dates.getValue());
47+
if (expectedDates[d].length > 0) {
48+
assertEquals(expectedDates[d][0] , dates.getValue());
49+
} else {
50+
Exception e = expectThrows(IllegalStateException.class, () -> dates.getValue());
51+
assertEquals("A document doesn't have a value for a field! " +
52+
"Use doc[<field>].size()==0 to check if a document is missing a field!", e.getMessage());
53+
}
4854

4955
assertEquals(values[d].length, dates.size());
5056
for (int i = 0; i < values[d].length; i++) {

server/src/test/java/org/elasticsearch/index/fielddata/ScriptDocValuesLongsTests.java

+7-3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424

2525
import java.io.IOException;
2626

27-
2827
public class ScriptDocValuesLongsTests extends ESTestCase {
2928
public void testLongs() throws IOException {
3029
long[][] values = new long[between(3, 10)][];
@@ -39,8 +38,13 @@ public void testLongs() throws IOException {
3938
for (int round = 0; round < 10; round++) {
4039
int d = between(0, values.length - 1);
4140
longs.setNextDocId(d);
42-
assertEquals(values[d].length > 0 ? values[d][0] : 0, longs.getValue());
43-
41+
if (values[d].length > 0) {
42+
assertEquals(values[d][0], longs.getValue());
43+
} else {
44+
Exception e = expectThrows(IllegalStateException.class, () -> longs.getValue());
45+
assertEquals("A document doesn't have a value for a field! " +
46+
"Use doc[<field>].size()==0 to check if a document is missing a field!", e.getMessage());
47+
}
4448
assertEquals(values[d].length, longs.size());
4549
assertEquals(values[d].length, longs.getValues().size());
4650
for (int i = 0; i < values[d].length; i++) {

0 commit comments

Comments
 (0)