Skip to content

Commit

Permalink
Add support for EXECUTE IMMEDIATE statement
Browse files Browse the repository at this point in the history
Syntax:
EXECUTE IMMEDIATE
'SELECT * FROM t WHERE a = ? and b = ?'
USING 'foo', 42

The semantics are the same as:
PREPARE stmt1 FROM SELECT * FROM t WHERE a = ? and b = ?
EXECUTE stmt1 USING 'foo', 42
DEALLOCATE PREPARE stmt1
  • Loading branch information
aalbu authored and martint committed May 16, 2023
1 parent 88ced6f commit c8fb290
Show file tree
Hide file tree
Showing 13 changed files with 503 additions and 113 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.trino.sql.parser.ParsingException;
import io.trino.sql.parser.SqlParser;
import io.trino.sql.tree.Execute;
import io.trino.sql.tree.ExecuteImmediate;
import io.trino.sql.tree.ExplainAnalyze;
import io.trino.sql.tree.Expression;
import io.trino.sql.tree.Statement;
Expand Down Expand Up @@ -65,19 +66,29 @@ public PreparedQuery prepareQuery(Session session, Statement wrappedStatement)
prepareSql = Optional.of(session.getPreparedStatementFromExecute(executeStatement));
statement = sqlParser.createStatement(prepareSql.get(), createParsingOptions(session));
}

if (statement instanceof ExplainAnalyze explainAnalyzeStatement) {
else if (statement instanceof ExecuteImmediate executeImmediateStatement) {
statement = sqlParser.createStatement(
executeImmediateStatement.getStatement().getValue(),
executeImmediateStatement.getStatement().getLocation().orElseThrow(() -> new ParsingException("Missing location for embedded statement")),
createParsingOptions(session));
}
else if (statement instanceof ExplainAnalyze explainAnalyzeStatement) {
Statement innerStatement = explainAnalyzeStatement.getStatement();
Optional<QueryType> innerQueryType = getQueryType(innerStatement);
if (innerQueryType.isEmpty() || innerQueryType.get() == QueryType.DATA_DEFINITION) {
throw new TrinoException(NOT_SUPPORTED, "EXPLAIN ANALYZE doesn't support statement type: " + innerStatement.getClass().getSimpleName());
}
}

List<Expression> parameters = ImmutableList.of();
if (wrappedStatement instanceof Execute executeStatement) {
parameters = executeStatement.getParameters();
}
else if (wrappedStatement instanceof ExecuteImmediate executeImmediateStatement) {
parameters = executeImmediateStatement.getParameters();
}
validateParameters(statement, parameters);

return new PreparedQuery(statement, parameters, prepareSql);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
import io.trino.sql.tree.EmptyTableTreatment;
import io.trino.sql.tree.Except;
import io.trino.sql.tree.Execute;
import io.trino.sql.tree.ExecuteImmediate;
import io.trino.sql.tree.Explain;
import io.trino.sql.tree.ExplainAnalyze;
import io.trino.sql.tree.Expression;
Expand Down Expand Up @@ -1319,6 +1320,12 @@ protected Scope visitExecute(Execute node, Optional<Scope> scope)
return createAndAssignScope(node, scope);
}

@Override
protected Scope visitExecuteImmediate(ExecuteImmediate node, Optional<Scope> scope)
{
return createAndAssignScope(node, scope);
}

@Override
protected Scope visitGrant(Grant node, Optional<Scope> scope)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import io.trino.Session;
import io.trino.execution.QueryPreparer.PreparedQuery;
import io.trino.sql.parser.ParsingException;
import io.trino.sql.parser.SqlParser;
import io.trino.sql.tree.AllColumns;
import io.trino.sql.tree.QualifiedName;
Expand All @@ -28,6 +29,7 @@
import static io.trino.sql.QueryUtil.table;
import static io.trino.testing.TestingSession.testSessionBuilder;
import static io.trino.testing.assertions.TrinoExceptionAssert.assertTrinoExceptionThrownBy;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.testng.Assert.assertEquals;

public class TestQueryPreparer
Expand All @@ -54,13 +56,37 @@ public void testExecuteStatement()
simpleQuery(selectList(new AllColumns()), table(QualifiedName.of("foo"))));
}

