Skip to content

Commit 55f5db9

Browse files
committed
Fix for Bug#107543 (Bug#34464351), Cannot execute a SELECT statement that writes to an OUTFILE.
Change-Id: If84169f6f9f0671afdbc869751fd6e0dca00d207
1 parent 2d71ec2 commit 55f5db9

File tree

3 files changed

+122
-17
lines changed

3 files changed

+122
-17
lines changed

CHANGES

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
Version 9.5.0
55

6+
- Fix for Bug#107543 (Bug#34464351), Cannot execute a SELECT statement that writes to an OUTFILE.
7+
68
- Fix for Bug#17881458, BEHAVIOR OF SETBINARYSTREAM() METHOD IS DIFFERENT WHEN USESERVERPREPSTMTS=TRUE.
79

810
- Fix for Bug#45554 (Bug#11754018), Connector/J does not encode binary data if useServerPrepStatements=false.

src/main/core-api/java/com/mysql/cj/QueryInfo.java

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@
4242
public class QueryInfo {
4343

4444
private static final String OPENING_MARKERS = "`'\"";
45+
private static final String OPENING_MARKERS_WITH_PARENS = "`'\"(";
4546
private static final String CLOSING_MARKERS = "`'\"";
47+
private static final String CLOSING_MARKERS_WITH_PARENS = "`'\")";
4648
private static final String OVERRIDING_MARKERS = "";
4749

4850
private static final String SELECT_STATEMENT = "SELECT";
@@ -57,6 +59,7 @@ public class QueryInfo {
5759
private static final String AS_CLAUSE = "AS";
5860
private static final String[] ODKU_CLAUSE = new String[] { "ON", "DUPLICATE", "KEY", "UPDATE" };
5961
private static final String LAST_INSERT_ID_FUNC = "LAST_INSERT_ID";
62+
private static final String INTO_CLAUSE = "INTO";
6063

6164
private QueryInfo baseQueryInfo = null;
6265

@@ -814,15 +817,16 @@ public static boolean isReadOnlySafeQuery(String sql, boolean noBackslashEscapes
814817
public static QueryReturnType getQueryReturnType(String sql, boolean noBackslashEscapes) {
815818
/*
816819
* Statements that return results:
817-
* - ANALYZE; CHECK/CHECKSUM; DESC/DESCRIBE; EXPLAIN; HELP; OPTIMIZE; REPAIR; SELECT; SHOW; TABLE; VALUES; WITH ... SELECT|TABLE|VALUES ...; XA RECOVER;
820+
* - ANALYZE; CHECK/CHECKSUM; DESC/DESCRIBE; EXPLAIN; HELP; OPTIMIZE; REPAIR; SELECT (exc. [... INTO]); SHOW; TABLE (exc. [... INTO]); VALUES; WITH ...
821+
* SELECT|TABLE|VALUES ...; XA RECOVER;
818822
*
819823
* Statements that may return results:
820824
* - CALL; EXECUTE;
821825
*
822826
* Statements that do not return results:
823827
* - ALTER; BINLOG; CACHE; CHANGE; CLONE; COMMIT; CREATE; DEALLOCATE; DELETE; DO; DROP; FLUSH; GET; GRANT; HANDLER; IMPORT; INSERT; INSTALL; KILL; LOAD;
824-
* - LOCK; PREPARE; PURGE; RELEASE; RENAME; REPLACE; RESET; RESIGNAL; RESTART; REVOKE; ROLLBACK; SAVEPOINT; SET; SHUTDOWN; SIGNAL; START; STOP;
825-
* - TRUNCATE; UNINSTALL; UNLOCK; UPDATE; USE; WITH ... DELETE|UPDATE ...; XA [!RECOVER];
828+
* LOCK; PREPARE; PURGE; RELEASE; RENAME; REPLACE; RESET; RESIGNAL; RESTART; REVOKE; ROLLBACK; SAVEPOINT; SELECT ... INTO; SET; SHUTDOWN; SIGNAL; START;
829+
* STOP; TABLE ... INTO; TRUNCATE; UNINSTALL; UNLOCK; UPDATE; USE; WITH ... DELETE|UPDATE ...; XA [!RECOVER];
826830
*/
827831
int statementKeywordPos = indexOfStatementKeyword(sql, noBackslashEscapes);
828832
if (statementKeywordPos == -1) {
@@ -848,9 +852,11 @@ public static QueryReturnType getQueryReturnType(String sql, boolean noBackslash
848852
} else if (firstStatementChar == 'R' && StringUtils.startsWithIgnoreCaseAndWs(sql, "REPAIR", statementKeywordPos)) {
849853
return QueryReturnType.PRODUCES_RESULT_SET;
850854
} else if (firstStatementChar == 'S' && (StringUtils.startsWithIgnoreCaseAndWs(sql, "SELECT", statementKeywordPos)
855+
&& !containsIntoClause(statementKeywordPos, sql, noBackslashEscapes)
851856
|| StringUtils.startsWithIgnoreCaseAndWs(sql, "SHOW", statementKeywordPos))) {
852857
return QueryReturnType.PRODUCES_RESULT_SET;
853-
} else if (firstStatementChar == 'T' && StringUtils.startsWithIgnoreCaseAndWs(sql, "TABLE", statementKeywordPos)) {
858+
} else if (firstStatementChar == 'T' && StringUtils.startsWithIgnoreCaseAndWs(sql, "TABLE", statementKeywordPos)
859+
&& !containsIntoClause(statementKeywordPos, sql, noBackslashEscapes)) {
854860
return QueryReturnType.PRODUCES_RESULT_SET;
855861
} else if (firstStatementChar == 'V' && StringUtils.startsWithIgnoreCaseAndWs(sql, "VALUES", statementKeywordPos)) {
856862
return QueryReturnType.PRODUCES_RESULT_SET;
@@ -870,6 +876,23 @@ public static QueryReturnType getQueryReturnType(String sql, boolean noBackslash
870876
return QueryReturnType.DOES_NOT_PRODUCE_RESULT_SET;
871877
}
872878

879+
/**
880+
* Checks whether the given input string contains an "INTO" clause, starting from the specified position, while optionally considering MySQL's
881+
* NO_BACKSLASH_ESCAPES SQL mode for escape sequence handling.
882+
*
883+
* @param startingPosition
884+
* the position in the string to begin the search
885+
* @param searchIn
886+
* the string to search for the "INTO" clause
887+
* @param noBackslashEscapes
888+
* whether backslash escapes should be ignored (true for NO_BACKSLASH_ESCAPES mode)
889+
* @return {@code true} if the "INTO" clause is found in the input string from the given position, {@code false} otherwise
890+
*/
891+
private static boolean containsIntoClause(int startingPosition, String searchIn, boolean noBackslashEscapes) {
892+
return StringUtils.indexOfIgnoreCase(startingPosition, searchIn, INTO_CLAUSE, OPENING_MARKERS_WITH_PARENS, CLOSING_MARKERS_WITH_PARENS,
893+
noBackslashEscapes ? SearchMode.__MRK_COM_MYM_HNT_WS : SearchMode.__FULL) != -1;
894+
}
895+
873896
/**
874897
* Returns the context of the WITH statement. The context can be: SELECT, TABLE, VALUES, UPDATE or DELETE. This operation does not take into consideration
875898
* the multiplicity of queries in the specified SQL.
@@ -885,7 +908,7 @@ private static String getContextForWithStatement(String sql, boolean noBackslash
885908
String commentsFreeSql = StringUtils.stripCommentsAndHints(sql, OPENING_MARKERS, CLOSING_MARKERS, !noBackslashEscapes);
886909

887910
// Iterate through statement words, skipping all sub-queries sections enclosed by parens.
888-
StringInspector strInspector = new StringInspector(commentsFreeSql, OPENING_MARKERS + "(", CLOSING_MARKERS + ")", OPENING_MARKERS,
911+
StringInspector strInspector = new StringInspector(commentsFreeSql, OPENING_MARKERS_WITH_PARENS, CLOSING_MARKERS_WITH_PARENS, OPENING_MARKERS,
889912
noBackslashEscapes ? SearchMode.__MRK_COM_MYM_HNT_WS : SearchMode.__BSE_MRK_COM_MYM_HNT_WS);
890913
boolean asFound = false;
891914
while (true) {

src/test/java/testsuite/regression/StatementRegressionTest.java

Lines changed: 92 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11810,10 +11810,8 @@ public void testBug103878() throws Exception {
1181011810
*/
1181111811
@Test
1181211812
public void testBug23204652() throws Exception {
11813-
assertThrows(SQLException.class, "Statement\\.executeQuery\\(\\) cannot issue statements that do not produce result sets\\.", () -> {
11814-
this.stmt.executeQuery("DO 1 + 2");
11815-
return null;
11816-
});
11813+
assertThrows(SQLException.class, "Statement\\.executeQuery\\(\\) cannot issue statements that do not produce result sets\\.",
11814+
() -> this.stmt.executeQuery("DO 1 + 2"));
1181711815
}
1181811816

1181911817
/**
@@ -11826,19 +11824,15 @@ public void testBug71929() throws Exception {
1182611824
String[] queries = new String[] { "CREATE TABLE testBug71929 (id INT)", "/* comments */CREATE TABLE testBug71929 (id INT)",
1182711825
"/* comments *//* more comments */CREATE TABLE testBug71929 (id INT)" };
1182811826
for (String query : queries) {
11829-
assertThrows(SQLException.class, "Statement\\.executeQuery\\(\\) cannot issue statements that do not produce result sets\\.", () -> {
11830-
this.stmt.executeQuery(query);
11831-
return null;
11832-
});
11827+
assertThrows(SQLException.class, "Statement\\.executeQuery\\(\\) cannot issue statements that do not produce result sets\\.",
11828+
() -> this.stmt.executeQuery(query));
1183311829
}
1183411830

1183511831
queries = new String[] { "SELECT 1", "/* comments */SELECT 1", "/* comments *//* more comments */SELECT 1" };
1183611832
for (String query : queries) {
1183711833
assertThrows(SQLException.class,
11838-
"Statement\\.executeUpdate\\(\\) or Statement\\.executeLargeUpdate\\(\\) cannot issue statements that produce result sets\\.", () -> {
11839-
this.stmt.executeUpdate(query);
11840-
return null;
11841-
});
11834+
"Statement\\.executeUpdate\\(\\) or Statement\\.executeLargeUpdate\\(\\) cannot issue statements that produce result sets\\.",
11835+
() -> this.stmt.executeUpdate(query));
1184211836
}
1184311837
}
1184411838

@@ -14580,4 +14574,90 @@ private void testBug17881458Clob() throws Exception {
1458014574
}
1458114575
}
1458214576

14577+
/**
14578+
* Tests fix for Bug#107543 (Bug#34464351), Cannot execute a SELECT statement that writes to an OUTFILE.
14579+
* (INTO variable variant)
14580+
*
14581+
* @throws Exception
14582+
*/
14583+
@Test
14584+
public void testBug107543_IntoVar() throws Exception {
14585+
// Test 1: execute() with INTO @var
14586+
this.stmt.execute("SELECT 'testBug107543_1' INTO @testbug107543_a");
14587+
assertEquals("testBug107543_1", getSingleValueWithQuery("SELECT @testbug107543_a"));
14588+
14589+
// Test 2: executeUpdate() with INTO @var
14590+
this.stmt.executeUpdate("SELECT 'testBug107543_2' INTO @testbug107543_b");
14591+
assertEquals("testBug107543_2", getSingleValueWithQuery("SELECT @testbug107543_b"));
14592+
14593+
// Test 3: executeQuery() with INTO @var
14594+
assertThrows(SQLException.class, "Statement\\.executeQuery\\(\\) cannot issue statements that do not produce result sets\\.",
14595+
() -> this.stmt.executeQuery("SELECT 'testBug107543_3' INTO @testbug107543_c"));
14596+
}
14597+
14598+
/**
14599+
* Tests fix for Bug#107543 (Bug#34464351), Cannot execute a SELECT statement that writes to an OUTFILE.
14600+
* (INTO file variant)
14601+
*
14602+
* @throws Exception
14603+
*/
14604+
@Test
14605+
public void testBug107543_IntoFile() throws Exception {
14606+
String filePrivDir = getMysqlVariable("secure_file_priv");
14607+
assumeTrue(filePrivDir != null && !"NULL".equalsIgnoreCase(filePrivDir),
14608+
"To run this test the server needs to be started with the option\"--secure-file-priv=\"");
14609+
filePrivDir = filePrivDir.isEmpty() ? Paths.get("").toAbsolutePath().toString() : Paths.get(filePrivDir).toRealPath().toString();
14610+
filePrivDir = filePrivDir.endsWith(File.separator) ? filePrivDir : filePrivDir + File.separator;
14611+
14612+
createTable("testBug107543", "(txt VARCHAR(100))");
14613+
this.stmt.executeUpdate("INSERT INTO testBug107543 VALUES ('MySQL Connector/J')");
14614+
14615+
Path dataFilePath = Paths.get(filePrivDir, "testbug107543.dat");
14616+
for (String statement : new String[] { "SELECT * FROM", "TABLE" }) {
14617+
for (String outType : new String[] { "OUTFILE", "DUMPFILE" }) {
14618+
final String sql = statement + " testBug107543 INTO " + outType + " '" + dataFilePath.toString() + "'";
14619+
14620+
// Test 1: execute() with INTO [OUTFILE | DUMPFILE]
14621+
try {
14622+
this.stmt.execute(sql);
14623+
List<String> lines = Files.readAllLines(dataFilePath);
14624+
assertEquals(1, lines.size());
14625+
assertEquals("MySQL Connector/J", lines.get(0));
14626+
} finally {
14627+
try {
14628+
Files.delete(dataFilePath);
14629+
} catch (Exception e) {
14630+
// Ignore.
14631+
}
14632+
}
14633+
14634+
// Test 2: executeUpdate() with INTO [OUTFILE | DUMPFILE]
14635+
try {
14636+
this.stmt.executeUpdate(sql);
14637+
List<String> lines = Files.readAllLines(dataFilePath);
14638+
assertEquals(1, lines.size());
14639+
assertEquals("MySQL Connector/J", lines.get(0));
14640+
} finally {
14641+
try {
14642+
Files.delete(dataFilePath);
14643+
} catch (Exception e) {
14644+
// Ignore.
14645+
}
14646+
}
14647+
14648+
// Test 3: executeQuery() with INTO [OUTFILE | DUMPFILE]
14649+
try {
14650+
assertThrows(SQLException.class, "Statement\\.executeQuery\\(\\) cannot issue statements that do not produce result sets\\.",
14651+
() -> this.stmt.executeQuery(sql));
14652+
} finally {
14653+
try {
14654+
Files.delete(dataFilePath);
14655+
} catch (Exception e) {
14656+
// Ignore.
14657+
}
14658+
}
14659+
}
14660+
}
14661+
}
14662+
1458314663
}

0 commit comments

Comments
 (0)