Skip to content

Commit 1a75353

Browse files
Vladsz83alex-plekhanov
authored andcommitted
IGNITE-24336 SQL Calcite: Add support of user-defined table functions - Fixes #11832.
Signed-off-by: Aleksey Plekhanov <plehanov.alex@gmail.com>
1 parent 405e62b commit 1a75353

File tree

17 files changed

+807
-26
lines changed

17 files changed

+807
-26
lines changed

docs/_docs/SQL/custom-sql-func.adoc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,21 @@ Once you have deployed a cache with the above configuration, you can call the cu
4646
include::{javaFile}[tags=sql-function-query, indent=0]
4747
----
4848

49+
50+
Custom SQL function can be a table function. Result of table function is treated as a row set (a table) and can be used
51+
by other SQL operators. Custom SQL function is also a `public` method marked by annotation `@QuerySqlTableFunction`.
52+
Table function must return an `Iterable` as a row set. Each row can be represented by an `Object[]` or by a `Collection`.
53+
Row length must match the defined number of column types. Row value types must match the defined column types or be able
54+
assigned to them.
55+
56+
[source,java]
57+
----
58+
include::{javaFile}[tags=sql-table-function-example, indent=0]
59+
----
60+
[source,java]
61+
----
62+
include::{javaFile}[tags=sql-table-function-config-query, indent=0]
63+
----
64+
NOTE: The table functions also are available currently only with link:SQL/sql-calcite[Calcite, window=_blank].
65+
4966
NOTE: Classes registered with `CacheConfiguration.setSqlFunctionClasses(...)` must be added to the classpath of all the nodes where the defined custom functions might be executed. Otherwise, you will get a `ClassNotFoundException` error when trying to execute the custom function.

docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/SqlAPI.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,18 @@ public static int sqr(int x) {
165165

166166
// end::sql-function-example[]
167167

168+
// tag::sql-table-function-example[]
169+
static class SqlTableFunctions {
170+
@QuerySqlTableFunction(columnTypes = {Integer.class, String.class}, columnNames = {"INT_COL", "STR_COL"})
171+
public static Iterable<Object[]> table_function(int i) {
172+
return Arrays.asList(
173+
new Object[] {i, "" + i},
174+
new Object[] {i * 10, "empty"}
175+
);
176+
}
177+
}
178+
// end::sql-table-function-example[]
179+
168180
@Test
169181
IgniteCache setSqlFunction(Ignite ignite) {
170182

@@ -181,6 +193,23 @@ IgniteCache setSqlFunction(Ignite ignite) {
181193
return cache;
182194
}
183195

196+
@Test
197+
IgniteCache testSqlTableFunction(Ignite ignite) {
198+
// tag::sql-table-function-config-query[]
199+
CacheConfiguration cfg = new CacheConfiguration("myCache");
200+
201+
cfg.setSqlFunctionClasses(SqlTableFunctions.class);
202+
203+
IgniteCache cache = ignite.createCache(cfg);
204+
205+
SqlFieldsQuery query = new SqlFieldsQuery("SELECT STR_COL FROM TABLE_FUNCTION(10) WHERE INT_COL > 50");
206+
207+
cache.query(query).getAll();
208+
// end::sql-table-function-config-query[]
209+
210+
return cache;
211+
}
212+
184213
void call(IgniteCache cache) {
185214

186215
// tag::sql-function-query[]

modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -662,13 +662,13 @@ else if (rel instanceof Intersect)
662662

663663
/** {@inheritDoc} */
664664
@Override public Node<Row> visit(IgniteTableFunctionScan rel) {
665-
Supplier<Iterable<Object[]>> dataSupplier = expressionFactory.execute(rel.getCall());
665+
Supplier<Iterable<?>> dataSupplier = expressionFactory.execute(rel.getCall());
666666

667667
RelDataType rowType = rel.getRowType();
668668

669669
RowFactory<Row> rowFactory = ctx.rowHandler().factory(ctx.getTypeFactory(), rowType);
670670

671-
return new ScanNode<>(ctx, rowType, new TableFunctionScan<>(dataSupplier, rowFactory));
671+
return new ScanNode<>(ctx, rowType, new TableFunctionScan<>(rowType, dataSupplier, rowFactory));
672672
}
673673

674674
/** {@inheritDoc} */

modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/TableFunctionScan.java

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,55 @@
1717

1818
package org.apache.ignite.internal.processors.query.calcite.exec;
1919

20+
import java.util.Collection;
2021
import java.util.Iterator;
2122
import java.util.function.Supplier;
23+
import org.apache.calcite.rel.type.RelDataType;
24+
import org.apache.ignite.internal.processors.query.IgniteSQLException;
2225
import org.apache.ignite.internal.processors.query.calcite.exec.RowHandler.RowFactory;
2326
import org.apache.ignite.internal.util.typedef.F;
2427

2528
/** */
2629
public class TableFunctionScan<Row> implements Iterable<Row> {
2730
/** */
28-
private final Supplier<Iterable<Object[]>> dataSupplier;
31+
private final RelDataType rowType;
32+
33+
/** */
34+
private final Supplier<Iterable<?>> dataSupplier;
2935

3036
/** */
3137
private final RowFactory<Row> rowFactory;
3238

3339
/** */
3440
public TableFunctionScan(
35-
Supplier<Iterable<Object[]>> dataSupplier,
41+
RelDataType rowType,
42+
Supplier<Iterable<?>> dataSupplier,
3643
RowFactory<Row> rowFactory
3744
) {
45+
this.rowType = rowType;
3846
this.dataSupplier = dataSupplier;
3947
this.rowFactory = rowFactory;
4048
}
4149

4250
/** {@inheritDoc} */
4351
@Override public Iterator<Row> iterator() {
44-
return F.iterator(dataSupplier.get(), rowFactory::create, true);
52+
return F.iterator(dataSupplier.get(), this::convertToRow, true);
53+
}
54+
55+
/** */
56+
private Row convertToRow(Object rowContainer) {
57+
if (rowContainer.getClass() != Object[].class && !Collection.class.isAssignableFrom(rowContainer.getClass()))
58+
throw new IgniteSQLException("Unable to process table function data: row type is neither Collection or Object[].");
59+
60+
Object[] rowArr = rowContainer.getClass() == Object[].class
61+
? (Object[])rowContainer
62+
: ((Collection<?>)rowContainer).toArray();
63+
64+
if (rowArr.length != rowType.getFieldCount()) {
65+
throw new IgniteSQLException("Unable to process table function data: row length [" + rowArr.length
66+
+ "] doesn't match defined columns number [" + rowType.getFieldCount() + "].");
67+
}
68+
69+
return rowFactory.create(rowArr);
4570
}
4671
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.ignite.internal.processors.query.calcite.exec.exp;
18+
19+
import java.lang.reflect.Method;
20+
import org.apache.calcite.schema.impl.ReflectiveFunctionBase;
21+
22+
/** A base for outer java-method functions. */
23+
abstract class IgniteReflectiveFunctionBase extends ReflectiveFunctionBase implements ImplementableFunction {
24+
/** */
25+
protected final CallImplementor implementor;
26+
27+
/** */
28+
protected IgniteReflectiveFunctionBase(Method method, CallImplementor implementor) {
29+
super(method);
30+
31+
this.implementor = implementor;
32+
}
33+
34+
/** {@inheritDoc} */
35+
@Override public CallImplementor getImplementor() {
36+
return implementor;
37+
}
38+
}

modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/IgniteScalarFunction.java

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,16 @@
2121
import org.apache.calcite.rel.type.RelDataType;
2222
import org.apache.calcite.rel.type.RelDataTypeFactory;
2323
import org.apache.calcite.schema.ScalarFunction;
24-
import org.apache.calcite.schema.impl.ReflectiveFunctionBase;
2524

2625
/**
2726
* Implementation of {@link ScalarFunction} for Ignite user defined functions.
2827
*/
29-
public class IgniteScalarFunction extends ReflectiveFunctionBase implements ScalarFunction, ImplementableFunction {
30-
/** Implementor. */
31-
private final CallImplementor implementor;
32-
28+
public class IgniteScalarFunction extends IgniteReflectiveFunctionBase implements ScalarFunction {
3329
/**
3430
* Private constructor.
3531
*/
3632
private IgniteScalarFunction(Method method, CallImplementor implementor) {
37-
super(method);
38-
39-
this.implementor = implementor;
33+
super(method, implementor);
4034
}
4135

4236
/**
@@ -56,9 +50,4 @@ public static ScalarFunction create(Method method) {
5650
@Override public RelDataType getReturnType(RelDataTypeFactory typeFactory) {
5751
return typeFactory.createJavaType(method.getReturnType());
5852
}
59-
60-
/** {@inheritDoc} */
61-
@Override public CallImplementor getImplementor() {
62-
return implementor;
63-
}
6453
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.ignite.internal.processors.query.calcite.exec.exp;
18+
19+
import java.lang.reflect.Method;
20+
import java.lang.reflect.Type;
21+
import java.util.Arrays;
22+
import java.util.HashSet;
23+
import java.util.List;
24+
import java.util.stream.Collectors;
25+
import java.util.stream.Stream;
26+
import org.apache.calcite.adapter.enumerable.NullPolicy;
27+
import org.apache.calcite.adapter.java.JavaTypeFactory;
28+
import org.apache.calcite.rel.type.RelDataType;
29+
import org.apache.calcite.rel.type.RelDataTypeFactory;
30+
import org.apache.calcite.schema.TableFunction;
31+
import org.apache.ignite.cache.query.annotations.QuerySqlTableFunction;
32+
import org.apache.ignite.internal.processors.query.IgniteSQLException;
33+
import org.apache.ignite.internal.util.typedef.F;
34+
35+
/**
36+
* Holder of user-defined table function.
37+
*
38+
* @see QuerySqlTableFunction
39+
*/
40+
public class IgniteTableFunction extends IgniteReflectiveFunctionBase implements TableFunction {
41+
/** Column types of the returned table representation. */
42+
private final Class<?>[] colTypes;
43+
44+
/** Column names of the returned table representation. */
45+
private final List<String> colNames;
46+
47+
/**
48+
* Creates user-defined table function holder.
49+
*
50+
* @param method The implementatng method.
51+
* @param colTypes Column types of the returned table representation.
52+
* @param colNames Column names of the returned table representation.
53+
* @param implementor Call implementor.
54+
*/
55+
private IgniteTableFunction(Method method, Class<?>[] colTypes, String[] colNames, CallImplementor implementor) {
56+
super(method, implementor);
57+
58+
validate(method, colTypes, colNames);
59+
60+
this.colTypes = colTypes;
61+
this.colNames = Arrays.asList(colNames);
62+
}
63+
64+
/**
65+
* Creates user-defined table function implementor and holder.
66+
*
67+
* @param method The implementating method.
68+
* @param colTypes Column types of the returned table representation.
69+
* @param colNames Column names of the returned table representation.
70+
*/
71+
public static IgniteTableFunction create(Method method, Class<?>[] colTypes, String[] colNames) {
72+
NotNullImplementor implementor = new ReflectiveCallNotNullImplementor(method);
73+
74+
CallImplementor impl = RexImpTable.createImplementor(implementor, NullPolicy.NONE, false);
75+
76+
return new IgniteTableFunction(method, colTypes, colNames, impl);
77+
}
78+
79+
/** {@inheritDoc} */
80+
@Override public RelDataType getRowType(RelDataTypeFactory typeFactory, List<?> arguments) {
81+
JavaTypeFactory tf = (JavaTypeFactory)typeFactory;
82+
83+
List<RelDataType> converted = Stream.of(colTypes).map(cl -> tf.toSql(tf.createType(cl))).collect(Collectors.toList());
84+
85+
return typeFactory.createStructType(converted, colNames);
86+
}
87+
88+
/** {@inheritDoc} */
89+
@Override public Type getElementType(List<?> arguments) {
90+
// Calcite's {@link TableFunctionImpl} does real invocation ({@link TableFunctionImpl#apply(List)}) to determine
91+
// the type. The call might be long, 'heavy', may affect some metrics and should not be executed at validation/planning.
92+
// We may check the argument number here but not their types. The types might be wrong, but converted further.
93+
if (F.isEmpty(arguments) && !F.isEmpty(method.getParameterTypes())
94+
|| F.isEmpty(method.getParameterTypes()) && !F.isEmpty(arguments)
95+
|| method.getParameterTypes().length != arguments.size()) {
96+
throw new IllegalArgumentException("Wrong arguments number: " + arguments.size() + ". Expected: "
97+
+ method.getParameterTypes().length + '.');
98+
}
99+
100+
return Iterable.class;
101+
}
102+
103+
/** Validates the parameters and throws an exception if it finds an incorrect parameter. */
104+
private static void validate(Method mtd, Class<?>[] colTypes, String[] colNames) {
105+
if (F.isEmpty(colTypes))
106+
raiseValidationError(mtd, "Column types cannot be empty.");
107+
108+
if (F.isEmpty(colNames))
109+
raiseValidationError(mtd, "Column names cannot be empty.");
110+
111+
if (colTypes.length != colNames.length) {
112+
raiseValidationError(mtd, "Number of the table column names [" + colNames.length
113+
+ "] must match the number of column types [" + colTypes.length + "].");
114+
}
115+
116+
if (new HashSet<>(Arrays.asList(colNames)).size() != colNames.length)
117+
raiseValidationError(mtd, "One or more column names is not unique.");
118+
119+
if (!Iterable.class.isAssignableFrom(mtd.getReturnType()))
120+
raiseValidationError(mtd, "The method is expected to return a collection (iterable).");
121+
}
122+
123+
/**
124+
* Throws a parameter validation exception with a standard text prefix.
125+
*
126+
* @param method A java-method implementing related user-defined table function.
127+
* @param errPostfix Error text postfix.
128+
*/
129+
private static void raiseValidationError(Method method, String errPostfix) {
130+
String mtdSign = method.getName() + '(' + Stream.of(method.getParameterTypes()).map(Class::getSimpleName)
131+
.collect(Collectors.joining(", ")) + ')';
132+
133+
throw new IgniteSQLException("Unable to create table function for method '" + mtdSign + "'. " + errPostfix);
134+
}
135+
}

modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SchemaHolderImpl.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.apache.ignite.internal.processors.query.QueryField;
4343
import org.apache.ignite.internal.processors.query.QueryUtils;
4444
import org.apache.ignite.internal.processors.query.calcite.exec.exp.IgniteScalarFunction;
45+
import org.apache.ignite.internal.processors.query.calcite.exec.exp.IgniteTableFunction;
4546
import org.apache.ignite.internal.processors.query.calcite.trait.TraitUtils;
4647
import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
4748
import org.apache.ignite.internal.processors.query.calcite.util.AbstractService;
@@ -357,6 +358,21 @@ private static Object affinityIdentity(CacheConfiguration<?, ?> ccfg) {
357358
rebuild();
358359
}
359360

361+
/** {@inheritDoc} */
362+
@Override public void onTableFunctionCreated(
363+
String schemaName,
364+
String name,
365+
Method method,
366+
Class<?>[] colTypes,
367+
String[] colNames
368+
) {
369+
IgniteSchema schema = igniteSchemas.computeIfAbsent(schemaName, IgniteSchema::new);
370+
371+
schema.addFunction(name.toUpperCase(), IgniteTableFunction.create(method, colTypes, colNames));
372+
373+
rebuild();
374+
}
375+
360376
/** {@inheritDoc} */
361377
@Override public void onSystemViewCreated(String schemaName, SystemView<?> sysView) {
362378
IgniteSchema schema = igniteSchemas.computeIfAbsent(schemaName, IgniteSchema::new);

modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AbstractBasicIntegrationTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,5 +334,18 @@ public Employer(String name, Double salary) {
334334
this.name = name;
335335
this.salary = salary;
336336
}
337+
338+
/** {@inheritDoc} */
339+
@Override public boolean equals(Object o) {
340+
if (this == o)
341+
return true;
342+
343+
if (o == null || getClass() != o.getClass())
344+
return false;
345+
346+
Employer employer = (Employer)o;
347+
348+
return name.equals(employer.name) && salary.equals(employer.salary);
349+
}
337350
}
338351
}

0 commit comments

Comments
 (0)