@Test
public void testExecuteImmediateStatement()
{
PreparedQuery preparedQuery = QUERY_PREPARER.prepareQuery(TEST_SESSION, "EXECUTE IMMEDIATE 'SELECT * FROM foo'");
assertEquals(preparedQuery.getStatement(),
simpleQuery(selectList(new AllColumns()), table(QualifiedName.of("foo"))));
}

@Test
public void testExecuteStatementDoesNotExist()
{
assertTrinoExceptionThrownBy(() -> QUERY_PREPARER.prepareQuery(TEST_SESSION, "execute my_query"))
.hasErrorCode(NOT_FOUND);
}

@Test
public void testExecuteImmediateInvalidStatement()
{
assertThatThrownBy(() -> QUERY_PREPARER.prepareQuery(TEST_SESSION, "EXECUTE IMMEDIATE 'SELECT FROM'"))
.isInstanceOf(ParsingException.class)
.hasMessageMatching("line 1:27: mismatched input 'FROM'. Expecting: .*");
}

@Test
public void testExecuteImmediateInvalidMultilineStatement()
{
assertThatThrownBy(() -> QUERY_PREPARER.prepareQuery(TEST_SESSION, "EXECUTE\nIMMEDIATE 'SELECT\n FROM'"))
.isInstanceOf(ParsingException.class)
.hasMessageMatching("line 3:2: mismatched input 'FROM'. Expecting: .*");
}

@Test
public void testTooManyParameters()
{
Expand All @@ -69,6 +95,8 @@ public void testTooManyParameters()
.build();
assertTrinoExceptionThrownBy(() -> QUERY_PREPARER.prepareQuery(session, "EXECUTE my_query USING 1,2"))
.hasErrorCode(INVALID_PARAMETER_USAGE);
assertTrinoExceptionThrownBy(() -> QUERY_PREPARER.prepareQuery(TEST_SESSION, "EXECUTE IMMEDIATE 'SELECT * FROM foo where col1 = ?' USING 1,2"))
.hasErrorCode(INVALID_PARAMETER_USAGE);
}

@Test
Expand All @@ -79,6 +107,8 @@ public void testTooFewParameters()
.build();
assertTrinoExceptionThrownBy(() -> QUERY_PREPARER.prepareQuery(session, "EXECUTE my_query USING 1"))
.hasErrorCode(INVALID_PARAMETER_USAGE);
assertTrinoExceptionThrownBy(() -> QUERY_PREPARER.prepareQuery(TEST_SESSION, "EXECUTE IMMEDIATE 'SELECT ? FROM foo where col1 = ?' USING 1"))
.hasErrorCode(INVALID_PARAMETER_USAGE);
}

@Test
Expand All @@ -89,8 +119,13 @@ public void testParameterMismatchWithOffset()
.build();
assertTrinoExceptionThrownBy(() -> QUERY_PREPARER.prepareQuery(session, "EXECUTE my_query USING 1"))
.hasErrorCode(INVALID_PARAMETER_USAGE);
assertTrinoExceptionThrownBy(() -> QUERY_PREPARER.prepareQuery(TEST_SESSION, "EXECUTE IMMEDIATE 'SELECT ? FROM foo OFFSET ? ROWS' USING 1"))
.hasErrorCode(INVALID_PARAMETER_USAGE);

assertTrinoExceptionThrownBy(() -> QUERY_PREPARER.prepareQuery(session, "EXECUTE my_query USING 1, 2, 3, 4, 5, 6"))
.hasErrorCode(INVALID_PARAMETER_USAGE);
assertTrinoExceptionThrownBy(() -> QUERY_PREPARER.prepareQuery(TEST_SESSION, "EXECUTE IMMEDIATE 'SELECT ? FROM foo OFFSET ? ROWS' USING 1, 2, 3, 4, 5, 6"))
.hasErrorCode(INVALID_PARAMETER_USAGE);
}

@Test
Expand All @@ -101,8 +136,13 @@ public void testParameterMismatchWithLimit()
.build();
assertTrinoExceptionThrownBy(() -> QUERY_PREPARER.prepareQuery(session, "EXECUTE my_query USING 1"))
.hasErrorCode(INVALID_PARAMETER_USAGE);
assertTrinoExceptionThrownBy(() -> QUERY_PREPARER.prepareQuery(TEST_SESSION, "EXECUTE IMMEDIATE 'SELECT ? FROM foo LIMIT ?' USING 1"))
.hasErrorCode(INVALID_PARAMETER_USAGE);

