blob: 9c931e86bb55ed586843b01b0da011c3a6f79419 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.cql3.validation.entities;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.UUID;
import org.junit.Assert;
import org.junit.Test;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.cql3.functions.FunctionName;
import org.apache.cassandra.exceptions.FunctionExecutionException;
import org.apache.cassandra.utils.UUIDGen;
public class UFScriptTest extends CQLTester
{
// Just JavaScript UDFs to check how UDF - especially security/class-loading/sandboxing stuff -
// behaves, if no Java UDF has been executed before.
// Do not add any other test here - especially none using Java UDFs
@Test
public void testJavascriptSimpleCollections() throws Throwable
{
createTable("CREATE TABLE %s (key int primary key, lst list<double>, st set<text>, mp map<int, boolean>)");
String fName1 = createFunction(KEYSPACE_PER_TEST, "list<double>",
"CREATE FUNCTION %s( lst list<double> ) " +
"RETURNS NULL ON NULL INPUT " +
"RETURNS list<double> " +
"LANGUAGE javascript\n" +
"AS 'lst;';");
String fName2 = createFunction(KEYSPACE_PER_TEST, "set<text>",
"CREATE FUNCTION %s( st set<text> ) " +
"RETURNS NULL ON NULL INPUT " +
"RETURNS set<text> " +
"LANGUAGE javascript\n" +
"AS 'st;';");
String fName3 = createFunction(KEYSPACE_PER_TEST, "map<int, boolean>",
"CREATE FUNCTION %s( mp map<int, boolean> ) " +
"RETURNS NULL ON NULL INPUT " +
"RETURNS map<int, boolean> " +
"LANGUAGE javascript\n" +
"AS 'mp;';");
List<Double> list = Arrays.asList(1d, 2d, 3d);
Set<String> set = new TreeSet<>(Arrays.asList("one", "three", "two"));
Map<Integer, Boolean> map = new TreeMap<>();
map.put(1, true);
map.put(2, false);
map.put(3, true);
execute("INSERT INTO %s (key, lst, st, mp) VALUES (1, ?, ?, ?)", list, set, map);
assertRows(execute("SELECT lst, st, mp FROM %s WHERE key = 1"),
row(list, set, map));
assertRows(execute("SELECT " + fName1 + "(lst), " + fName2 + "(st), " + fName3 + "(mp) FROM %s WHERE key = 1"),
row(list, set, map));
for (int version : PROTOCOL_VERSIONS)
assertRowsNet(version,
executeNet(version, "SELECT " + fName1 + "(lst), " + fName2 + "(st), " + fName3 + "(mp) FROM %s WHERE key = 1"),
row(list, set, map));
}
@Test
public void testJavascriptTupleType() throws Throwable
{
createTable("CREATE TABLE %s (key int primary key, tup frozen<tuple<double, text, int, boolean>>)");
String fName = createFunction(KEYSPACE_PER_TEST, "tuple<double, text, int, boolean>",
"CREATE FUNCTION %s( tup tuple<double, text, int, boolean> ) " +
"RETURNS NULL ON NULL INPUT " +
"RETURNS tuple<double, text, int, boolean> " +
"LANGUAGE javascript\n" +
"AS $$tup;$$;");
Object t = tuple(1d, "foo", 2, true);
execute("INSERT INTO %s (key, tup) VALUES (1, ?)", t);
assertRows(execute("SELECT tup FROM %s WHERE key = 1"),
row(t));
assertRows(execute("SELECT " + fName + "(tup) FROM %s WHERE key = 1"),
row(t));
}
@Test
public void testJavascriptUserType() throws Throwable
{
String type = createType("CREATE TYPE %s (txt text, i int)");
createTable("CREATE TABLE %s (key int primary key, udt frozen<" + type + ">)");
String fUdt1 = createFunction(KEYSPACE, type,
"CREATE FUNCTION %s( udt " + type + " ) " +
"RETURNS NULL ON NULL INPUT " +
"RETURNS " + type + ' ' +
"LANGUAGE javascript\n" +
"AS $$" +
" udt;$$;");
String fUdt2 = createFunction(KEYSPACE, type,
"CREATE FUNCTION %s( udt " + type + " ) " +
"RETURNS NULL ON NULL INPUT " +
"RETURNS text " +
"LANGUAGE javascript\n" +
"AS $$" +
" udt.getString(\"txt\");$$;");
String fUdt3 = createFunction(KEYSPACE, type,
"CREATE FUNCTION %s( udt " + type + " ) " +
"RETURNS NULL ON NULL INPUT " +
"RETURNS int " +
"LANGUAGE javascript\n" +
"AS $$" +
" udt.getInt(\"i\");$$;");
execute("INSERT INTO %s (key, udt) VALUES (1, {txt: 'one', i:1})");
UntypedResultSet rows = execute("SELECT " + fUdt1 + "(udt) FROM %s WHERE key = 1");
Assert.assertEquals(1, rows.size());
assertRows(execute("SELECT " + fUdt2 + "(udt) FROM %s WHERE key = 1"),
row("one"));
assertRows(execute("SELECT " + fUdt3 + "(udt) FROM %s WHERE key = 1"),
row(1));
}
@Test
public void testJavascriptUTCollections() throws Throwable
{
String type = createType("CREATE TYPE %s (txt text, i int)");
createTable(String.format("CREATE TABLE %%s " +
"(key int primary key, lst list<frozen<%s>>, st set<frozen<%s>>, mp map<int, frozen<%s>>)",
type, type, type));
String fName = createFunction(KEYSPACE, "list<frozen<" + type + ">>",
"CREATE FUNCTION %s( lst list<frozen<" + type + ">> ) " +
"RETURNS NULL ON NULL INPUT " +
"RETURNS text " +
"LANGUAGE javascript\n" +
"AS $$" +
" lst.get(1).getString(\"txt\");$$;");
createFunctionOverload(fName, "set<frozen<" + type + ">>",
"CREATE FUNCTION %s( st set<frozen<" + type + ">> ) " +
"RETURNS NULL ON NULL INPUT " +
"RETURNS text " +
"LANGUAGE javascript\n" +
"AS $$" +
" st.iterator().next().getString(\"txt\");$$;");
createFunctionOverload(fName, "map<int, frozen<" + type + ">>",
"CREATE FUNCTION %s( mp map<int, frozen<" + type + ">> ) " +
"RETURNS NULL ON NULL INPUT " +
"RETURNS text " +
"LANGUAGE javascript\n" +
"AS $$" +
" mp.get(java.lang.Integer.valueOf(3)).getString(\"txt\");$$;");
execute("INSERT INTO %s (key, lst, st, mp) values (1, " +
// list<frozen<UDT>>
"[ {txt: 'one', i:1}, {txt: 'three', i:1}, {txt: 'one', i:1} ] , " +
// set<frozen<UDT>>
"{ {txt: 'one', i:1}, {txt: 'three', i:3}, {txt: 'two', i:2} }, " +
// map<int, frozen<UDT>>
"{ 1: {txt: 'one', i:1}, 2: {txt: 'one', i:3}, 3: {txt: 'two', i:2} })");
assertRows(execute("SELECT " + fName + "(lst) FROM %s WHERE key = 1"),
row("three"));
assertRows(execute("SELECT " + fName + "(st) FROM %s WHERE key = 1"),
row("one"));
assertRows(execute("SELECT " + fName + "(mp) FROM %s WHERE key = 1"),
row("two"));
String cqlSelect = "SELECT " + fName + "(lst), " + fName + "(st), " + fName + "(mp) FROM %s WHERE key = 1";
assertRows(execute(cqlSelect),
row("three", "one", "two"));
// same test - but via native protocol
for (int version : PROTOCOL_VERSIONS)
assertRowsNet(version,
executeNet(version, cqlSelect),
row("three", "one", "two"));
}
@Test
public void testJavascriptFunction() throws Throwable
{
createTable("CREATE TABLE %s (key int primary key, val double)");
String functionBody = '\n' +
" Math.sin(val);\n";
String fName = createFunction(KEYSPACE, "double",
"CREATE OR REPLACE FUNCTION %s(val double) " +
"RETURNS NULL ON NULL INPUT " +
"RETURNS double " +
"LANGUAGE javascript\n" +
"AS '" + functionBody + "';");
FunctionName fNameName = parseFunctionName(fName);
assertRows(execute("SELECT language, body FROM system_schema.functions WHERE keyspace_name=? AND function_name=?",
fNameName.keyspace, fNameName.name),
row("javascript", functionBody));
execute("INSERT INTO %s (key, val) VALUES (?, ?)", 1, 1d);
execute("INSERT INTO %s (key, val) VALUES (?, ?)", 2, 2d);
execute("INSERT INTO %s (key, val) VALUES (?, ?)", 3, 3d);
assertRows(execute("SELECT key, val, " + fName + "(val) FROM %s"),
row(1, 1d, Math.sin(1d)),
row(2, 2d, Math.sin(2d)),
row(3, 3d, Math.sin(3d))
);
}
@Test
public void testJavascriptBadReturnType() throws Throwable
{
createTable("CREATE TABLE %s (key int primary key, val double)");
String fName = createFunction(KEYSPACE, "double",
"CREATE OR REPLACE FUNCTION %s(val double) " +
"RETURNS NULL ON NULL INPUT " +
"RETURNS double " +
"LANGUAGE javascript\n" +
"AS '\"string\";';");
execute("INSERT INTO %s (key, val) VALUES (?, ?)", 1, 1d);
// throws IRE with ClassCastException
assertInvalidMessage("Invalid value for CQL type double", "SELECT key, val, " + fName + "(val) FROM %s");
}
@Test
public void testJavascriptThrow() throws Throwable
{
createTable("CREATE TABLE %s (key int primary key, val double)");
String fName = createFunction(KEYSPACE, "double",
"CREATE OR REPLACE FUNCTION %s(val double) " +
"RETURNS NULL ON NULL INPUT " +
"RETURNS double " +
"LANGUAGE javascript\n" +
"AS 'throw \"fool\";';");
execute("INSERT INTO %s (key, val) VALUES (?, ?)", 1, 1d);
// throws IRE with ScriptException
assertInvalidThrowMessage("fool", FunctionExecutionException.class,
"SELECT key, val, " + fName + "(val) FROM %s");
}
@Test
public void testScriptReturnTypeCasting() throws Throwable
{
createTable("CREATE TABLE %s (key int primary key, val double)");
execute("INSERT INTO %s (key, val) VALUES (?, ?)", 1, 1d);
Object[][] variations = {
new Object[] { "true", "boolean", true },
new Object[] { "false", "boolean", false },
new Object[] { "100", "tinyint", (byte)100 },
new Object[] { "100.", "tinyint", (byte)100 },
new Object[] { "100", "smallint", (short)100 },
new Object[] { "100.", "smallint", (short)100 },
new Object[] { "100", "int", 100 },
new Object[] { "100.", "int", 100 },
new Object[] { "100", "double", 100d },
new Object[] { "100.", "double", 100d },
new Object[] { "100", "bigint", 100L },
new Object[] { "100.", "bigint", 100L },
new Object[] { "100", "varint", BigInteger.valueOf(100L) },
new Object[] { "100.", "varint", BigInteger.valueOf(100L) },
new Object[] { "parseInt(\"100\");", "decimal", BigDecimal.valueOf(100d) },
new Object[] { "100.", "decimal", BigDecimal.valueOf(100d) },
};
for (Object[] variation : variations)
{
Object functionBody = variation[0];
Object returnType = variation[1];
Object expectedResult = variation[2];
String fName = createFunction(KEYSPACE, "double",
"CREATE OR REPLACE FUNCTION %s(val double) " +
"RETURNS NULL ON NULL INPUT " +
"RETURNS " +returnType + ' ' +
"LANGUAGE javascript " +
"AS '" + functionBody + ";';");
assertRows(execute("SELECT key, val, " + fName + "(val) FROM %s"),
row(1, 1d, expectedResult));
}
}
@Test
public void testScriptParamReturnTypes() throws Throwable
{
UUID ruuid = UUID.randomUUID();
UUID tuuid = UUIDGen.getTimeUUID();
createTable("CREATE TABLE %s (key int primary key, " +
"tival tinyint, sival smallint, ival int, lval bigint, fval float, dval double, vval varint, ddval decimal, " +
"timval time, dtval date, tsval timestamp, uval uuid, tuval timeuuid)");
execute("INSERT INTO %s (key, tival, sival, ival, lval, fval, dval, vval, ddval, timval, dtval, tsval, uval, tuval) VALUES " +
"(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 1,
(byte)1, (short)1, 1, 1L, 1f, 1d, BigInteger.valueOf(1L), BigDecimal.valueOf(1d), 1L, Integer.MAX_VALUE, new Date(1), ruuid, tuuid);
Object[][] variations = {
new Object[] { "tinyint", "tival", (byte)1, (byte)2 },
new Object[] { "smallint", "sival", (short)1, (short)2 },
new Object[] { "int", "ival", 1, 2 },
new Object[] { "bigint", "lval", 1L, 2L },
new Object[] { "float", "fval", 1f, 2f },
new Object[] { "double", "dval", 1d, 2d },
new Object[] { "varint", "vval", BigInteger.valueOf(1L), BigInteger.valueOf(2L) },
new Object[] { "decimal", "ddval", BigDecimal.valueOf(1d), BigDecimal.valueOf(2d) },
new Object[] { "time", "timval", 1L, 2L },
};
for (Object[] variation : variations)
{
Object type = variation[0];
Object col = variation[1];
Object expected1 = variation[2];
Object expected2 = variation[3];
String fName = createFunction(KEYSPACE, type.toString(),
"CREATE OR REPLACE FUNCTION %s(val " + type + ") " +
"RETURNS NULL ON NULL INPUT " +
"RETURNS " + type + ' ' +
"LANGUAGE javascript " +
"AS 'val+1;';");
assertRows(execute("SELECT key, " + col + ", " + fName + '(' + col + ") FROM %s"),
row(1, expected1, expected2));
}
variations = new Object[][] {
new Object[] { "timestamp","tsval", new Date(1), new Date(1) },
new Object[] { "uuid", "uval", ruuid, ruuid },
new Object[] { "timeuuid", "tuval", tuuid, tuuid },
new Object[] { "date", "dtval", Integer.MAX_VALUE, Integer.MAX_VALUE },
};
for (Object[] variation : variations)
{
Object type = variation[0];
Object col = variation[1];
Object expected1 = variation[2];
Object expected2 = variation[3];
String fName = createFunction(KEYSPACE, type.toString(),
"CREATE OR REPLACE FUNCTION %s(val " + type + ") " +
"RETURNS NULL ON NULL INPUT " +
"RETURNS " + type + ' ' +
"LANGUAGE javascript " +
"AS 'val;';");
assertRows(execute("SELECT key, " + col + ", " + fName + '(' + col + ") FROM %s"),
row(1, expected1, expected2));
}
}
@Test
public void testJavascriptDisabled() throws Throwable
{
createTable("CREATE TABLE %s (key int primary key, val double)");
DatabaseDescriptor.enableScriptedUserDefinedFunctions(false);
try
{
assertInvalid("CREATE OR REPLACE FUNCTION " + KEYSPACE + ".assertNotEnabled(val double) " +
"RETURNS NULL ON NULL INPUT " +
"RETURNS double " +
"LANGUAGE javascript\n" +
"AS 'Math.sin(val);';");
}
finally
{
DatabaseDescriptor.enableScriptedUserDefinedFunctions(true);
}
}
@Test
public void testJavascriptCompileFailure() throws Throwable
{
assertInvalidMessage("Failed to compile function 'cql_test_keyspace.scrinv'",
"CREATE OR REPLACE FUNCTION " + KEYSPACE + ".scrinv(val double) " +
"RETURNS NULL ON NULL INPUT " +
"RETURNS double " +
"LANGUAGE javascript\n" +
"AS 'foo bar';");
}
@Test
public void testScriptInvalidLanguage() throws Throwable
{
assertInvalidMessage("Invalid language 'artificial_intelligence' for function 'cql_test_keyspace.scrinv'",
"CREATE OR REPLACE FUNCTION " + KEYSPACE + ".scrinv(val double) " +
"RETURNS NULL ON NULL INPUT " +
"RETURNS double " +
"LANGUAGE artificial_intelligence\n" +
"AS 'question for 42?';");
}
}