Skip to content

Commit e911d0c

Browse files
SNOW-1890076: Ensure correct file is downloaded as stream
1 parent e517038 commit e911d0c

File tree

6 files changed

+97
-1
lines changed

6 files changed

+97
-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

+9
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,14 @@ public class SnowflakeConnectionV1 implements Connection, SnowflakeConnection {
6161

6262
/** Refer to all created and open statements from this connection */
6363
private final Set<Statement> openStatements = ConcurrentHashMap.newKeySet();
64+
6465
// Injected delay for the purpose of connection timeout testing
6566
// Any statement execution will sleep for the specified number of milliseconds
6667
private final AtomicInteger _injectedDelay = new AtomicInteger(0);
6768
private boolean isClosed;
6869
private SQLWarning sqlWarnings = null;
6970
private List<DriverPropertyInfo> missingProperties = null;
71+
7072
/**
7173
* Amount of milliseconds a user is willing to tolerate for network related issues (e.g. HTTP
7274
* 503/504) or database transient issues (e.g. GS not responding)
@@ -76,12 +78,14 @@ public class SnowflakeConnectionV1 implements Connection, SnowflakeConnection {
7678
* <p>Default: 300 seconds
7779
*/
7880
private int networkTimeoutInMilli = 0; // in milliseconds
81+
7982
/* this should be set to Connection.TRANSACTION_READ_COMMITTED
8083
* There may not be many implications here since the call to
8184
* setTransactionIsolation doesn't do anything.
8285
*/
8386
private int transactionIsolation = Connection.TRANSACTION_NONE;
8487
private SFBaseSession sfSession;
88+
8589
/** The SnowflakeConnectionImpl that provides the underlying physical-layer implementation */
8690
private SFConnectionHandler sfConnectionHandler;
8791

@@ -1037,6 +1041,11 @@ public InputStream downloadStream(String stageName, String sourceFileName, boole
10371041
// no file will be downloaded to this location
10381042
getCommand.append(" file:///tmp/ /*jdbc download stream*/");
10391043

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

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

+72
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,72 @@ public void testSpecialCharactersInFileName() throws SQLException, IOException {
237241
}
238242
}
239243
}
244+
245+
@Test
246+
public void shouldDownloadStreamInDeterministicWay() throws Exception {
247+
try (Connection conn = getConnection();
248+
Statement stat = conn.createStatement()) {
249+
String randomStage = "test" + UUID.randomUUID().toString().replaceAll("-", "");
250+
try {
251+
stat.execute("CREATE OR REPLACE STAGE " + randomStage);
252+
String randomDir = UUID.randomUUID().toString();
253+
String sourceFilePathWithoutExtension = getClass().getResource("/test_file").getPath();
254+
String sourceFilePathWithExtension = getClass().getResource("/test_file.csv").getPath();
255+
String stageDest = String.format("@%s/%s", randomStage, randomDir);
256+
putFile(stat, sourceFilePathWithExtension, stageDest, false);
257+
putFile(stat, sourceFilePathWithoutExtension, stageDest, false);
258+
putFile(stat, sourceFilePathWithExtension, stageDest, true);
259+
putFile(stat, sourceFilePathWithoutExtension, stageDest, true);
260+
expectsFilesOnStage(stat, stageDest, 4);
261+
String stageName = "@" + randomStage;
262+
downloadStreamExpectingContent(
263+
conn, stageName, randomDir + "/test_file.gz", true, "I am a file without extension");
264+
downloadStreamExpectingContent(
265+
conn, stageName, randomDir + "/test_file.csv.gz", true, "I am a file with extension");
266+
downloadStreamExpectingContent(
267+
conn, stageName, randomDir + "/test_file", false, "I am a file without extension");
268+
downloadStreamExpectingContent(
269+
conn, stageName, randomDir + "/test_file.csv", false, "I am a file with extension");
270+
} finally {
271+
stat.execute("DROP STAGE IF EXISTS " + randomStage);
272+
}
273+
}
274+
}
275+
276+
private static void downloadStreamExpectingContent(
277+
Connection conn,
278+
String stageName,
279+
String fileName,
280+
boolean decompress,
281+
String expectedFileContent)
282+
throws IOException, SQLException {
283+
try (InputStream inputStream =
284+
conn.unwrap(SnowflakeConnectionV1.class)
285+
.downloadStream(stageName, fileName, decompress);
286+
InputStreamReader isr = new InputStreamReader(inputStream);
287+
BufferedReader br = new BufferedReader(isr)) {
288+
String content = br.lines().collect(Collectors.joining("\n"));
289+
assertEquals(expectedFileContent, content);
290+
}
291+
}
292+
293+
private static void expectsFilesOnStage(Statement stat, String stageDest, int expectCount)
294+
throws SQLException {
295+
int filesInStageDir = 0;
296+
try (ResultSet rs = stat.executeQuery("LIST " + stageDest)) {
297+
while (rs.next()) {
298+
++filesInStageDir;
299+
}
300+
}
301+
assertEquals(expectCount, filesInStageDir);
302+
}
303+
304+
private static boolean putFile(
305+
Statement stat, String localFileName, String stageDest, boolean autoCompress)
306+
throws SQLException {
307+
return stat.execute(
308+
String.format(
309+
"PUT file://%s %s AUTO_COMPRESS=%s",
310+
localFileName, stageDest, String.valueOf(autoCompress).toUpperCase()));
311+
}
240312
}

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)