Skip to content

Commit e8c4709

Browse files
#70: Made script.execute robust against quotes (#71)
* #70: Made `script.execute` robust against quotes Co-authored-by: Sebastian Bär <[email protected]>
1 parent 99e83df commit e8c4709

File tree

7 files changed

+147
-68
lines changed

7 files changed

+147
-68
lines changed

doc/changes/changelog.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Changes
22

3-
* [3.0.1](changes_3.0.1.md)
3+
* [3.0.1](changes_3.1.0.md)
44
* [3.0.0](changes_3.0.0.md)
55
* [2.1.0](changes_2.1.0.md)
66
* [2.0.0](changes_2.0.0.md)

doc/changes/changes_3.0.1.md renamed to doc/changes/changes_3.1.0.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
# Test Database Builder 3.0.1, released 2021-MM-DD
1+
# Test Database Builder 3.1.0, released 2021-02-24
2+
3+
Code name: Made `script.execute` robust against quotes
24

35
## Summary
46

7+
TDDB 3.1.0 allows you to create and execute Exasol scripts. In this released we fixed a bug in the implementation of execute that lead to an exception when string parameters containing quotes were passed to the execute function.
8+
59
## Features
610

7-
* #48: Added support for multiple jars in BucketFS content.
11+
* #48: Added support for multiple JARs in BucketFS content.
12+
13+
## Bugfixes
14+
15+
* #70: Made `script.execute` robust against quotes
816

917
## Documentation
1018

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<modelVersion>4.0.0</modelVersion>
55
<groupId>com.exasol</groupId>
66
<artifactId>test-db-builder-java</artifactId>
7-
<version>3.0.0</version>
7+
<version>3.1.0</version>
88
<name>Test Database Builder for Java</name>
99
<description>pom.xml</description>
1010
<url>https://github.com/exasol/test-db-builder</url>

src/main/java/com/exasol/dbbuilder/dialects/exasol/ExasolImmediateDatabaseObjectWriter.java

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
package com.exasol.dbbuilder.dialects.exasol;
22

3-
import java.sql.Connection;
4-
import java.sql.ResultSet;
5-
import java.sql.SQLException;
6-
import java.sql.Statement;
7-
import java.util.ArrayList;
8-
import java.util.Collection;
9-
import java.util.List;
10-
import java.util.Map;
3+
import java.sql.*;
4+
import java.util.*;
115
import java.util.stream.Collectors;
126

137
import com.exasol.dbbuilder.dialects.*;
148
import com.exasol.dbbuilder.dialects.exasol.udf.*;
9+
import com.exasol.errorreporting.ExaError;
1510