assertTrinoExceptionThrownBy(() -> QUERY_PREPARER.prepareQuery(session, "EXECUTE my_query USING 1, 2, 3, 4, 5, 6"))
.hasErrorCode(INVALID_PARAMETER_USAGE);
assertTrinoExceptionThrownBy(() -> QUERY_PREPARER.prepareQuery(TEST_SESSION, "EXECUTE IMMEDIATE 'SELECT ? FROM foo LIMIT ?' USING 1, 2, 3, 4, 5, 6"))
.hasErrorCode(INVALID_PARAMETER_USAGE);
}

@Test
Expand All @@ -111,9 +151,15 @@ public void testParameterMismatchWithFetchFirst()
Session session = testSessionBuilder()
.addPreparedStatement("my_query", "SELECT ? FROM foo FETCH FIRST ? ROWS ONLY")
.build();

assertTrinoExceptionThrownBy(() -> QUERY_PREPARER.prepareQuery(session, "EXECUTE my_query USING 1"))
.hasErrorCode(INVALID_PARAMETER_USAGE);
assertTrinoExceptionThrownBy(() -> QUERY_PREPARER.prepareQuery(TEST_SESSION, "EXECUTE IMMEDIATE 'SELECT ? FROM foo FETCH FIRST ? ROWS ONLY' USING 1"))
.hasErrorCode(INVALID_PARAMETER_USAGE);

