Skip to content

Commit ca9f0a5

Browse files
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 elastic#29286
1 parent 30a8f9d commit ca9f0a5

File tree

13 files changed

+115
-23
lines changed

13 files changed

+115
-23
lines changed

docs/painless/painless-getting-started.asciidoc

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,27 @@ 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.script.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+
122143
[float]
123144
==== Updating Fields with Painless
124145

modules/lang-painless/build.gradle

Lines changed: 10 additions & 1 deletion
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+
import org.elasticsearch.gradle.test.RestIntegTestTask
2121

2222
esplugin {
2323
description 'An easy, safe and fast scripting language for Elasticsearch'
@@ -28,6 +28,15 @@ integTestCluster {
2828
module project.project(':modules:mapper-extras')
2929
}
3030

31+
Task additionalClusterTest = tasks.create(
32+
name: "additionalClusterTest", type: RestIntegTestTask)
33+
additionalClusterTestCluster {
34+
systemProperty 'es.script.exception_for_missing_value', 'true'
35+
distribution = 'integ-test-zip'
36+
module project // add the lang-painless module itself
37+
module project.project(':modules:mapper-extras')
38+
}
39+
3140
dependencies {
3241
compile 'org.antlr:antlr4-runtime:4.5.3'
3342
compile 'org.ow2.asm:asm-debug-all:5.1'

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

Lines changed: 1 addition & 2 deletions
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

modules/mapper-extras/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,6 @@ esplugin {
2121
description 'Adds advanced field mappers'
2222
classname 'org.elasticsearch.index.mapper.MapperExtrasPlugin'
2323
}
24+
test.configure {
25+
systemProperty 'es.script.exception_for_missing_value', 'true'
26+
}

modules/parent-join/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ esplugin {
2222
classname 'org.elasticsearch.join.ParentJoinPlugin'
2323
hasClientJar = true
2424
}
25+
test.configure {
26+
systemProperty 'es.script.exception_for_missing_value', 'true'
27+
}

modules/percolator/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,7 @@ dependencyLicenses {
3636

3737
compileJava.options.compilerArgs << "-Xlint:-deprecation,-rawtypes"
3838
compileTestJava.options.compilerArgs << "-Xlint:-deprecation,-rawtypes"
39+
40+
test.configure {
41+
systemProperty 'es.script.exception_for_missing_value', 'true'
42+
}

server/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,3 +338,7 @@ if (isEclipse == false || project.path == ":server-tests") {
338338
check.dependsOn integTest
339339
integTest.mustRunAfter test
340340
}
341+
342+
test.configure {
343+
systemProperty 'es.script.exception_for_missing_value', 'true'
344+
}

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

Lines changed: 41 additions & 3 deletions
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;
@@ -128,6 +129,10 @@ protected void resize(int newSize) {
128129

129130
public long getValue() {
130131
if (count == 0) {
132+
if (ScriptModule.EXCEPTION_FOR_MISSING_VALUE) {
133+
throw new IllegalStateException("A document doesn't have a value for a field! " +
134+
"Use doc[<field>].size()==0 to check if a document is missing a field!");
135+
}
131136
return 0L;
132137
}
133138
return values[0];
@@ -170,6 +175,10 @@ public Dates(SortedNumericDocValues in) {
170175
*/
171176
public ReadableDateTime getValue() {
172177
if (count == 0) {
178+
if (ScriptModule.EXCEPTION_FOR_MISSING_VALUE) {
179+
throw new IllegalStateException("A document doesn't have a value for a field! " +
180+
"Use doc[<field>].size()==0 to check if a document is missing a field!");
181+
}
173182
return EPOCH;
174183
}
175184
return get(0);
@@ -271,6 +280,10 @@ public SortedNumericDoubleValues getInternalValues() {
271280

272281
public double getValue() {
273282
if (count == 0) {
283+
if (ScriptModule.EXCEPTION_FOR_MISSING_VALUE) {
284+
throw new IllegalStateException("A document doesn't have a value for a field! " +
285+
"Use doc[<field>].size()==0 to check if a document is missing a field!");
286+
}
274287
return 0d;
275288
}
276289
return values[0];
@@ -327,6 +340,10 @@ protected void resize(int newSize) {
327340

328341
public GeoPoint getValue() {
329342
if (count == 0) {
343+
if (ScriptModule.EXCEPTION_FOR_MISSING_VALUE) {
344+
throw new IllegalStateException("A document doesn't have a value for a field! " +
345+
"Use doc[<field>].size()==0 to check if a document is missing a field!");
346+
}
330347
return null;
331348
}
332349
return values[0];
@@ -439,7 +456,14 @@ protected void resize(int newSize) {
439456
}
440457

441458
public boolean getValue() {
442-
return count != 0 && values[0];
459+
if (count == 0) {
460+
if (ScriptModule.EXCEPTION_FOR_MISSING_VALUE) {
461+
throw new IllegalStateException("A document doesn't have a value for a field! " +
462+
"Use doc[<field>].size()==0 to check if a document is missing a field!");
463+
}
464+
return false;
465+
}
466+
return values[0];
443467
}
444468

445469
@Override
@@ -522,7 +546,14 @@ public String get(int index) {
522546
}
523547

524548
public String getValue() {
525-
return count == 0 ? null : get(0);
549+
if (count == 0) {
550+
if (ScriptModule.EXCEPTION_FOR_MISSING_VALUE) {
551+
throw new IllegalStateException("A document doesn't have a value for a field! " +
552+
"Use doc[<field>].size()==0 to check if a document is missing a field!");
553+
}
554+
return null;
555+
}
556+
return get(0);
526557
}
527558
}
528559

@@ -543,7 +574,14 @@ public BytesRef get(int index) {
543574
}
544575

545576
public BytesRef getValue() {
546-
return count == 0 ? new BytesRef() : get(0);
577+
if (count == 0) {
578+
if (ScriptModule.EXCEPTION_FOR_MISSING_VALUE) {
579+
throw new IllegalStateException("A document doesn't have a value for a field! " +
580+
"Use doc[<field>].size()==0 to check if a document is missing a field!");
581+
}
582+
return new BytesRef();
583+
}
584+
return get(0);
547585
}
548586

549587
}

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

Lines changed: 12 additions & 0 deletions
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}.
@@ -57,6 +60,11 @@ public class ScriptModule {
5760
).collect(Collectors.toMap(c -> c.name, Function.identity()));
5861
}
5962

63+
public static final boolean EXCEPTION_FOR_MISSING_VALUE =
64+
Booleans.parseBoolean(System.getProperty("es.script.exception_for_missing_value", "false"));
65+
66+
private static final DeprecationLogger DEPRECATION_LOGGER = new DeprecationLogger(Loggers.getLogger(ScriptModule.class));
67+
6068
private final ScriptService scriptService;
6169

6270
public ScriptModule(Settings settings, List<ScriptPlugin> scriptPlugins) {
@@ -80,6 +88,10 @@ public ScriptModule(Settings settings, List<ScriptPlugin> scriptPlugins) {
8088
}
8189
}
8290
}
91+
if (EXCEPTION_FOR_MISSING_VALUE == false)
92+
DEPRECATION_LOGGER.deprecated("Script: returning default values for missing document values is deprecated. " +
93+
"Set system property '-Des.script.exception_for_missing_value=true' " +
94+
"to make behaviour compatible with future major versions.");
8395
scriptService = new ScriptService(settings, Collections.unmodifiableMap(engines), Collections.unmodifiableMap(contexts));
8496
}
8597

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,13 @@ public void test() throws IOException {
5454
for (int round = 0; round < 10; round++) {
5555
int d = between(0, values.length - 1);
5656
dates.setNextDocId(d);
57-
assertEquals(expectedDates[d].length > 0 ? expectedDates[d][0] : new DateTime(0, DateTimeZone.UTC), dates.getValue());
57+
if (expectedDates[d].length > 0) {
58+
assertEquals(expectedDates[d][0] , dates.getValue());
59+
} else {
60+
Exception e = expectThrows(IllegalStateException.class, () -> dates.getValue());
61+
assertEquals("A document doesn't have a value for a field! " +
62+
"Use doc[<field>].size()==0 to check if a document is missing a field!", e.getMessage());
63+
}
5864

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

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

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,8 @@
2121

2222
import org.elasticsearch.index.fielddata.ScriptDocValues.Longs;
2323
import org.elasticsearch.test.ESTestCase;
24-
import org.joda.time.DateTime;
25-
import org.joda.time.DateTimeZone;
26-
import org.joda.time.ReadableDateTime;
2724

2825
import java.io.IOException;
29-
import java.security.AccessControlContext;
30-
import java.security.AccessController;
31-
import java.security.PermissionCollection;
32-
import java.security.Permissions;
33-
import java.security.PrivilegedAction;
34-
import java.security.ProtectionDomain;
35-
import java.util.HashSet;
36-
import java.util.Set;
37-
import java.util.function.Consumer;
38-
39-
import static org.hamcrest.Matchers.containsInAnyOrder;
4026

4127
public class ScriptDocValuesLongsTests extends ESTestCase {
4228
public void testLongs() throws IOException {
@@ -52,8 +38,13 @@ public void testLongs() throws IOException {
5238
for (int round = 0; round < 10; round++) {
5339
int d = between(0, values.length - 1);
5440
longs.setNextDocId(d);
55-
assertEquals(values[d].length > 0 ? values[d][0] : 0, longs.getValue());
56-
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+
}
5748
assertEquals(values[d].length, longs.size());
5849
assertEquals(values[d].length, longs.getValues().size());
5950
for (int i = 0; i < values[d].length; i++) {

test/framework/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,5 @@ precommit.dependsOn namingConventionsMain
7373
test.configure {
7474
systemProperty 'tests.gradle_index_compat_versions', bwcVersions.indexCompatible.join(',')
7575
systemProperty 'tests.gradle_wire_compat_versions', bwcVersions.wireCompatible.join(',')
76+
systemProperty 'es.script.exception_for_missing_value', 'true'
7677
}

x-pack/plugin/security/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ test {
225225
* other if we allow them to set the number of available processors as it's set-once in Netty.
226226
*/
227227
systemProperty 'es.set.netty.runtime.available.processors', 'false'
228+
systemProperty 'es.script.exception_for_missing_value', 'true'
228229
}
229230

230231
// xpack modules are installed in real clusters as the meta plugin, so

0 commit comments

Comments
 (0)