Skip to content

Commit 64c0061

Browse files
committed
SQL: Added support for string manipulating functions with more than one parameter (#32356)
Added support for string manipulating functions with more than one parameter: CONCAT, LEFT, RIGHT, REPEAT, POSITION, LOCATE, REPLACE, SUBSTRING, INSERT
1 parent 89a25a6 commit 64c0061

File tree

55 files changed

+4649
-5
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+4649
-5
lines changed

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@
6868
import org.elasticsearch.xpack.sql.expression.function.scalar.string.RTrim;
6969
import org.elasticsearch.xpack.sql.expression.function.scalar.string.Space;
7070
import org.elasticsearch.xpack.sql.expression.function.scalar.string.UCase;
71+
import org.elasticsearch.xpack.sql.expression.function.scalar.string.Concat;
72+
import org.elasticsearch.xpack.sql.expression.function.scalar.string.Insert;
73+
import org.elasticsearch.xpack.sql.expression.function.scalar.string.Left;
74+
import org.elasticsearch.xpack.sql.expression.function.scalar.string.Locate;
75+
import org.elasticsearch.xpack.sql.expression.function.scalar.string.Position;
76+
import org.elasticsearch.xpack.sql.expression.function.scalar.string.Repeat;
77+
import org.elasticsearch.xpack.sql.expression.function.scalar.string.Replace;
78+
import org.elasticsearch.xpack.sql.expression.function.scalar.string.Right;
79+
import org.elasticsearch.xpack.sql.expression.function.scalar.string.Substring;
7180
import org.elasticsearch.xpack.sql.parser.ParsingException;
7281
import org.elasticsearch.xpack.sql.tree.Location;
7382
import org.elasticsearch.xpack.sql.util.StringUtils;
@@ -154,6 +163,15 @@ public class FunctionRegistry {
154163
def(LTrim.class, LTrim::new),
155164
def(RTrim.class, RTrim::new),
156165
def(Space.class, Space::new),
166+
def(Concat.class, Concat::new),
167+
def(Insert.class, Insert::new),
168+
def(Left.class, Left::new),
169+
def(Locate.class, Locate::new),
170+
def(Position.class, Position::new),
171+
def(Repeat.class, Repeat::new),
172+
def(Replace.class, Replace::new),
173+
def(Right.class, Right::new),
174+
def(Substring.class, Substring::new),
157175
def(UCase.class, UCase::new),
158176
// Special
159177
def(Score.class, Score::new)));
@@ -337,6 +355,47 @@ private static FunctionDefinition def(Class<? extends Function> function, Functi
337355
private interface FunctionBuilder {
338356
Function build(Location location, List<Expression> children, boolean distinct, TimeZone tz);
339357
}
358+
359+
@SuppressWarnings("overloads") // These are ambiguous if you aren't using ctor references but we always do
360+
static <T extends Function> FunctionDefinition def(Class<T> function,
361+
ThreeParametersFunctionBuilder<T> ctorRef, String... aliases) {
362+
FunctionBuilder builder = (location, children, distinct, tz) -> {
363+
boolean isLocateFunction = function.isAssignableFrom(Locate.class);
364+
if (isLocateFunction && (children.size() > 3 || children.size() < 2)) {
365+
throw new IllegalArgumentException("expects two or three arguments");
366+
} else if (!isLocateFunction && children.size() != 3) {
367+
throw new IllegalArgumentException("expects exactly three arguments");
368+
}
369+
if (distinct) {
370+
throw new IllegalArgumentException("does not support DISTINCT yet it was specified");
371+
}
372+
return ctorRef.build(location, children.get(0), children.get(1), children.size() == 3 ? children.get(2) : null);
373+
};
374+
return def(function, builder, false, aliases);
375+
}
376+
377+
interface ThreeParametersFunctionBuilder<T> {
378+
T build(Location location, Expression source, Expression exp1, Expression exp2);
379+
}
380+
381+
@SuppressWarnings("overloads") // These are ambiguous if you aren't using ctor references but we always do
382+
static <T extends Function> FunctionDefinition def(Class<T> function,
383+
FourParametersFunctionBuilder<T> ctorRef, String... aliases) {
384+
FunctionBuilder builder = (location, children, distinct, tz) -> {
385+
if (children.size() != 4) {
386+
throw new IllegalArgumentException("expects exactly four arguments");
387+
}
388+
if (distinct) {
389+
throw new IllegalArgumentException("does not support DISTINCT yet it was specified");
390+
}
391+
return ctorRef.build(location, children.get(0), children.get(1), children.get(2), children.get(3));
392+
};
393+
return def(function, builder, false, aliases);
394+
}
395+
396+
interface FourParametersFunctionBuilder<T> {
397+
T build(Location location, Expression source, Expression exp1, Expression exp2, Expression exp3);
398+
}
340399

341400
private static String normalize(String name) {
342401
// translate CamelCase to camel_case

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Processors.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
33
* or more contributor license agreements. Licensed under the Elastic License;
44
* you may not use this file except in compliance with the Elastic License.
55
*/
@@ -18,6 +18,13 @@
1818
import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.HitExtractorProcessor;
1919
import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.Processor;
2020
import org.elasticsearch.xpack.sql.expression.function.scalar.string.StringProcessor;
21+
import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringNumericProcessor;
22+
import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringStringProcessor;
23+
import org.elasticsearch.xpack.sql.expression.function.scalar.string.ConcatFunctionProcessor;
24+
import org.elasticsearch.xpack.sql.expression.function.scalar.string.InsertFunctionProcessor;
25+
import org.elasticsearch.xpack.sql.expression.function.scalar.string.LocateFunctionProcessor;
26+
import org.elasticsearch.xpack.sql.expression.function.scalar.string.ReplaceFunctionProcessor;
27+
import org.elasticsearch.xpack.sql.expression.function.scalar.string.SubstringFunctionProcessor;
2128

2229
import java.util.ArrayList;
2330
import java.util.List;
@@ -49,6 +56,13 @@ public static List<NamedWriteableRegistry.Entry> getNamedWriteables() {
4956
entries.add(new Entry(Processor.class, MathProcessor.NAME, MathProcessor::new));
5057
// string
5158
entries.add(new Entry(Processor.class, StringProcessor.NAME, StringProcessor::new));
59+
entries.add(new Entry(Processor.class, BinaryStringNumericProcessor.NAME, BinaryStringNumericProcessor::new));
60+
entries.add(new Entry(Processor.class, BinaryStringStringProcessor.NAME, BinaryStringStringProcessor::new));
61+
entries.add(new Entry(Processor.class, ConcatFunctionProcessor.NAME, ConcatFunctionProcessor::new));
62+
entries.add(new Entry(Processor.class, InsertFunctionProcessor.NAME, InsertFunctionProcessor::new));
63+
entries.add(new Entry(Processor.class, LocateFunctionProcessor.NAME, LocateFunctionProcessor::new));
64+
entries.add(new Entry(Processor.class, ReplaceFunctionProcessor.NAME, ReplaceFunctionProcessor::new));
65+
entries.add(new Entry(Processor.class, SubstringFunctionProcessor.NAME, SubstringFunctionProcessor::new));
5266
return entries;
5367
}
5468
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.sql.expression.function.scalar.string;
7+
8+
import org.elasticsearch.xpack.sql.expression.Expression;
9+
import org.elasticsearch.xpack.sql.expression.FieldAttribute;
10+
import org.elasticsearch.xpack.sql.expression.function.scalar.BinaryScalarFunction;
11+
import org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate;
12+
import org.elasticsearch.xpack.sql.tree.Location;
13+
import org.elasticsearch.xpack.sql.type.DataType;
14+
import org.elasticsearch.xpack.sql.util.StringUtils;
15+
16+
import java.util.Locale;
17+
import java.util.Objects;
18+
import java.util.function.BiFunction;
19+
20+
import static java.lang.String.format;
21+
import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ParamsBuilder.paramsBuilder;
22+
import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate.formatTemplate;
23+
24+
/**
25+
* Base class for binary functions that have the first parameter a string, the second parameter a number
26+
* or a string and the result can be a string or a number.
27+
*/
28+
public abstract class BinaryStringFunction<T,R> extends BinaryScalarFunction {
29+
30+
protected BinaryStringFunction(Location location, Expression left, Expression right) {
31+
super(location, left, right);
32+
}
33+
34+
/*
35+
* the operation the binary function handles can receive one String argument, a number or String as second argument
36+
* and it can return a number or a String. The BiFunction below is the base operation for the subsequent implementations.
37+
* T is the second argument, R is the result of applying the operation.
38+
*/
39+
protected abstract BiFunction<String, T, R> operation();
40+
41+
@Override
42+
protected TypeResolution resolveType() {
43+
if (!childrenResolved()) {
44+
return new TypeResolution("Unresolved children");
45+
}
46+
47+
if (!left().dataType().isString()) {
48+
return new TypeResolution("'%s' requires first parameter to be a string type, received %s", functionName(), left().dataType());
49+
}
50+
51+
return resolveSecondParameterInputType(right().dataType());
52+
}
53+
54+
protected abstract TypeResolution resolveSecondParameterInputType(DataType inputType);
55+
56+
@Override
57+
public Object fold() {
58+
@SuppressWarnings("unchecked")
59+
T fold = (T) right().fold();
60+
return operation().apply((String) left().fold(), fold);
61+
}
62+
63+
@Override
64+
protected ScriptTemplate asScriptFrom(ScriptTemplate leftScript, ScriptTemplate rightScript) {
65+
// basically, transform the script to InternalSqlScriptUtils.[function_name](function_or_field1, function_or_field2)
66+
return new ScriptTemplate(format(Locale.ROOT, formatTemplate("{sql}.%s(%s,%s)"),
67+
StringUtils.underscoreToLowerCamelCase(operation().toString()),
68+
leftScript.template(),
69+
rightScript.template()),
70+
paramsBuilder()
71+
.script(leftScript.params()).script(rightScript.params())
72+
.build(), dataType());
73+
}
74+
75+
@Override
76+
protected ScriptTemplate asScriptFrom(FieldAttribute field) {
77+
return new ScriptTemplate(formatScript("doc[{}].value"),
78+
paramsBuilder().variable(field.isInexact() ? field.exactAttribute().name() : field.name()).build(),
79+
dataType());
80+
}
81+
82+
@Override
83+
public int hashCode() {
84+
return Objects.hash(left(), right());
85+
}
86+
87+
@Override
88+
public boolean equals(Object obj) {
89+
if (obj == null || obj.getClass() != getClass()) {
90+
return false;
91+
}
92+
BinaryStringFunction<?,?> other = (BinaryStringFunction<?,?>) obj;
93+
return Objects.equals(other.left(), left())
94+
&& Objects.equals(other.right(), right());
95+
}
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.sql.expression.function.scalar.string;
7+
8+
import org.elasticsearch.xpack.sql.expression.Expression;
9+
import org.elasticsearch.xpack.sql.tree.Location;
10+
import org.elasticsearch.xpack.sql.type.DataType;
11+
12+
/**
13+
* A binary string function with a numeric second parameter and a string result
14+
*/
15+
public abstract class BinaryStringNumericFunction extends BinaryStringFunction<Number, String> {
16+
17+
public BinaryStringNumericFunction(Location location, Expression left, Expression right) {
18+
super(location, left, right);
19+
}
20+
21+
@Override
22+
protected TypeResolution resolveSecondParameterInputType(DataType inputType) {
23+
return inputType.isNumeric() ?
24+
TypeResolution.TYPE_RESOLVED :
25+
new TypeResolution("'%s' requires second parameter to be a numeric type, received %s", functionName(), inputType);
26+
}
27+
28+
@Override
29+
public DataType dataType() {
30+
return DataType.KEYWORD;
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.sql.expression.function.scalar.string;
7+
8+
import org.elasticsearch.common.io.stream.StreamInput;
9+
import org.elasticsearch.common.io.stream.StreamOutput;
10+
import org.elasticsearch.xpack.sql.SqlIllegalArgumentException;
11+
import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.Processor;
12+
import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringNumericProcessor.BinaryStringNumericOperation;
13+
14+
import java.io.IOException;
15+
import java.util.function.BiFunction;
16+
17+
/**
18+
* Processor class covering string manipulating functions that have the first parameter as string,
19+
* second parameter as numeric and a string result.
20+
*/
21+
public class BinaryStringNumericProcessor extends BinaryStringProcessor<BinaryStringNumericOperation, Number, String> {
22+
23+
public static final String NAME = "sn";
24+
25+
public BinaryStringNumericProcessor(StreamInput in) throws IOException {
26+
super(in, i -> i.readEnum(BinaryStringNumericOperation.class));
27+
}
28+
29+
public BinaryStringNumericProcessor(Processor left, Processor right, BinaryStringNumericOperation operation) {
30+
super(left, right, operation);
31+
}
32+
33+
public enum BinaryStringNumericOperation implements BiFunction<String, Number, String> {
34+
LEFT((s,c) -> {
35+
int i = c.intValue();
36+
if (i < 0) return "";
37+
return i > s.length() ? s : s.substring(0, i);
38+
}),
39+
RIGHT((s,c) -> {
40+
int i = c.intValue();
41+
if (i < 0) return "";
42+
return i > s.length() ? s : s.substring(s.length() - i);
43+
}),
44+
REPEAT((s,c) -> {
45+
int i = c.intValue();
46+
if (i <= 0) return null;
47+
48+
StringBuilder sb = new StringBuilder(s.length() * i);
49+
for (int j = 0; j < i; j++) {
50+
sb.append(s);
51+
}
52+
return sb.toString();
53+
});
54+
55+
BinaryStringNumericOperation(BiFunction<String, Number, String> op) {
56+
this.op = op;
57+
}
58+
59+
private final BiFunction<String, Number, String> op;
60+
61+
@Override
62+
public String apply(String stringExp, Number count) {
63+
return op.apply(stringExp, count);
64+
}
65+
}
66+
67+
@Override
68+
protected void doWrite(StreamOutput out) throws IOException {
69+
out.writeEnum(operation());
70+
}
71+
72+
@Override
73+
protected Object doProcess(Object left, Object right) {
74+
if (left == null || right == null) {
75+
return null;
76+
}
77+
if (!(left instanceof String || left instanceof Character)) {
78+
throw new SqlIllegalArgumentException("A string/char is required; received [{}]", left);
79+
}
80+
if (!(right instanceof Number)) {
81+
throw new SqlIllegalArgumentException("A number is required; received [{}]", right);
82+
}
83+
84+
return operation().apply(left instanceof Character ? left.toString() : (String) left, (Number) right);
85+
}
86+
87+
@Override
88+
public String getWriteableName() {
89+
return NAME;
90+
}
91+
92+
}

0 commit comments

Comments
 (0)