assertTrinoExceptionThrownBy(() -> QUERY_PREPARER.prepareQuery(session, "EXECUTE my_query USING 1, 2, 3, 4, 5, 6"))
.hasErrorCode(INVALID_PARAMETER_USAGE);
assertTrinoExceptionThrownBy(() -> QUERY_PREPARER.prepareQuery(TEST_SESSION, "EXECUTE IMMEDIATE 'SELECT ? FROM foo FETCH FIRST ? ROWS ONLY' USING 1, 2, 3, 4, 5, 6"))
.hasErrorCode(INVALID_PARAMETER_USAGE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Licensed 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 io.trino.sql.query;

import io.trino.sql.parser.ParsingException;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

import static io.trino.testing.assertions.TrinoExceptionAssert.assertTrinoExceptionThrownBy;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;

@TestInstance(PER_CLASS)
public class TestExecuteImmediate
{
private QueryAssertions assertions;

@BeforeAll
public void init()
{
assertions = new QueryAssertions();
}

@AfterAll
public void teardown()
{
assertions.close();
assertions = null;
}

@Test
public void testNoParameters()
{
assertThat(assertions.query("EXECUTE IMMEDIATE 'SELECT * FROM (VALUES 1, 2, 3)'"))
.matches("VALUES 1,2,3");
}

@Test
public void testParameterInLambda()
{
assertThat(assertions.query("EXECUTE IMMEDIATE 'SELECT * FROM (VALUES ARRAY[1,2,3], ARRAY[4,5,6]) t(a) WHERE any_match(t.a, v -> v = ?)' USING 1"))
.matches("VALUES ARRAY[1,2,3]");
}

@Test
public void testQuotesInStatement()
{
assertThat(assertions.query("EXECUTE IMMEDIATE 'SELECT ''foo'''"))
.matches("VALUES 'foo'");
}

@Test
public void testSyntaxError()
{
assertThatThrownBy(() -> assertions.query("EXECUTE IMMEDIATE 'SELECT ''foo'"))
.isInstanceOf(ParsingException.class)
.hasMessageMatching("line 1:27: mismatched input '''. Expecting: .*");
assertThatThrownBy(() -> assertions.query("EXECUTE IMMEDIATE\n'SELECT ''foo'"))
.isInstanceOf(ParsingException.class)
.hasMessageMatching("line 2:8: mismatched input '''. Expecting: .*");
}

@Test
public void testSemanticError()
{
assertTrinoExceptionThrownBy(() -> assertions.query("EXECUTE IMMEDIATE 'SELECT * FROM tiny.tpch.orders'"))
.hasMessageMatching("line 1:34: Catalog 'tiny' does not exist");
assertTrinoExceptionThrownBy(() -> assertions.query("EXECUTE IMMEDIATE\n'SELECT *\nFROM tiny.tpch.orders'"))
.hasMessageMatching("line 3:6: Catalog 'tiny' does not exist");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ statement
| PREPARE identifier FROM statement #prepare
| DEALLOCATE PREPARE identifier #deallocate
| EXECUTE identifier (USING expression (',' expression)*)? #execute
| EXECUTE IMMEDIATE string (USING expression (',' expression)*)? #executeImmediate
| DESCRIBE INPUT identifier #describeInput
| DESCRIBE OUTPUT identifier #describeOutput
| SET PATH pathSpecification #setPath
Expand Down Expand Up @@ -846,7 +847,7 @@ nonReserved
| FETCH | FILTER | FINAL | FIRST | FOLLOWING | FORMAT | FUNCTIONS
| GRACE | GRANT | GRANTED | GRANTS | GRAPHVIZ | GROUPS
| HOUR
| IF | IGNORE | INCLUDING | INITIAL | INPUT | INTERVAL | INVOKER | IO | ISOLATION
| IF | IGNORE | IMMEDIATE | INCLUDING | INITIAL | INPUT | INTERVAL | INVOKER | IO | ISOLATION
| JSON
| KEEP | KEY | KEYS
| LAST | LATERAL | LEADING | LEVEL | LIMIT | LOCAL | LOGICAL
Expand Down Expand Up @@ -962,6 +963,7 @@ HAVING: 'HAVING';
HOUR: 'HOUR';
IF: 'IF';
IGNORE: 'IGNORE';
IMMEDIATE: 'IMMEDIATE';
IN: 'IN';
INCLUDING: 'INCLUDING';
INITIAL: 'INITIAL';
Expand Down
16 changes: 16 additions & 0 deletions core/trino-parser/src/main/java/io/trino/sql/SqlFormatter.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import io.trino.sql.tree.DropView;
import io.trino.sql.tree.Except;
import io.trino.sql.tree.Execute;
import io.trino.sql.tree.ExecuteImmediate;
import io.trino.sql.tree.Explain;
import io.trino.sql.tree.ExplainAnalyze;
import io.trino.sql.tree.ExplainFormat;
Expand Down Expand Up @@ -408,6 +409,21 @@ protected Void visitExecute(Execute node, Integer indent)
return null;
}

@Override
protected Void visitExecuteImmediate(ExecuteImmediate node, Integer indent)
{
append(indent, "EXECUTE IMMEDIATE\n")
.append(formatStringLiteral(node.getStatement().getValue()));
List<Expression> parameters = node.getParameters();
if (!parameters.isEmpty()) {
builder.append("\nUSING ");
builder.append(parameters.stream()
.map(SqlFormatter::formatExpression)
.collect(joining(", ")));
}
return null;
}

@Override
protected Void visitDescribeOutput(DescribeOutput node, Integer indent)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
import io.trino.sql.tree.Except;
import io.trino.sql.tree.ExcludedPattern;
import io.trino.sql.tree.Execute;
import io.trino.sql.tree.ExecuteImmediate;
import io.trino.sql.tree.ExistsPredicate;
import io.trino.sql.tree.Explain;
import io.trino.sql.tree.ExplainAnalyze;
Expand Down Expand Up @@ -942,6 +943,15 @@ public Node visitExecute(SqlBaseParser.ExecuteContext context)
visit(context.expression(), Expression.class));
}

@Override
public Node visitExecuteImmediate(SqlBaseParser.ExecuteImmediateContext context)
{
return new ExecuteImmediate(
getLocation(context),
((StringLiteral) visit(context.string())),
visit(context.expression(), Expression.class));
}

@Override
public Node visitDescribeOutput(SqlBaseParser.DescribeOutputContext context)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ protected R visitExecute(Execute node, C context)
return visitStatement(node, context);
}

protected R visitExecuteImmediate(ExecuteImmediate node, C context)
{
return visitStatement(node, context);
}

protected R visitDescribeOutput(DescribeOutput node, C context)
{
return visitStatement(node, context);
Expand Down
Loading

0 comments on commit c8fb290

Please sign in to comment.