1611
/**
1712
* Database object writer that writes objects to the database immediately.
1813
*/
1914
public class ExasolImmediateDatabaseObjectWriter extends AbstractImmediateDatabaseObjectWriter {
15+
private static final ExasolStringLiteralEscaper SINGLE_QUOTE_ESCAPER = new ExasolStringLiteralEscaper();
2016
private final ExasolObjectConfiguration configuration;
2117

2218
/**
@@ -257,16 +253,24 @@ public void drop(final Schema schema) {
257253
/**
258254
* Execute a script.
259255
*
256+
* <p>
257+
* Implementation note: This method does not use prepared statements but string concatenation, since Exasol
258+
* currently does not support prepared statements for script execution (see
259+
* https://www.exasol.com/support/browse/IDEA-42).
260+
* </p>
261+
*
260262
* @param script script to execute
261263
* @param parameterValues script parameters
262264
* @return row count
263265
*/
264266
public int execute(final Script script, final Object... parameterValues) {
267+
final String query = getScriptExecutionSql(script, parameterValues);
265268
try (final Statement statement = this.connection.createStatement()) {
266-
statement.execute(getScriptExecutionSql(script, parameterValues));
269+
statement.execute(query);
267270
return statement.getUpdateCount();
268271
} catch (final SQLException exception) {
269-
throw new DatabaseObjectException(script, "Failed to execute script " + script.getFullyQualifiedName(),
272+
throw new DatabaseObjectException(script, ExaError.messageBuilder("E-TDBJ-4")
273+
.message("Failed to execute script query {{query}}.").parameter("query", query).toString(),
270274
exception);
271275
}
272276
}
@@ -276,41 +280,29 @@ private String getScriptExecutionSql(final Script script, final Object[] paramet
276280
builder.append(script.getFullyQualifiedName());
277281
if (parameterValues.length > 0) {
278282
builder.append(" (");
279-
boolean first = true;
280-
for (final Object parameter : parameterValues) {
281-
if (first) {
282-
first = false;
283-
} else {
284-
builder.append(", ");
285-
}
286-
appendScriptParameterValue(builder, parameter);
287-
}
283+
builder.append(Arrays.stream(parameterValues).map(this::formatScriptParameterValue)
284+
.collect(Collectors.joining(", ")));
288285
builder.append(")");
289286
}
290287
return builder.toString();
291288
}
292289

293-
private void appendScriptParameterValue(final StringBuilder builder, final Object parameter) {
290+
private String formatScriptParameterValue(final Object parameter) {
294291
if (parameter instanceof Collection) {
295-
builder.append(" ARRAY(");
296-
int i = 0;
297-
for (final Object arrayItem : (Collection<?>) parameter) {
298-
if (i++ > 0) {
299-
builder.append(", ");
300-
}
301-
appendScriptScalarParameter(builder, arrayItem);
302-
}
303-
builder.append(")");
292+
return " ARRAY(" + ((Collection<?>) parameter).stream().map(this::formatScriptScalarParameter)
293+
.collect(Collectors.joining(", ")) + ")";
304294
} else {
305-
appendScriptScalarParameter(builder, parameter);
295+
return formatScriptScalarParameter(parameter);
306296
}
307297
}
308298

309-
private void appendScriptScalarParameter(final StringBuilder builder, final Object value) {
299+
private String formatScriptScalarParameter(final Object value) {
310300
if (value instanceof String) {
311-
builder.append("'").append(value).append("'");
301+
return "'" + SINGLE_QUOTE_ESCAPER.escape(value.toString()) + "'";
302+
} else if (value == null) {
303+
return "NULL";
312304
} else {
313-
builder.append(value);
305+
return value.toString();
314306
}
315307
}
316308

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.exasol.dbbuilder.dialects.exasol;
2+
3+
/**
4+
* This class escapes a string so that it can be used in an Exasol string literal.
5+
*/
6+
public class ExasolStringLiteralEscaper {
7+
/**
8+
* Escape a given message for the use in an Exasol string literal.
9+
*
10+
* @param message message to escape
11+
* @return escaped message
12+
*/
13+
public String escape(final String message) {
14+
return message.replace("'", "''");
15+
}
16+
}

src/test/java/com/exasol/dbbuilder/dialects/exasol/ExasolDatabaseObjectCreationAndDeletionIT.java

Lines changed: 75 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,55 @@ void testExecuteScriptWithParameters() throws SQLException {
153153
script.execute(param1, param2);
154154
final Statement statement = this.adminConnection.createStatement();
155155
final ResultSet result = statement.executeQuery("SELECT * FROM " + table.getFullyQualifiedName());
156-
assertAll(() -> assertThat("Result has entry", result.next(), equalTo(true)),
156+
assertThat("Result has entry", result.next(), equalTo(true));
157+
assertAll(//
157158
() -> assertThat(result.getString(1), equalTo(param1)),
158159
() -> assertThat(result.getDouble(2), equalTo(param2)));
159160
}
160161

162+
// [itest->dsn~running-scripts-that-have-no-return~1]
163+
@ParameterizedTest
164+
@ValueSource(strings = { "test", "test \"quoted\"", "test 'quoted'", "lots'''of'single''quotes", "test \\\"",
165+
"test \\" })
166+
void testExecuteScriptWithStringParameters(final String parameterValue) throws SQLException {
167+
final ExasolSchema exasolSchema = (ExasolSchema) this.factory
168+
.createSchema("PARENT_SCHEMA_FOR_SCRIPT_WITH_PARAMETERS_2");
169+
final Table table = exasolSchema.createTable("LUA_RESULT_OF_QUOTING_TESTS", "A", "VARCHAR(40)");
170+
final String content = "query([[INSERT INTO " + table.getFullyQualifiedName() + " VALUES (:p1)]], {p1=param1})";
171+
final Script script = exasolSchema.createScript("LUA_SCRIPT_FOR_QUOTING_TEST", content, "param1");
172+
try {
173+
script.execute(parameterValue);
174+
final Statement statement = this.adminConnection.createStatement();
175+
final ResultSet result = statement.executeQuery("SELECT * FROM " + table.getFullyQualifiedName());
176+
assertThat("Result has entry", result.next(), equalTo(true));
177+
assertThat(result.getString(1), equalTo(parameterValue));
178+
} finally {
179+
script.drop();
180+
table.drop();
181+
exasolSchema.drop();
182+
}
183+
}
184+
185+
@Test
186+
void testExecuteScriptWithNullParameter() throws SQLException {
187+
final ExasolSchema exasolSchema = (ExasolSchema) this.factory
188+
.createSchema("PARENT_SCHEMA_FOR_SCRIPT_WITH_NULL_PARAMETER");
189+
final Table table = exasolSchema.createTable("LUA_RESULT_OF_NULL_SCRIPT_EXEC_TEST", "A", "VARCHAR(40)");
190+
final String content = "query([[INSERT INTO " + table.getFullyQualifiedName() + " VALUES (:p1)]], {p1=param1})";
191+
final Script script = exasolSchema.createScript("LUA_SCRIPT_FOR_NULL_SCRIPT_EXEC_TEST", content, "param1");
192+
try {
193+
script.execute((Object) null);
194+
final Statement statement = this.adminConnection.createStatement();
195+
final ResultSet result = statement.executeQuery("SELECT * FROM " + table.getFullyQualifiedName());
196+
assertThat("Result has entry", result.next(), equalTo(true));
197+
assertThat(result.getString(1), equalTo(null));
198+
} finally {
199+
script.drop();
200+
table.drop();
201+
exasolSchema.drop();
202+
}
203+
}
204+
161205
@Test
162206
void testExecuteScriptReturningRowCount() {
163207
final ExasolSchema exasolSchema = (ExasolSchema) this.factory
@@ -180,6 +224,35 @@ void testExecuteScriptReturningTable() {
180224
assertThat(result, contains(contains("foo", true), contains("bar", false)));
181225
}
182226

227+
@Test
228+
void testExecuteScriptWithArrayParameter() {
229+
final ExasolSchema exasolSchema = (ExasolSchema) this.factory
230+
.createSchema("PARENT_SCHEMA_FOR_SCRIPT_WITH_ARRAY_PARAMETER");
231+
final Script script = exasolSchema.createScriptBuilder("SUM_UP") //
232+
.arrayParameter("operands") //
233+
.content("s = 0\n" //
234+
+ "for _, operand in ipairs(operands) do\n" //
235+
+ " s = s + operand\n" //
236+
+ "end\n" //
237+
+ "exit({rows_affected = s})") //
238+
.build();
239+
final int sum = script.execute(List.of(1, 2, 3, 4, 5));
240+
assertThat(sum, equalTo(15));
241+
}
242+
243+
@ValueSource(booleans = { true, false })
244+
@ParameterizedTest
245+
void testExecuteScriptThrowsExceptionOnLuaError(final boolean returnsTable) {
246+
final ExasolSchema exasolSchema = (ExasolSchema) this.factory.createSchema(
247+
"PARENT_SCHEMA_FOR_SCRIPT" + (returnsTable ? "_RETURING_TABLE" : "") + "_THROWING_EXCEPTION");
248+
final Script.Builder builder = exasolSchema.createScriptBuilder("LUA_SCRIPT").content("error()");
249+
if (returnsTable) {
250+
builder.returnsTable();
251+
}
252+
final Script build = builder.build();
253+
assertThrows(DatabaseObjectException.class, build::executeQuery);
254+
}
255+
183256
@Test
184257
// [itest->dsn~creating-udfs~1]
185258
void testCreateUdf() throws SQLException {
@@ -252,35 +325,6 @@ private ResultSet getScriptDescription(final ExasolSchema exasolSchema) throws S
252325
return result;
253326
}
254327

255-
@ValueSource(booleans = { true, false })
256-
@ParameterizedTest
257-
void testExecuteScriptThrowsException(final boolean returnsTable) {
258-
final ExasolSchema exasolSchema = (ExasolSchema) this.factory.createSchema(
259-
"PARENT_SCHEMA_FOR_SCRIPT" + (returnsTable ? "_RETURING_TABLE" : "") + "_THROWING_EXCEPTION");
260-
final Script.Builder builder = exasolSchema.createScriptBuilder("LUA_SCRIPT").content("error()");
261-
if (returnsTable) {
262-
builder.returnsTable();
263-
}
264-
final Script build = builder.build();
265-
assertThrows(DatabaseObjectException.class, build::executeQuery);
266-
}
267-
268-
@Test
269-
void testExecuteScriptWithArrayParameter() {
270-
final ExasolSchema exasolSchema = (ExasolSchema) this.factory
271-
.createSchema("PARENT_SCHEMA_FOR_SCRIPT_WITH_ARRAY_PARAMETER");
272-
final Script script = exasolSchema.createScriptBuilder("SUM_UP") //
273-
.arrayParameter("operands") //
274-
.content("s = 0\n" //
275-
+ "for i=1, #operands do\n" //
276-
+ " s = s + operands[i]\n" //
277-
+ "end\n" //
278-
+ "exit({rows_affected=s})") //
279-
.build();
280-
final int sum = script.execute(List.of(1, 2, 3, 4, 5));
281-
assertThat(sum, equalTo(15));
282-
}
283-
284328
@Test
285329
// [itest->dsn~creating-database-users~1]
286330
void testCreateLoginUser() throws SQLException {
@@ -427,4 +471,4 @@ private static String getTableSysName(final DatabaseObject object) {
427471
}
428472
}
429473
}
430-
}
474+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.exasol.dbbuilder.dialects.exasol;
2+
3+
import static org.hamcrest.MatcherAssert.assertThat;
4+
import static org.hamcrest.Matchers.equalTo;
5+
6+
import org.junit.jupiter.params.ParameterizedTest;
7+
import org.junit.jupiter.params.provider.CsvSource;
8+
9+
class ExasolStringLiteralEscaperTest {
10+
@ParameterizedTest
11+
@CsvSource({ //
12+
"test', test''", //
13+
"test\", test\"", //
14+
"test'', test''''", //
15+
})
16+
void test(final String input, final String expectedOutput) {
17+
assertThat(new ExasolStringLiteralEscaper().escape(input), equalTo(expectedOutput));
18+
}
19+
}

0 commit comments

Comments
 (0)