Skip to content

Commit 6a73e1b

Browse files
SNOW-1890076: Ensure correct file is downloaded as stream
1 parent 979a161 commit 6a73e1b

File tree

6 files changed

+100
-1
lines changed

6 files changed

+100
-1
lines changed

src/main/java/net/snowflake/client/jdbc/ErrorCode.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ public enum ErrorCode {
8484
GCP_SERVICE_ERROR(200061, SqlState.SYSTEM_ERROR),
8585
AUTHENTICATOR_REQUEST_TIMEOUT(200062, SqlState.CONNECTION_EXCEPTION),
8686
INVALID_STRUCT_DATA(200063, SqlState.DATA_EXCEPTION),
87-
DISABLEOCSP_INSECUREMODE_VALUE_MISMATCH(200064, SqlState.INVALID_PARAMETER_VALUE);
87+
DISABLEOCSP_INSECUREMODE_VALUE_MISMATCH(200064, SqlState.INVALID_PARAMETER_VALUE),
88+
TOO_MANY_FILES_TO_DOWNLOAD_AS_STREAM(200065, SqlState.DATA_EXCEPTION);
8889

8990
public static final String errorMessageResource = "net.snowflake.client.jdbc.jdbc_error_messages";
9091

src/main/java/net/snowflake/client/jdbc/SnowflakeConnectionV1.java

+11
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import java.util.concurrent.ConcurrentHashMap;
4141
import java.util.concurrent.Executor;
4242
import java.util.concurrent.atomic.AtomicInteger;
43+
import java.util.regex.Pattern;
4344
import java.util.zip.GZIPInputStream;
4445
import net.snowflake.client.core.SFBaseSession;
4546
import net.snowflake.client.core.SFException;
@@ -61,12 +62,14 @@ public class SnowflakeConnectionV1 implements Connection, SnowflakeConnection {
6162

6263
/** Refer to all created and open statements from this connection */
6364
private final Set<Statement> openStatements = ConcurrentHashMap.newKeySet();
65+
6466
// Injected delay for the purpose of connection timeout testing
6567
// Any statement execution will sleep for the specified number of milliseconds
6668
private final AtomicInteger _injectedDelay = new AtomicInteger(0);
6769
private boolean isClosed;
6870
private SQLWarning sqlWarnings = null;
6971
private List<DriverPropertyInfo> missingProperties = null;
72+
7073
/**
7174
* Amount of milliseconds a user is willing to tolerate for network related issues (e.g. HTTP
7275
* 503/504) or database transient issues (e.g. GS not responding)
@@ -76,12 +79,14 @@ public class SnowflakeConnectionV1 implements Connection, SnowflakeConnection {
7679
* <p>Default: 300 seconds
7780
*/
7881
private int networkTimeoutInMilli = 0; // in milliseconds
82+
7983
/* this should be set to Connection.TRANSACTION_READ_COMMITTED
8084
* There may not be many implications here since the call to
8185
* setTransactionIsolation doesn't do anything.
8286
*/
8387
private int transactionIsolation = Connection.TRANSACTION_NONE;
8488
private SFBaseSession sfSession;
89+
8590
/** The SnowflakeConnectionImpl that provides the underlying physical-layer implementation */
8691
private SFConnectionHandler sfConnectionHandler;
8792

@@ -1037,6 +1042,12 @@ public InputStream downloadStream(String stageName, String sourceFileName, boole
10371042
// no file will be downloaded to this location
10381043
getCommand.append(" file:///tmp/ /*jdbc download stream*/");
10391044

1045+
// We cannot match whole sourceFileName since it may be different e.g. for git repositories so
1046+
// we match only raw filename
1047+
String[] split = sourceFileName.split("/");
1048+
String fileName = Pattern.quote(split[split.length - 1]);
1049+
getCommand.append(" PATTERN=\".*").append(fileName).append("$\"");
1050+
10401051
SFBaseFileTransferAgent transferAgent =
10411052
sfConnectionHandler.getFileTransferAgent(getCommand.toString(), stmt.getSFBaseStatement());
10421053

src/main/java/net/snowflake/client/jdbc/SnowflakeFileTransferAgent.java

+12
Original file line numberDiff line numberDiff line change
@@ -1696,6 +1696,18 @@ public InputStream downloadStream(String fileName) throws SnowflakeSQLException
16961696
remoteLocation remoteLocation = extractLocationAndPath(stageInfo.getLocation());
16971697

16981698
// when downloading files as stream there should be only one file in source files
1699+
// let's fail fast when more than one file matches instead of fetching random one
1700+
if (sourceFiles.size() > 1) {
1701+
throw new SnowflakeSQLException(
1702+
queryID,
1703+
SqlState.NO_DATA,
1704+
ErrorCode.TOO_MANY_FILES_TO_DOWNLOAD_AS_STREAM.getMessageCode(),
1705+
session,
1706+
"There are more than one file matching "
1707+
+ fileName
1708+
+ ": "
1709+
+ String.join(",", sourceFiles));
1710+
}
16991711
String sourceLocation =
17001712
sourceFiles.stream()
17011713
.findFirst()

src/test/java/net/snowflake/client/jdbc/StreamLatestIT.java

+73
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,22 @@
77
import static org.junit.jupiter.api.Assertions.assertTrue;
88
import static org.junit.jupiter.api.Assertions.fail;
99

10+
import java.io.BufferedReader;
1011
import java.io.BufferedWriter;
1112
import java.io.File;
1213
import java.io.FileWriter;
1314
import java.io.IOException;
1415
import java.io.InputStream;
16+
import java.io.InputStreamReader;
1517
import java.io.StringWriter;
1618
import java.nio.charset.StandardCharsets;
1719
import java.sql.Connection;
1820
import java.sql.ResultSet;
1921
import java.sql.SQLException;
2022
import java.sql.Statement;
2123
import java.util.Properties;
24+
import java.util.UUID;
25+
import java.util.stream.Collectors;
2226
import net.snowflake.client.annotations.DontRunOnGithubActions;
2327
import net.snowflake.client.category.TestTags;
2428
import org.apache.commons.io.IOUtils;
@@ -237,4 +241,73 @@ public void testSpecialCharactersInFileName() throws SQLException, IOException {
237241
}
238242
}
239243
}
244+
245+
/** Added > 3.21.0. Fixed regression introduced in 3.19.1 */
246+
@Test
247+
public void shouldDownloadStreamInDeterministicWay() throws Exception {
248+
try (Connection conn = getConnection();
249+
Statement stat = conn.createStatement()) {
250+
String randomStage = "test" + UUID.randomUUID().toString().replaceAll("-", "");
251+
try {
252+
stat.execute("CREATE OR REPLACE STAGE " + randomStage);
253+
String randomDir = UUID.randomUUID().toString();
254+
String sourceFilePathWithoutExtension = getFullPathFileInResource("test_file");
255+
String sourceFilePathWithExtension = getFullPathFileInResource("test_file.csv");
256+
String stageDest = String.format("@%s/%s", randomStage, randomDir);
257+
putFile(stat, sourceFilePathWithExtension, stageDest, false);
258+
putFile(stat, sourceFilePathWithoutExtension, stageDest, false);
259+
putFile(stat, sourceFilePathWithExtension, stageDest, true);
260+
putFile(stat, sourceFilePathWithoutExtension, stageDest, true);
261+
expectsFilesOnStage(stat, stageDest, 4);
262+
String stageName = "@" + randomStage;
263+
downloadStreamExpectingContent(
264+
conn, stageName, randomDir + "/test_file.gz", true, "I am a file without extension");
265+
downloadStreamExpectingContent(
266+
conn, stageName, randomDir + "/test_file.csv.gz", true, "I am a file with extension");
267+
downloadStreamExpectingContent(
268+
conn, stageName, randomDir + "/test_file", false, "I am a file without extension");
269+
downloadStreamExpectingContent(
270+
conn, stageName, randomDir + "/test_file.csv", false, "I am a file with extension");
271+
} finally {
272+
stat.execute("DROP STAGE IF EXISTS " + randomStage);
273+
}
274+
}
275+
}
276+
277+
private static void downloadStreamExpectingContent(
278+
Connection conn,
279+
String stageName,
280+
String fileName,
281+
boolean decompress,
282+
String expectedFileContent)
283+
throws IOException, SQLException {
284+
try (InputStream inputStream =
285+
conn.unwrap(SnowflakeConnectionV1.class)
286+
.downloadStream(stageName, fileName, decompress);
287+
InputStreamReader isr = new InputStreamReader(inputStream);
288+
BufferedReader br = new BufferedReader(isr)) {
289+
String content = br.lines().collect(Collectors.joining("\n"));
290+
assertEquals(expectedFileContent, content);
291+
}
292+
}
293+
294+
private static void expectsFilesOnStage(Statement stmt, String stageDest, int expectCount)
295+
throws SQLException {
296+
int filesInStageDir = 0;
297+
try (ResultSet rs = stmt.executeQuery("LIST " + stageDest)) {
298+
while (rs.next()) {
299+
++filesInStageDir;
300+
}
301+
}
302+
assertEquals(expectCount, filesInStageDir);
303+
}
304+
305+
private static boolean putFile(
306+
Statement stmt, String localFileName, String stageDest, boolean autoCompress)
307+
throws SQLException {
308+
return stmt.execute(
309+
String.format(
310+
"PUT file://%s %s AUTO_COMPRESS=%s",
311+
localFileName, stageDest, String.valueOf(autoCompress).toUpperCase()));
312+
}
240313
}

src/test/resources/test_file

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
I am a file without extension

src/test/resources/test_file.csv

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
I am a file with extension

0 commit comments

Comments
 (0)