From d0477604fd06ec3fd1d453adbfe1f81a5289788c Mon Sep 17 00:00:00 2001 From: Waleed Fateem <72769898+sfc-gh-wfateem@users.noreply.github.com> Date: Wed, 22 Nov 2023 14:00:20 -0600 Subject: [PATCH 01/17] SNOW-917458 Snowflake JDBC drivers fails on JDK 21 with Arrow (#1529) SNOW-917458: Snowflake JDBC drivers fail on JDK 21 with Arrow Cherry picked Arrow fix GH-35053 into Arrow v10.0.1 --- dependencies/arrow-format-10.0.1.jar | Bin 109977 -> 110008 bytes dependencies/arrow-memory-core-10.0.1.jar | Bin 117958 -> 118203 bytes dependencies/arrow-memory-netty-10.0.1.jar | Bin 39241 -> 39267 bytes dependencies/arrow-memory-unsafe-10.0.1.jar | Bin 10780 -> 10807 bytes dependencies/arrow-vector-10.0.1.jar | Bin 1875451 -> 1875149 bytes 5 files changed, 0 insertions(+), 0 deletions(-) diff --git a/dependencies/arrow-format-10.0.1.jar b/dependencies/arrow-format-10.0.1.jar index 8403b48b90aba10f0507067529e0aec883d2dccb..c197c5b0a9ded7e727074fd31b9a83a3149f59f0 100644 GIT binary patch delta 6471 zcmaJ_30RF=7yfpgbJTGfHR|LPWlG5uX;9*dZUg$wR~fp947sMnQ9@KwpW2mXi4>)S zgA5sd+{iUtVD|M7U9?^%28cdxbHwbov{^cvBj8c~M;A5DRn z(bCdlZbvT}e=2_Fil(Fv)^cCJ(~kPTH*-U%b)M zw`Ob`S)x7 zEo$SELIo;ly-=Dbofz^9$osdzI81H&-p8Vo}N#Aq=jK;nx;`P>>uv*V^s zGKq_>U2^82MIKxBc|GsGqpbXw@T8KP2c0I0lWQHmt`4v7@25!l5FGP9GWO~9?fwIU zEB|Kt8Z`g))4G3(!)gxIR{i6->_Fd};}3|v2hFcqzog>MBJIS~;1~n*eSUK`c6_PX z{P*LQS5F(<&MtTVZ**wX=x8lZqien7w{2|h@9lAS@6(BI-+WnA)!;fvU_U;?p#1#e zBj?BGtol#k682QimWb=(?3SzBvY#dG(_I>onD2Q_JMhkjQy1SXZyH$9+xqa>!t96s zSB6eJUASmenA@i{8l@{M4&E=%(yljjACy?5$u7&=H(x8hN9X-|KQ~v}h#Ve{@QVtn zZMKzHwhI|m-m5g#DAo4&Cw9ALPc`w;NWWNSWIlC|_!JJAdj4UG?jmcctL5X;(b~s< z8&zrGzaxCw*CFFx%&2=KQVL(M(Ao83zq?6yZ71_tIhoZhQ~FLg;r}l0w(qDy;WCZd zCzp%I$_vk&9uO255Er$=`(pIV;7>OX{uL4F7(1}icGl^y9)kW=M?G{WSuhiKK4iVJ zu9av{d#ZeSyZ+Dm^no7^U;RALyU(Lbx1SD*eC6?>K^8gP?#r$7!CtNnB`0GXOygb_ zpWrg5jaF6Ss4A=PoL-H?*^a?BxULVJCKPE3W%=K*A=Dl%1z-(8d{w4gejkW*p_fx#oTSPHMBX&ncxrqJ{h(`_;B$3UpQvWoQVvoK!STaif+fm3@%CkQgvj@*!*m81P)1 zH5ekpz4MS*;nc`dD-^E~u73Ar2NI87|v~go-VW7d6Og#Hi+x_p})% z)DjOF2CK3(B*J=`NTj%>{G?5tcdN^$UdT(AhO>QqY;i>a(pMIkqj9{=21ajr^XeC( zJYE>)n#zAEtA`7zOMRB2WGYnp`=MK!)H|isPCdGAHwN9~L+ȎdqKw9B7{fCJ|Q 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 diff --git a/dependencies/arrow-memory-core-10.0.1.jar b/dependencies/arrow-memory-core-10.0.1.jar index 289cbc48c0923288633204975eb09bc026d32f22..cedbca385ef9658333615e0a7036c2bcf03c7086 100644 GIT binary patch delta 9758 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 delta 9787 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 CZiY7i delta 2406 zcmZux3pkW%8~)}S!;lPS8qAn!rZK8PqR4q1lVrkZiPE+e>A;oRRA{K59J-udgZ$2H zmgF=xTdB(-g(9@;EQ*d4Ti6b^_M7=qrvHDh>%Hcl_j}&^eV+R{j8(&})iBl787rdz z;qiE=dC!?`vAQj_vKR>B!Vp9POB2-uLbtoTrnSm-K5w{_ggz&0TVc>@xFp6ShFsnf zx8c@`Dwh8PW5HI{7=_exCfgroXJ5!}uMy#{XweO7ddzXL=S_Y7bcr%D>t0W4?s1P& z=vJPm8Xzf+4bT zZhzKaHVs$oH&L?*_iwA!^kN9(R5u6@Npu50U|2Fuh=3p* zLqkE%!bvlRqb>dfEm|@N4hr_y|HdM@ybNZem#ttO6dZ;PWyFaAh45+=bi!sRP{O)M zY(!baRymnQ#7LxZpqPN3$w!-F1>!UOW6SVxl5R;X~@W!M;&UP?6c_ zy?vX!3kxj0Dz0V((e-!1T#3TAgurJ0rn5?hJs2<^Z=f%xBVsGppv&CdL zepY*3aKO}sKQ;Wj{yN=>nhnsjtXR~=4t^`fux&^ ziq@d-3aoS_sBF_-E}{}!J$ru+_;|79q0;3S`ug6)l9|q4Z^b)3XB*A$1ZVP%_nfVY zJD^tJHFA37^SqnxSl6*t!90y(i)R+sJJbc-$+-E-yS@FN<^o%3@ToYOx=xvle%CI! z2TpmvO+1`FwJFw)4}Kud(^`qZ>#EknLc;CYpH2=BT#I0K<6mXeQtbkt7yiy^d!zTH zL{T%pXH$LX-_~)dI=Qic9{p=Jx0ho+c|u73IQY`J{Yd;frweWced~P^1~Zp=vW>$8 zd{3X~C{8dtzD`$1cJLIxe)G7$ za_qp2e$t%|hoX-E&MbW~db!hp-n6^c+llQ(2(Ihm%)HL8nqDH@;bKRiSa32;_&MW` z8BYu2O9o;mG)BsM)71o!IK$tv2O<9##*Mh(MYm4MVbFqYiLk_>bsZPlg&qY^*|I3w zpt2iA(O%sBB`8W^I(|9Z6}C?XJ=++YgHx1xU^#z|jD8zt?f$}MZr`>Tx2s$` zjr2qevTZ;F*9>T;DF|ryY!n*TA}tZl2vL82tw{@)Y^&z3vIBp{_s2tbG97^7?ahG| z$38BfFZFz+%d$|=B-4FiHD9*v+0dcw{OtAhZn6wQNA1$z5=Kv5Kf7}3@^*(2W^;#% zs@Xwpm)s{7$DLM=A3j%>d9$bT>GU}B^AVj{-J{*^j;&X{-eO%jh+>N!lU6J(w>|Su z5(KT=3_)wa5gZW+Q|Y1z&J+oq0P?OhSQP`4uq3IO4(1ZROpUBW3g9JbNHhTl6ZKG# zmADQC4-(B#ppvu(1$IeBCf$dm(uh%WAy+3?W#J5Ex4l%vpm@ zV2@tTLtM47lUz;@TM3pTAxP5k5eco-5qW5>;hH^%b{{?Zy?@N*Nev}r2zqJ`K`beH z4;P@vX9FjI`q3DAwU{gmLHm|L5L3#Kw--#XSjyis105j+799Xi_IeVM|LJWhlE~vo zc1@91CrhH1>L@A0z!2cdSu2iEQdDU#lI5Nv^7tW%K|K4qiXwB*gFK-I35WPB_LvDB LL`te9LHzGug7kXT diff --git a/dependencies/arrow-memory-unsafe-10.0.1.jar b/dependencies/arrow-memory-unsafe-10.0.1.jar index c5ea8d7475d3d3f096d04c5f04f7f40bd8241d98..c291149030573627e66fe3164573e5ddf92bda6e 100644 GIT binary patch delta 1765 zcmZux3p7+)7(Vl0#v{a7WaLWYF(K29l1IiXW-zhHkhc(dHgrqLtV!P8l;hDz<(A65 zo_Ur;#$zs4D66>%#f>IwB$Zb~+%ryHy6f(<&RX9-d+-1M_jkVUFLY~g!#g{m(2@Wa zivJET&{%P^4138*nT)8@ZeM^q>xW z;Mj}HZ)QI(5K>CWl4;*#Oc0TIsCQ?(Ki@i=lU zzz5cHKx<7Vg-R5Yeda2wpDbjRSMZY3%P1KwmDtz<7BPy0dyv{E{={k|f6S6?Zrn6V zDZS|=-FK-~g~TsCVLEfV!CzNU+pQkl=e+V?!FOuAstm7x%pYcw*#0TlSYrQ#&M>ITR!qXf=Srh(4)*G%Rjs z3?n)wm=PBi9GmOv6+T2#K5H;*p*(L~X3vP^BCI>rsCLBU-1nLWjS2O2UGDw2eOA7X z8;2E!-~~yWc@NS)Prhr@m-c64nCuxJMrnd@aQ3YCbda#bx->;l>K@;&kGG+xMuxM` zYg>~zX{uF4wKms|H?Pc3j;$<>pgI>rF2+B%c9PIQyExVaUAq)mb8|4n_S5)B!9XEi z)dOwr+Neb^kStK@$*R~KHgtQY=Pqro@V3S5X@ZHNvXs{&>!(FhsuBZ!%qyGpuCH`V zp$QAeLivlaway_W{Nv~M$Q0ULue;@A5yd7g3gus!{#386CDpw97s(4mjW}sX?qh;@ z#ou-`mfr}sqs=s!%JY1hgQHqjU)40Ml{-XH2e;&zwe zF`OMTc4oQV!jab=zh|<(GA-_#8*>kno%)3yMeSF|NlW7q^Ze{C;i}<>f|0VIg55o> z6$_6{=8n>a{XW}@8Qr+D)Lz$sxWkM@52Rv}JTQ*DMOkbrZByl?^9T3e%bo7Gk4`XqU#8W1J}*A;>>Si8 zFxZX%dtC`@qMpKYUY^-zE{EfenfU&iLq*`0_}2)UN4iu_k5_p(QQyI)anIX9ThWU zLVb4yx73pr1zF;!pLQoBO_Em)hHalS>gyMxhV!@Nbxdypnh>H=*Rn&BEC&GD4XPEH zv?fO_0J-MLnna~DD732|^zDm!1WNjQGhEwcrnmZl+`kl!LRWzd)C z0P`V$GTP#yxeC~%tppnD$gj~4mJ?Nw?P}1KObkv%M*KJS+w}cWBT#H{DE8hTVw-7v Wi?t0WCN5f0fSy}o0FX!!&He!wHqIOX delta 1790 zcmZux2{e>j9RKDs%owuE*oI^*S%=A97$Kv^gb9x$nxmR{VQ5ohqMFKiUeA0XTMN@h zG`1vf(iD2uJfejrCCX6rNVL9h%sWNr-E;ou{O-NKd+-1L{!3kOVXzw2Uj~T*&}cLu zq^Hv`+nDWHYDVkQ}wdk0%Nhc>FIkK$-Y)dG-uq*ZLG`8p^eL) zC2i{}Od8#=qP&8yFrmu2RHXUGyHi|RuTEUIoHoa3;B^$$!8D=l-{H;P|dbTvV4 zE#ZROh&I07CRX6Z77yyW&FF7fM$Qp9q*VmH}wV5i)!_<2zygDef5g7$d_y~S84WF7Bvuq!I&T?w*7@vJUo7=ZFZ8f1G zz<_+{J)Y1S%_2u4sQxIlL=qGN07xJJNW}usz=Bv%2x28tq8gNnz)8V51YQbS5f)M~ zhp>|ZYtUQ@!a)})$O9?t+wk!^x=8K{{DM3Z05kvqh-Nb5_gZadBwHsi;}c?-$&8q! z>>%N;K|>XRZ<~9lSbO>KM1?>tkcTTSU2`>G?}Od#M?THlWStmaZ#nG=jIUzk%YdB+ z;yK#5ExY)>%Bs6f461e2TSFf1W_-HXDpJZEv$4@pzcSO!F59elujO!yYp#9UjvMo* zJt_Rz$3bDqk3UB|ZcQp8Uo`r5Y5qZ!`>cpN7eCDy9%h$Z7RnWZ{pS2>)8cNU9l|a{ zwea3X51gx3mP>q(BkyosappU$QbXS=;Tywyy5*!v+)jl?YI`4XraRqtqx$wXC_=C4 zU^0Q`n#k&^8-G92Y#_;+#WDR+Vv=ar)V$#Qstf>;mH?nqeF~kgEH7D3#pC#?kR#Td zjV}pT9W1=6tip{|zg(YCPn>z9DeN2DvNjd#ePg&N($e+JR9;zud_}|_KmXvG?Xig& zN9c)PA=LdRMJl2jBSXYQxzk%qLyve(XPAGgl`Rcr6T5vZv@owVzBW#&(As0LDU&U% zyy!4a3F45W#M_0(;mj+CP3ecblH9H)8##cAi+e2-RRiC3vw>jCt2zjm`inj3wK~Q&z2&jh zik#V^6O1~I|lYD5gXG{MB2~l^D62Ys-K@no15Ubx} zqe0|B8obtni<419fEqFyOIjktHU6(=yN&Io#f!pQOMx?fwG=S%u2N8mCrQBvyt5P# zO~_J^ZW3wueHEt7z)PFdfrfA{aO9U8N^(Ng?44Pn>T WET{U*!dnt|)PWLN0H7O7X8!^x9kUkz diff --git a/dependencies/arrow-vector-10.0.1.jar b/dependencies/arrow-vector-10.0.1.jar index 7c50b4364fd4c65a134a56976d579b226635cb17..c9e8c33e31afb2c7b9c720e6334c9e4e8d7d4ae2 100644 GIT binary patch delta 85049 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

N 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∈^-w;k~cStI*Smk(QY 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%0AkG{BY| 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 From 119f72bde30066c47a389a5a9c88a9a6e37556eb Mon Sep 17 00:00:00 2001 From: Dominik Przybysz <132913826+sfc-gh-dprzybysz@users.noreply.github.com> Date: Fri, 24 Nov 2023 08:50:12 +0100 Subject: [PATCH 02/17] SNOW-565154: Test download stream should fail fast on not existing file (#1566) --- .../storage/CloudStorageClientLatestIT.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/test/java/net/snowflake/client/jdbc/cloud/storage/CloudStorageClientLatestIT.java diff --git a/src/test/java/net/snowflake/client/jdbc/cloud/storage/CloudStorageClientLatestIT.java b/src/test/java/net/snowflake/client/jdbc/cloud/storage/CloudStorageClientLatestIT.java new file mode 100644 index 000000000..20a070a02 --- /dev/null +++ b/src/test/java/net/snowflake/client/jdbc/cloud/storage/CloudStorageClientLatestIT.java @@ -0,0 +1,46 @@ +package net.snowflake.client.jdbc.cloud.storage; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.fail; + +import java.io.InputStream; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.UUID; +import net.snowflake.client.category.TestCategoryOthers; +import net.snowflake.client.jdbc.BaseJDBCTest; +import net.snowflake.client.jdbc.SnowflakeConnection; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +@Category(TestCategoryOthers.class) +public class CloudStorageClientLatestIT extends BaseJDBCTest { + + /** + * Test for SNOW-565154 - it was waiting for ~5 minutes so the test is waiting much shorter time + */ + @Test(timeout = 30000L) + public void testDownloadStreamShouldFailFastOnNotExistingFile() throws Throwable { + String stageName = + "testDownloadStream_stage_" + UUID.randomUUID().toString().replaceAll("-", "_"); + try (Connection connection = getConnection(); + Statement statement = connection.createStatement()) { + try { + statement.execute("CREATE OR REPLACE TEMP STAGE " + stageName); + + try (InputStream out = + connection + .unwrap(SnowflakeConnection.class) + .downloadStream("@" + stageName, "/fileNotExist.gz", true)) { + fail("file should not exist"); + } catch (Throwable e) { + assertThat(e, instanceOf(SQLException.class)); + } + } finally { + statement.execute("DROP STAGE IF EXISTS " + stageName); + } + } + } +} From ca8ddbe37786e566ffdd542856201561bbb6471d Mon Sep 17 00:00:00 2001 From: Dominik Przybysz <132913826+sfc-gh-dprzybysz@users.noreply.github.com> Date: Mon, 27 Nov 2023 07:10:13 +0100 Subject: [PATCH 03/17] SNOW-647217: Test schema with length 255 does not cause problems (#1568) --- .../net/snowflake/client/jdbc/StatementLatestIT.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/java/net/snowflake/client/jdbc/StatementLatestIT.java b/src/test/java/net/snowflake/client/jdbc/StatementLatestIT.java index 06feef190..3349c5ee4 100644 --- a/src/test/java/net/snowflake/client/jdbc/StatementLatestIT.java +++ b/src/test/java/net/snowflake/client/jdbc/StatementLatestIT.java @@ -225,4 +225,16 @@ public void testPreparedStatementLogging() throws SQLException { } } } + + @Test // SNOW-647217 + public void testSchemaWith255CharactersDoesNotCauseException() throws SQLException { + String schemaName = "a" + SnowflakeUtil.randomAlphaNumeric(254); + try (Connection con = getConnection()) { + try (Statement stmt = con.createStatement()) { + stmt.execute("create schema " + schemaName); + stmt.execute("use schema " + schemaName); + stmt.execute("drop schema " + schemaName); + } + } + } } From 4db543430459dab106a25964083781fc9c7474b2 Mon Sep 17 00:00:00 2001 From: Piotr Fus Date: Tue, 28 Nov 2023 07:42:34 +0100 Subject: [PATCH 04/17] SNOW-977206 Fix testPreparedStatement, fix resource management in LogicalConnectionLatestIT (#1569) --- .../pooling/LogicalConnectionLatestIT.java | 185 ++++++++++-------- 1 file changed, 102 insertions(+), 83 deletions(-) diff --git a/src/test/java/net/snowflake/client/pooling/LogicalConnectionLatestIT.java b/src/test/java/net/snowflake/client/pooling/LogicalConnectionLatestIT.java index d3cf5d3bd..78957c7b3 100644 --- a/src/test/java/net/snowflake/client/pooling/LogicalConnectionLatestIT.java +++ b/src/test/java/net/snowflake/client/pooling/LogicalConnectionLatestIT.java @@ -4,7 +4,6 @@ package net.snowflake.client.pooling; import static org.junit.Assert.*; -import static org.junit.Assert.assertTrue; import java.sql.*; import java.util.Collections; @@ -28,16 +27,17 @@ public void testLogicalConnection() throws SQLException { poolDataSource = setProperties(poolDataSource); PooledConnection pooledConnection = poolDataSource.getPooledConnection(); Connection logicalConnection = pooledConnection.getConnection(); - Statement statement = + try (Statement statement = logicalConnection.createStatement( ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, - ResultSet.CLOSE_CURSORS_AT_COMMIT); - ResultSet resultSet = statement.executeQuery("show parameters"); - assertTrue(resultSet.next()); - assertFalse(logicalConnection.isClosed()); - assertEquals(ResultSet.CLOSE_CURSORS_AT_COMMIT, logicalConnection.getHoldability()); - statement.close(); + ResultSet.CLOSE_CURSORS_AT_COMMIT)) { + try (ResultSet resultSet = statement.executeQuery("show parameters")) { + assertTrue(resultSet.next()); + assertFalse(logicalConnection.isClosed()); + assertEquals(ResultSet.CLOSE_CURSORS_AT_COMMIT, logicalConnection.getHoldability()); + } + } logicalConnection.close(); assertTrue(logicalConnection.isClosed()); pooledConnection.close(); @@ -154,27 +154,32 @@ public void testTransactionStatement() throws SQLException { logicalConnection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); assertEquals(2, logicalConnection.getTransactionIsolation()); - Statement statement = logicalConnection.createStatement(); - statement.executeUpdate("create or replace table test_transaction (colA int, colB string)"); - // start a transaction - statement.executeUpdate("insert into test_transaction values (1, 'abc')"); - - // commit - logicalConnection.commit(); - ResultSet resultSet = statement.executeQuery("select count(*) from test_transaction"); - assertTrue(resultSet.next()); - assertEquals(1, resultSet.getInt(1)); - resultSet.close(); - - // rollback - statement.executeUpdate("delete from test_transaction"); - logicalConnection.rollback(); - resultSet = statement.executeQuery("select count(*) from test_transaction"); - assertTrue(resultSet.next()); - assertEquals(1, resultSet.getInt(1)); + try (Statement statement = logicalConnection.createStatement()) { + statement.executeUpdate("create or replace table test_transaction (colA int, colB string)"); + // start a transaction + statement.executeUpdate("insert into test_transaction values (1, 'abc')"); + + // commit + logicalConnection.commit(); + try (ResultSet resultSet = + statement.executeQuery("select count(*) from test_transaction")) { + assertTrue(resultSet.next()); + assertEquals(1, resultSet.getInt(1)); + } - statement.execute("drop table if exists test_transaction"); - resultSet.close(); + // rollback + statement.executeUpdate("delete from test_transaction"); + logicalConnection.rollback(); + try (ResultSet resultSet = + statement.executeQuery("select count(*) from test_transaction")) { + assertTrue(resultSet.next()); + assertEquals(1, resultSet.getInt(1)); + } + } finally { + try (Statement statement = logicalConnection.createStatement()) { + statement.execute("drop table if exists test_transaction"); + } + } } pooledConnection.close(); } @@ -212,20 +217,20 @@ public void testPreparedStatement() throws SQLException { PooledConnection pooledConnection = poolDataSource.getPooledConnection(); try (Connection logicalConnection = pooledConnection.getConnection()) { try (Statement statement = logicalConnection.createStatement()) { - statement.execute("use database " + logicalConnection.getCatalog()); - statement.execute("use schema " + logicalConnection.getSchema()); statement.execute("create or replace table test_prep (colA int, colB varchar)"); try (PreparedStatement preparedStatement = logicalConnection.prepareStatement("insert into test_prep values (?, ?)")) { preparedStatement.setInt(1, 25); preparedStatement.setString(2, "hello world"); preparedStatement.execute(); - ResultSet resultSet = statement.executeQuery("select * from test_prep"); int count = 0; - while (resultSet.next()) { - count++; + try (ResultSet resultSet = statement.executeQuery("select * from test_prep")) { + while (resultSet.next()) { + count++; + } } assertEquals(1, count); + } finally { statement.execute("drop table if exists test_prep"); } } @@ -241,15 +246,20 @@ public void testSetSchema() throws SQLException { try (Connection logicalConnection = pooledConnection.getConnection()) { String schema = logicalConnection.getSchema(); // get the current schema - ResultSet rst = logicalConnection.createStatement().executeQuery("select current_schema()"); - assertTrue(rst.next()); - assertEquals(schema, rst.getString(1)); + try (ResultSet rst = + logicalConnection.createStatement().executeQuery("select current_schema()")) { + assertTrue(rst.next()); + assertEquals(schema, rst.getString(1)); + } logicalConnection.setSchema("PUBLIC"); + // get the current schema - rst = logicalConnection.createStatement().executeQuery("select current_schema()"); - assertTrue(rst.next()); - assertEquals("PUBLIC", rst.getString(1)); + try (ResultSet rst = + logicalConnection.createStatement().executeQuery("select current_schema()")) { + assertTrue(rst.next()); + assertEquals("PUBLIC", rst.getString(1)); + } } pooledConnection.close(); } @@ -268,40 +278,44 @@ public void testPrepareCall() throws SQLException { poolDataSource = setProperties(poolDataSource); PooledConnection pooledConnection = poolDataSource.getPooledConnection(); try (Connection logicalConnection = pooledConnection.getConnection()) { - Statement statement = logicalConnection.createStatement(); - statement.execute(procedure); - - CallableStatement callableStatement = logicalConnection.prepareCall("call output_message(?)"); - callableStatement.setString(1, "hello world"); - ResultSet resultSet = callableStatement.executeQuery(); - resultSet.next(); - assertEquals("hello world", resultSet.getString(1)); - - callableStatement = - logicalConnection.prepareCall( - "call output_message('hello world')", - ResultSet.TYPE_FORWARD_ONLY, - ResultSet.CONCUR_READ_ONLY); - resultSet = callableStatement.executeQuery(); - resultSet.next(); - assertEquals("hello world", resultSet.getString(1)); - assertEquals(1003, callableStatement.getResultSetType()); - assertEquals(1007, callableStatement.getResultSetConcurrency()); + try (Statement statement = logicalConnection.createStatement()) { + statement.execute(procedure); + + try (CallableStatement callableStatement = + logicalConnection.prepareCall("call output_message(?)")) { + callableStatement.setString(1, "hello world"); + try (ResultSet resultSet = callableStatement.executeQuery()) { + resultSet.next(); + assertEquals("hello world", resultSet.getString(1)); + } + } - callableStatement = - logicalConnection.prepareCall( - "call output_message('hello world')", - ResultSet.TYPE_FORWARD_ONLY, - ResultSet.CONCUR_READ_ONLY, - ResultSet.CLOSE_CURSORS_AT_COMMIT); - resultSet = callableStatement.executeQuery(); - resultSet.next(); - assertEquals(2, callableStatement.getResultSetHoldability()); + try (CallableStatement callableStatement = + logicalConnection.prepareCall( + "call output_message('hello world')", + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY)) { + try (ResultSet resultSet = callableStatement.executeQuery()) { + resultSet.next(); + assertEquals("hello world", resultSet.getString(1)); + assertEquals(1003, callableStatement.getResultSetType()); + assertEquals(1007, callableStatement.getResultSetConcurrency()); + } + } - statement.execute("drop procedure if exists output_message(varchar)"); - statement.close(); - resultSet.close(); - callableStatement.close(); + try (CallableStatement callableStatement = + logicalConnection.prepareCall( + "call output_message('hello world')", + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.CLOSE_CURSORS_AT_COMMIT)) { + try (ResultSet resultSet = callableStatement.executeQuery()) { + resultSet.next(); + assertEquals(2, callableStatement.getResultSetHoldability()); + } + } + statement.execute("drop procedure if exists output_message(varchar)"); + } } } @@ -311,20 +325,25 @@ public void testClob() throws SQLException { poolDataSource = setProperties(poolDataSource); PooledConnection pooledConnection = poolDataSource.getPooledConnection(); try (Connection logicalConnection = pooledConnection.getConnection()) { - Statement statement = logicalConnection.createStatement(); - statement.execute("create or replace table test_clob (colA text)"); + try (Statement statement = logicalConnection.createStatement()) { + statement.execute("create or replace table test_clob (colA text)"); + } - Clob clob = logicalConnection.createClob(); - clob.setString(1, "hello world"); - PreparedStatement preparedStatement = - logicalConnection.prepareStatement("insert into test_clob values (?)"); - preparedStatement.setClob(1, clob); - preparedStatement.execute(); + try (PreparedStatement preparedStatement = + logicalConnection.prepareStatement("insert into test_clob values (?)")) { + Clob clob = logicalConnection.createClob(); + clob.setString(1, "hello world"); + preparedStatement.setClob(1, clob); + preparedStatement.execute(); + } - statement.execute("select * from test_clob"); - ResultSet resultSet = statement.getResultSet(); - resultSet.next(); - assertEquals("hello world", resultSet.getString("COLA")); + try (Statement statement = logicalConnection.createStatement()) { + statement.execute("select * from test_clob"); + try (ResultSet resultSet = statement.getResultSet()) { + resultSet.next(); + assertEquals("hello world", resultSet.getString("COLA")); + } + } } } From dcb74ed6ed9150226828a14155a0c8149859fd8f Mon Sep 17 00:00:00 2001 From: Piotr Fus Date: Thu, 30 Nov 2023 10:59:46 +0100 Subject: [PATCH 05/17] SNOW-937199 Use JDK 11 and 17 in Jenkins (#1564) --- Jenkinsfile | 79 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index a18d9ebdb..5c62291a8 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,5 +1,12 @@ import groovy.json.JsonOutput +class JdbcJobDefinition { + String jdk + List params + String jobToRun + String runName +} + pipeline { // TODO Please migrate this to C7 as sfc-dev2 servers do not support c6 nodes agent { label 'regular-memory-node' } @@ -23,7 +30,6 @@ pipeline { } timestamps { - // TODO Please migrate this to C7 as sfc-dev2 servers do not support c6 nodes node('regular-memory-node') { stage('checkout') { scmInfo = checkout scm @@ -40,39 +46,50 @@ timestamps { '''.stripMargin() } - params = [ - string(name: 'client_git_branch', value: scmInfo.GIT_BRANCH), - string(name: 'client_git_commit', value: scmInfo.GIT_COMMIT), - string(name: 'branch', value: 'main'), - string(name: 'TARGET_DOCKER_TEST_IMAGE', value: 'jdbc-centos6-default'), - string(name: 'parent_job', value: env.JOB_NAME), - string(name: 'parent_build_number', value: env.BUILD_NUMBER), - string(name: 'timeout_value', value: '420'), - string(name: 'PR_Key', value: scmInfo.GIT_BRANCH.substring(3)) - ] - stage('Test') { - try { - def commit_hash = "main" // default which we want to override - def bptp_tag = "bptp-built" + def commit_hash = "main" // default which we want to override + def bptp_tag = "bptp-built" + try { def response = authenticatedGithubCall("https://api.github.com/repos/snowflakedb/snowflake/git/ref/tags/${bptp_tag}") commit_hash = response.object.sha - // Append the bptp-built commit sha to params - params += [string(name: 'svn_revision', value: commit_hash)] - } catch(Exception e) { - println("Exception computing commit hash from: ${response}") + } catch (Exception e) { + println("error when calling Github API: ${e.message}") + e.printStackTrace() + } + + jdkToParams = ['openjdk8': 'jdbc-centos7-openjdk8', 'openjdk11': 'jdbc-centos7-openjdk11', 'openjdk17': 'jdbc-centos7-openjdk17'].collectEntries { jdk, image -> + return [(jdk): [ + string(name: 'client_git_branch', value: scmInfo.GIT_BRANCH), + string(name: 'client_git_commit', value: scmInfo.GIT_COMMIT), + string(name: 'branch', value: 'main'), + string(name: 'TARGET_DOCKER_TEST_IMAGE', value: image), + string(name: 'parent_job', value: env.JOB_NAME), + string(name: 'parent_build_number', value: env.BUILD_NUMBER), + string(name: 'timeout_value', value: '420'), + string(name: 'PR_Key', value: scmInfo.GIT_BRANCH.substring(3)), + string(name: 'svn_revision', value: commit_hash) + ]] + } + + jobDefinitions = jdkToParams.collectMany { jdk, params -> + return [ + 'RT-LanguageJDBC1-PC' : "Test JDBC 1 - $jdk", + 'RT-LanguageJDBC2-PC' : "Test JDBC 2 - $jdk", + 'RT-LanguageJDBC3-PC' : "Test JDBC 3 - $jdk", + 'RT-LanguageJDBC4-PC' : "Test JDBC 4 - $jdk", + 'RT-LanguageJDBC-CodeCoverage-PC' : "CodeCoverage JDBC - $jdk" + ].collect { jobToRun, runName -> + return new JdbcJobDefinition( + jdk: jdk, + params: params, + jobToRun: jobToRun, + runName: runName + ) } - parallel ( - 'Test JDBC 1': { build job: 'RT-LanguageJDBC1-PC',parameters: params - }, - 'Test JDBC 2': { build job: 'RT-LanguageJDBC2-PC',parameters: params - }, - 'Test JDBC 3': { build job: 'RT-LanguageJDBC3-PC',parameters: params - }, - 'Test JDBC 4': { build job: 'RT-LanguageJDBC4-PC',parameters: params - }, - 'CodeCoverage JDBC': { build job: 'RT-LanguageJDBC-CodeCoverage-PC',parameters: params - } - ) + }.collectEntries { jobDefinition -> + return [(jobDefinition.runName): { build job: jobDefinition.jobToRun, parameters: jobDefinition.params }] + } + stage('Test') { + parallel (jobDefinitions) } } } From dacea075ca56ea26b2bc57ec87bab7c05bfe3b64 Mon Sep 17 00:00:00 2001 From: Saber Mirzaei Date: Fri, 1 Dec 2023 11:48:48 -0800 Subject: [PATCH 06/17] SNOW-957349: Make some API/classes needed in stored procs public (#1570) Make some API/classes needed in stored procs public --- .../java/net/snowflake/client/core/SFChildResult.java | 10 ++++++---- .../java/net/snowflake/client/core/SFStatement.java | 4 ++-- .../net/snowflake/client/jdbc/SFAsyncResultSet.java | 5 +++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/java/net/snowflake/client/core/SFChildResult.java b/src/main/java/net/snowflake/client/core/SFChildResult.java index bc74440fc..4418e80d1 100644 --- a/src/main/java/net/snowflake/client/core/SFChildResult.java +++ b/src/main/java/net/snowflake/client/core/SFChildResult.java @@ -3,20 +3,22 @@ /** Data class to wrap information about child job results */ public class SFChildResult { // query id of child query, to look up child result - String id; + private final String id; // statement type of child query, to properly interpret result - SFStatementType type; + private final SFStatementType type; public SFChildResult(String id, SFStatementType type) { this.id = id; this.type = type; } - String getId() { + // For Snowflake internal use + public String getId() { return id; } - SFStatementType getType() { + // For Snowflake internal use + public SFStatementType getType() { return type; } } diff --git a/src/main/java/net/snowflake/client/core/SFStatement.java b/src/main/java/net/snowflake/client/core/SFStatement.java index b251d2f25..10f9c9559 100644 --- a/src/main/java/net/snowflake/client/core/SFStatement.java +++ b/src/main/java/net/snowflake/client/core/SFStatement.java @@ -238,7 +238,7 @@ SFBaseResultSet executeQueryInternal( // current result to the first child's result. // we still construct the first result set for its side effects. if (!childResults.isEmpty()) { - SFStatementType type = childResults.get(0).type; + SFStatementType type = childResults.get(0).getType(); // ensure first query type matches the calling JDBC method, if exists if (caller == CallingMethod.EXECUTE_QUERY && !type.isGenerateResultSet()) { @@ -661,7 +661,7 @@ public String[] getChildQueryIds(String queryID) throws SQLException { List childResults = ResultUtil.getChildResults(session, requestId, jsonResult); List resultList = new ArrayList<>(); for (int i = 0; i < childResults.size(); i++) { - resultList.add(childResults.get(i).id); + resultList.add(childResults.get(i).getId()); } if (resultList.isEmpty()) { resultList.add(queryID); diff --git a/src/main/java/net/snowflake/client/jdbc/SFAsyncResultSet.java b/src/main/java/net/snowflake/client/jdbc/SFAsyncResultSet.java index 7fe9ecbbe..77662cbd8 100644 --- a/src/main/java/net/snowflake/client/jdbc/SFAsyncResultSet.java +++ b/src/main/java/net/snowflake/client/jdbc/SFAsyncResultSet.java @@ -18,8 +18,9 @@ import net.snowflake.client.core.SFSession; import net.snowflake.common.core.SqlState; -/** SFAsyncResultSet implementation */ -class SFAsyncResultSet extends SnowflakeBaseResultSet implements SnowflakeResultSet, ResultSet { +/** SFAsyncResultSet implementation. Note: For Snowflake internal use */ +public class SFAsyncResultSet extends SnowflakeBaseResultSet + implements SnowflakeResultSet, ResultSet { private final SFBaseResultSet sfBaseResultSet; private ResultSet resultSetForNext = new SnowflakeResultSetV1.EmptyResultSet(); private boolean resultSetForNextInitialized = false; From 1bf4e1f979cf34d0640f827098c6cc39c45f329f Mon Sep 17 00:00:00 2001 From: Joyce <115662568+sfc-gh-ext-simba-jl@users.noreply.github.com> Date: Tue, 5 Dec 2023 10:08:14 -0800 Subject: [PATCH 07/17] SNOW-919389: move the dependencies to parent pom file (#1546) * move the dependencies to base_pom.xml * update base pom * update pom files * change hamcrest-core to hamcrest * add hamcrest dependency * fix BanDuplicateClasses for hamcrest-core * add netty-buffer dependency * fix missing netty dependencies error * update bouncycastle properties --- FIPS/pom.xml | 379 +----- FIPS/public_pom.xml | 6 +- parent-pom.xml | 702 ++++++++++ pom.xml | 2988 ++++++++++++++++++------------------------- 4 files changed, 1983 insertions(+), 2092 deletions(-) create mode 100644 parent-pom.xml diff --git a/FIPS/pom.xml b/FIPS/pom.xml index f98d95ced..533da6be1 100644 --- a/FIPS/pom.xml +++ b/FIPS/pom.xml @@ -2,9 +2,14 @@ 4.0.0 - net.snowflake + + net.snowflake + snowflake-jdbc-parent + 3.14.3 + ../parent-pom.xml + + snowflake-jdbc-fips - 3.14.3 jar snowflake-jdbc-fips @@ -34,354 +39,14 @@ http://127.0.0.1/websvn/dummy - - UTF-8 - UTF-8 - 10.0.1 - 1.7.36 - 1.15.3 - 2.4.1 - 2.15.3 - 4.5.11 - 0.8.4 - true - 1.12.501 - 1.33.2 - 1.43.2 - 5.13.0 - 3.5.6 - net.snowflake.client.jdbc.internal - net.snowflake.client.category.AllTestCategory - - - - - - com.amazonaws - aws-java-sdk-bom - ${awssdk.version} - pom - import - - - com.fasterxml.jackson - jackson-bom - ${jackson.version} - pom - import - - - io.grpc - grpc-bom - 1.59.0 - pom - import - - - io.netty - netty-bom - 4.1.100.Final - pom - import - - - - - io.netty - netty-common - - - io.netty - netty-buffer - - - com.nimbusds - nimbus-jose-jwt - 9.21 - - - net.minidev - json-smart - 2.4.9 - - - org.apache.maven.plugins - maven-failsafe-plugin - 3.0.0-M1 - test - - - junit - junit - 4.13.1 - jar - test - - - org.hamcrest - hamcrest - 2.1 - jar - test - - - org.mockito - mockito-inline - ${mockito.version} - test - - - commons-dbcp - commons-dbcp - 1.4 - test - - - com.mchange - c3p0 - 0.9.5.4 - jar - test - - - com.zaxxer - HikariCP - 2.4.3 - test - - - org.apache.httpcomponents - httpclient - ${httpclient.version} - jar - - - commons-io - commons-io - 2.7 - jar - - - com.amazonaws - aws-java-sdk-s3 - - - com.fasterxml.jackson.core - jackson-databind - jar - - - com.fasterxml.jackson.core - jackson-core - - - com.fasterxml.jackson.core - jackson-annotations - - - com.google.guava - guava - 32.0.0-jre - - - net.snowflake - snowflake-common - 5.1.4 - jar - compile - - - commons-cli - commons-cli - 1.2 - jar - test - - - org.apache.commons - commons-lang3 - 3.11 - jar - test - - - org.apache.commons - commons-text - 1.10.0 - jar - test - - - org.codehaus.mojo - exec-maven-plugin - 1.2.1 - test - - - org.codehaus.plexus - plexus-archiver - 4.8.0 - provided - - - com.yammer.metrics - metrics-graphite - 2.2.0 - - - com.yammer.metrics - metrics-servlet - 2.2.0 - - - javax.servlet - javax.servlet-api - 3.1.0 - compile - - - org.apache.tika - tika-core - ${tika.version} - - - org.apache.avro - avro - 1.8.1 - test - - - org.slf4j - slf4j-api - ${slf4j.version} - provided - - - org.jsoup - jsoup - ${jsoup.version} - - - com.microsoft.azure - azure-storage - 5.0.0 - org.bouncycastle bc-fips - 1.0.2.4 - provided org.bouncycastle bcpkix-fips - 1.0.5 - provided - - - com.amazonaws - aws-java-sdk-sns - test - - - ch.qos.logback - logback-classic - 1.2.3 - test - - - org.apache.arrow - arrow-vector - ${arrow.version} - - - io.netty - netty-common - - - io.netty - netty-buffer - - - - - org.apache.arrow - arrow-memory-netty - ${arrow.version} - - - io.netty - netty-common - - - io.netty - netty-buffer - - - - - org.apache.arrow - arrow-memory-unsafe - ${arrow.version} - - - com.google.auth - google-auth-library-oauth2-http - 1.12.1 - - - com.google.cloud - google-cloud-storage - 2.22.5 - - - com.google.code.gson - gson - - - com.google.oauth-client - google-oauth-client - - - - - com.google.auth - google-auth-library-credentials - 1.15.0 - - - com.google.api-client - google-api-client - ${google.api.client.version} - - - com.google.oauth-client - google-oauth-client - - - - - com.google.oauth-client - google-oauth-client - 1.33.3 - - - com.google.http-client - google-http-client - ${google.http.client.version} - - - net.java.dev.jna - jna - ${jna.version} - provided - - - net.java.dev.jna - jna-platform - ${jna.version} - provided @@ -404,7 +69,7 @@ org.apache.maven.plugins maven-install-plugin - 3.0.0-M1 + ${version.plugin.install} install-tika-core @@ -516,7 +181,7 @@ org.apache.maven.plugins maven-clean-plugin - 3.0.0 + ${version.plugin.clean} @@ -532,7 +197,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.0 + ${version.plugin.compiler} true true @@ -548,6 +213,7 @@ org.apache.maven.plugins maven-surefire-plugin + ${version.plugin.surefire} **/*SFTrustManagerTest.java @@ -557,7 +223,7 @@ org.jacoco jacoco-maven-plugin - ${jacoco.version} + ${version.plugin.jacoco} pre-unit-test @@ -587,7 +253,7 @@ org.apache.maven.plugins maven-jar-plugin - 2.4 + ${version.plugin.jar} @@ -606,6 +272,7 @@ maven-dependency-plugin + ${version.plugin.dependency} install-jar @@ -629,7 +296,7 @@ org.apache.maven.plugins maven-source-plugin - 3.0.1 + ${version.plugin.source} attach-sources @@ -642,7 +309,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.1.0 + ${version.plugin.javadoc} 8 @@ -658,7 +325,7 @@ org.codehaus.mojo exec-maven-plugin - 1.6.0 + ${version.plugin.exec} check-shaded-content @@ -679,7 +346,7 @@ org.apache.maven.plugins maven-project-info-reports-plugin - 2.9 + ${version.plugin.projectinforeports} @@ -697,7 +364,7 @@ org.codehaus.mojo buildnumber-maven-plugin - 1.2 + ${version.plugin.buildnumber} package @@ -719,7 +386,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.1 + ${version.plugin.shade} @@ -961,7 +628,7 @@ com.coveo fmt-maven-plugin - 2.10 + ${version.plugin.fmt} @@ -982,6 +649,7 @@ maven-failsafe-plugin + ${version.plugin.failsafe} --add-opens=java.base/java.io=ALL-UNNAMED @@ -1025,6 +693,7 @@ org.apache.maven.plugins maven-failsafe-plugin + ${version.plugin.failsafe} DefaultIT @@ -1068,7 +737,7 @@ org.apache.maven.plugins maven-gpg-plugin - 1.6 + ${version.plugin.gpg} deploy diff --git a/FIPS/public_pom.xml b/FIPS/public_pom.xml index 8a576e5ff..d180e4a57 100644 --- a/FIPS/public_pom.xml +++ b/FIPS/public_pom.xml @@ -32,6 +32,8 @@ + 1.0.2.4 + 1.0.5 5.13.0 @@ -39,13 +41,13 @@ org.bouncycastle bc-fips - 1.0.2.1 + ${bouncycastle.bcfips.version} runtime org.bouncycastle bcpkix-fips - 1.0.5 + ${bouncycastle.bcpkixfips.version} runtime diff --git a/parent-pom.xml b/parent-pom.xml new file mode 100644 index 000000000..4b7385c81 --- /dev/null +++ b/parent-pom.xml @@ -0,0 +1,702 @@ + + + 4.0.0 + + net.snowflake + snowflake-jdbc-parent + 3.14.3 + pom + + + 1.21 + 3.12.0 + 1.10.0 + 4.5.14 + 4.4.16 + 10.0.1 + 9.3 + 1.8.1 + 1.12.501 + 5.0.0 + 1.74 + 1.0.2.4 + 1.0.5 + 1.1 + 3.33.0 + 1.2 + 1.15 + 1.4 + 2.11.0 + 1.2 + 1.5.4 + 0.9.5.4 + 2.22.0 + 2.21.0 + 2.22.6 + 2.10.1 + 2.18.0 + 1.12.0 + 2.31.0 + 32.1.1-jre + 1.43.3 + 3.0.2 + 3.23.3 + 1.59.0 + 2.2 + 2.4.3 + 2.15.3 + true + 3.1.0 + 5.13.0 + 2.8.1 + 2.4.9 + 4.13.2 + 1.15.3 + 1.3.6 + 2.2.0 + 4.11.0 + 4.1.100.Final + 9.21 + 0.31.1 + 1.0-alpha-9-stable-1 + 3.4.2 + UTF-8 + UTF-8 + net/snowflake/client/jdbc/internal + net.snowflake.client.jdbc.internal + 2.0.6 + 5.1.4 + net.snowflake.client.category.AllTestCategory + 1.6.8 + 2.4.1 + 1.9 + 3.6.3 + 3.1.0 + 3.0.0 + 3.2.0 + 3.11.0 + 3.5.0 + 3.1.1 + 3.0.0-M3 + 3.1.0 + 3.0.0 + 2.19 + 3.0.1 + 3.1.1 + 0.8.8 + 0.17.2 + 3.3.0 + 3.5.0 + 3.4.2 + 3.4.1 + 3.0.1 + 3.2.1 + 3.0.0 + + + + + + com.amazonaws + aws-java-sdk-bom + ${awssdk.version} + pom + import + + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + + com.google.http-client + google-http-client-bom + ${google.http.client.version} + pom + import + + + com.google.protobuf + protobuf-bom + ${google.protobuf.java.version} + pom + import + + + io.grpc + grpc-bom + ${grpc.version} + pom + import + + + io.netty + netty-bom + ${netty.version} + pom + import + + + classworlds + classworlds + ${classworlds.version} + + + com.google.api + gax + ${google.gax.version} + + + com.google.api.grpc + proto-google-common-protos + ${google.api.grpc.version} + + + com.google.cloud + google-cloud-core + ${google.cloud.core.version} + + + com.google.cloud + google-cloud-storage + ${google.cloud.storage.version} + + + com.google.code.findbugs + jsr305 + ${google.jsr305.version} + + + com.google.code.gson + gson + ${google.code.gson.version} + + + com.google.errorprone + error_prone_annotations + ${google.errorprone.version} + + + com.google.guava + guava + ${google.guava.version} + + + com.microsoft.azure + azure-storage + ${azure.storage.version} + + + com.nimbusds + nimbus-jose-jwt + ${nimbusds.version} + + + com.yammer.metrics + metrics-core + ${metrics.version} + + + com.yammer.metrics + metrics-servlet + ${metrics.version} + + + commons-cli + commons-cli + ${commons.cli.version} + test + + + commons-codec + commons-codec + ${commons.codec.version} + + + commons-io + commons-io + ${commons.io.version} + + + commons-logging + commons-logging + ${commons.logging.version} + + + commons-dbcp + commons-dbcp + ${commons.dbcp.version} + test + + + commons-pool + commons-pool + ${commons.pool.version} + test + + + io.opencensus + opencensus-api + ${opencensus.version} + + + junit + junit + ${junit.version} + test + + + org.apache.avro + avro + ${avro.version} + test + + + org.apache.commons + commons-compress + ${apache.commons.compress.version} + test + + + org.apache.commons + commons-lang3 + ${apache.commons.lang3.version} + test + + + org.apache.commons + commons-text + ${apache.commons.text.version} + test + + + javax.servlet + javax.servlet-api + ${javax.servlet.version} + + + joda-time + joda-time + ${joda.time.version} + + + net.minidev + json-smart + ${json.smart.version} + + + net.snowflake + snowflake-common + ${snowflake.common.version} + + + org.apache.arrow + arrow-memory-core + ${arrow.version} + + + org.apache.arrow + arrow-vector + ${arrow.version} + + + org.apache.httpcomponents + httpclient + ${apache.httpclient.version} + + + org.apache.httpcomponents + httpcore + ${apache.httpcore.version} + + + org.apache.tika + tika-core + ${tika.version} + + + org.jsoup + jsoup + ${jsoup.version} + + + net.java.dev.jna + jna + ${jna.version} + provided + + + net.java.dev.jna + jna-platform + ${jna.version} + provided + + + org.checkerframework + checker-qual + ${checkerframework.version} + + + org.codehaus.plexus + plexus-container-default + ${plexus.container.version} + + + org.codehaus.plexus + plexus-utils + ${plexus.utils.version} + + + org.hamcrest + hamcrest + ${hamcrest.version} + test + + + org.hamcrest + hamcrest-core + ${hamcrest.version} + test + + + org.ow2.asm + asm + ${asm.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + provided + + + + com.google.flatbuffers + flatbuffers-java + ${google.flatbuffers.version} + runtime + + + org.apache.arrow + arrow-format + ${arrow.version} + runtime + + + org.apache.arrow + arrow-memory-netty + ${arrow.version} + runtime + + + org.apache.arrow + arrow-memory-unsafe + ${arrow.version} + runtime + + + ch.qos.logback + logback-classic + ${logback.version} + test + + + ch.qos.logback + logback-core + ${logback.version} + test + + + com.mchange + c3p0 + ${c3p0.version} + test + + + com.zaxxer + HikariCP + ${hikaricp.version} + test + + + + org.bouncycastle + bcpkix-jdk18on + ${bouncycastle.version} + + + + org.bouncycastle + bcprov-jdk18on + ${bouncycastle.version} + + + + org.bouncycastle + bc-fips + ${bouncycastle.bcfips.version} + provided + + + + org.bouncycastle + bcpkix-fips + ${bouncycastle.bcpkixfips.version} + provided + + + org.threeten + threetenbp + ${threeten.version} + + + org.tukaani + xz + ${tukaani.version} + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.mockito + mockito-inline + ${mockito.version} + test + + + + + + + com.amazonaws + aws-java-sdk-core + + + com.amazonaws + aws-java-sdk-kms + + + com.amazonaws + aws-java-sdk-s3 + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.google.api + gax + + + com.google.cloud + google-cloud-core + + + com.google.cloud + google-cloud-storage + + + com.google.code.findbugs + jsr305 + + + com.google.guava + guava + + + com.google.http-client + google-http-client + + + com.microsoft.azure + azure-storage + + + com.nimbusds + nimbus-jose-jwt + + + com.yammer.metrics + metrics-core + + + com.yammer.metrics + metrics-servlet + + + commons-codec + commons-codec + + + commons-io + commons-io + + + commons-logging + commons-logging + + + javax.servlet + javax.servlet-api + + + joda-time + joda-time + + + net.minidev + json-smart + + + io.netty + netty-common + runtime + + + io.netty + netty-buffer + runtime + + + net.snowflake + snowflake-common + + + org.apache.arrow + arrow-memory-core + + + org.apache.arrow + arrow-vector + + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpcore + + + org.apache.tika + tika-core + + + org.jsoup + jsoup + + + net.java.dev.jna + jna + + + net.java.dev.jna + jna-platform + + + org.slf4j + slf4j-api + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + runtime + + + + com.google.flatbuffers + flatbuffers-java + + + org.apache.arrow + arrow-format + + + org.apache.arrow + arrow-memory-netty + + + org.apache.arrow + arrow-memory-unsafe + + + ch.qos.logback + logback-classic + + + ch.qos.logback + logback-core + + + com.amazonaws + aws-java-sdk-sns + test + + + com.mchange + c3p0 + + + com.zaxxer + HikariCP + + + commons-cli + commons-cli + + + commons-dbcp + commons-dbcp + + + commons-pool + commons-pool + + + junit + junit + + + org.apache.avro + avro + + + org.apache.commons + commons-compress + + + org.apache.commons + commons-lang3 + + + org.apache.commons + commons-text + + + org.hamcrest + hamcrest + + + org.mockito + mockito-core + + + org.mockito + mockito-inline + + + diff --git a/pom.xml b/pom.xml index eb8286ded..a7748dcf6 100644 --- a/pom.xml +++ b/pom.xml @@ -1,1735 +1,1253 @@ - - - 4.0.0 - - net.snowflake - snowflake-jdbc - 3.14.3 - jar - - snowflake-jdbc - https://github.com/snowflakedb/snowflake-jdbc - - - scm:git:https://github.com/snowflakedb/snowflake-jdbc.git - https://github.com/snowflakedb/snowflake-jdbc - - - - 10.0.1 - 1.12.501 - 1.74 - 1.34.0 - true - 5.13.0 - 1.15.3 - 4.11.0 - UTF-8 - UTF-8 - net/snowflake/client/jdbc/internal - net.snowflake.client.jdbc.internal - net.snowflake.client.category.AllTestCategory - 2.4.1 - 3.6.3 - 3.1.0 - 3.0.0 - 3.2.0 - 3.11.0 - 3.5.0 - 3.1.1 - 3.0.0-M3 - 3.1.0 - 3.0.0 - 2.19 - 3.0.1 - 3.1.1 - 0.8.8 - 0.17.2 - 3.3.0 - 3.5.0 - 3.4.2 - 3.4.1 - 3.0.1 - 3.2.1 - 3.0.0 - - - - - - com.amazonaws - aws-java-sdk-bom - ${awssdk.version} - pom - import - - - com.fasterxml.jackson - jackson-bom - 2.15.3 - pom - import - - - io.grpc - grpc-bom - 1.59.0 - pom - import - - - io.netty - netty-bom - 4.1.100.Final - pom - import - - - classworlds - classworlds - 1.1 - - - com.google.api.grpc - proto-google-common-protos - 2.22.0 - - - com.google.code.gson - gson - 2.10.1 - - - com.google.errorprone - error_prone_annotations - 2.18.0 - - - com.google.guava - guava - 32.1.1-jre - - - com.google.http-client - google-http-client - 1.43.3 - - - com.google.http-client - google-http-client-gson - 1.43.3 - - - com.google.protobuf - protobuf-java - 3.23.3 - - - com.google.protobuf - protobuf-java-util - 3.23.3 - - - commons-cli - commons-cli - 1.2 - - - commons-codec - commons-codec - 1.15 - - - commons-io - commons-io - 2.11.0 - - - commons-logging - commons-logging - 1.2 - - - io.opencensus - opencensus-api - 0.31.1 - - - junit - junit - 4.13.2 - - - org.apache.commons - commons-compress - 1.21 - - - org.apache.commons - commons-lang3 - 3.12.0 - - - org.apache.httpcomponents - httpclient - 4.5.14 - - - org.apache.httpcomponents - httpcore - 4.4.16 - - - org.checkerframework - checker-qual - 3.33.0 - - - org.codehaus.plexus - plexus-container-default - 1.0-alpha-9-stable-1 - - - org.codehaus.plexus - plexus-utils - 3.4.2 - - - org.hamcrest - hamcrest-core - 2.2 - - - org.ow2.asm - asm - 9.3 - - - org.slf4j - slf4j-api - 2.0.6 - - - org.threeten - threetenbp - 1.6.8 - - - org.tukaani - xz - 1.9 - - - - - - - com.amazonaws - aws-java-sdk-core - - - com.amazonaws - aws-java-sdk-kms - - - com.amazonaws - aws-java-sdk-s3 - - - com.fasterxml.jackson.core - jackson-annotations - - - com.fasterxml.jackson.core - jackson-core - - - com.fasterxml.jackson.core - jackson-databind - - - com.google.api - gax - 2.31.0 - - - com.google.cloud - google-cloud-core - 2.21.0 - - - com.google.cloud - google-cloud-storage - 2.22.6 - - - com.google.code.findbugs - jsr305 - 3.0.2 - - - com.google.guava - guava - - - com.google.http-client - google-http-client - - - com.microsoft.azure - azure-storage - 5.0.0 - - - com.nimbusds - nimbus-jose-jwt - 9.21 - - - com.yammer.metrics - metrics-core - 2.2.0 - - - com.yammer.metrics - metrics-servlet - 2.2.0 - - - commons-codec - commons-codec - - - commons-io - commons-io - - - commons-logging - commons-logging - 1.2 - - - javax.servlet - javax.servlet-api - 3.1.0 - - - joda-time - joda-time - 2.8.1 - - - net.minidev - json-smart - 2.4.9 - - - net.snowflake - snowflake-common - 5.1.4 - - - org.apache.arrow - arrow-memory-core - ${arrow.version} - - - org.apache.arrow - arrow-vector - ${arrow.version} - - - org.apache.httpcomponents - httpclient - - - org.apache.httpcomponents - httpcore - - - org.apache.tika - tika-core - ${tika.version} - - - org.bouncycastle - bcpkix-jdk18on - ${bouncycastle.version} - - - org.bouncycastle - bcprov-jdk18on - ${bouncycastle.version} - - - org.jsoup - jsoup - ${jsoup.version} - - - net.java.dev.jna - jna - ${jna.version} - provided - - - net.java.dev.jna - jna-platform - ${jna.version} - provided - - - org.slf4j - slf4j-api - provided - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - runtime - - - - com.google.flatbuffers - flatbuffers-java - 1.12.0 - runtime - - - io.netty - netty-buffer - runtime - - - org.apache.arrow - arrow-format - ${arrow.version} - runtime - - - org.apache.arrow - arrow-memory-netty - ${arrow.version} - runtime - - - org.apache.arrow - arrow-memory-unsafe - ${arrow.version} - runtime - - - ch.qos.logback - logback-classic - 1.3.6 - test - - - ch.qos.logback - logback-core - 1.3.6 - test - - - com.amazonaws - aws-java-sdk-sns - test - - - com.mchange - c3p0 - 0.9.5.4 - test - - - com.zaxxer - HikariCP - 2.4.3 - test - - - commons-cli - commons-cli - jar - test - - - commons-dbcp - commons-dbcp - 1.4 - test - - - commons-pool - commons-pool - 1.5.4 - test - - - junit - junit - test - - - org.apache.avro - avro - 1.8.1 - test - - - org.apache.commons - commons-compress - test - - - org.apache.commons - commons-lang3 - test - - - org.apache.commons - commons-text - 1.10.0 - test - - - org.hamcrest - hamcrest - 2.2 - test - - - org.mockito - mockito-core - ${mockito.version} - test - - - org.mockito - mockito-inline - ${mockito.version} - test - - - - - ${project.artifactId} - - - true - src/main/resources - - - - - - com.github.ekryd.sortpom - sortpom-maven-plugin - ${version.plugin.sortpom} - - - com.github.siom79.japicmp - japicmp-maven-plugin - ${version.plugin.japicmp} - - - com.spotify.fmt - fmt-maven-plugin - ${version.plugin.fmt} - - - org.apache.maven.plugins - maven-antrun-plugin - ${version.plugin.antrun} - - - org.apache.maven.plugins - maven-clean-plugin - ${version.plugin.clean} - - - org.apache.maven.plugins - maven-compiler-plugin - ${version.plugin.compiler} - - - org.apache.maven.plugins - maven-dependency-plugin - ${version.plugin.dependency} - - - org.apache.maven.plugins - maven-deploy-plugin - ${version.plugin.deploy} - - - org.apache.maven.plugins - maven-enforcer-plugin - ${version.plugin.enforcer} - - - org.apache.maven.plugins - maven-failsafe-plugin - ${version.plugin.failsafe} - - - org.apache.maven.plugins - maven-gpg-plugin - ${version.plugin.gpg} - - - org.apache.maven.plugins - maven-install-plugin - ${version.plugin.install} - - - org.apache.maven.plugins - maven-jar-plugin - ${version.plugin.jar} - - - org.apache.maven.plugins - maven-javadoc-plugin - ${version.plugin.javadoc} - - - org.apache.maven.plugins - maven-project-info-reports-plugin - ${version.plugin.projectinforeports} - - - org.apache.maven.plugins - maven-shade-plugin - ${version.plugin.shade} - - - org.apache.maven.plugins - maven-source-plugin - ${version.plugin.source} - - - org.apache.maven.plugins - maven-surefire-plugin - ${version.plugin.surefire} - - - org.codehaus.mojo - buildnumber-maven-plugin - ${version.plugin.buildnumber} - - - org.codehaus.mojo - exec-maven-plugin - ${version.plugin.exec} - - - org.jacoco - jacoco-maven-plugin - ${version.plugin.jacoco} - - - - - - com.github.ekryd.sortpom - sortpom-maven-plugin - - false - false - true - scope,groupId,artifactId - groupId,artifactId - true - true - groupId,artifactId - true - stop - strict - - - - - verify - - validate - - - - - com.github.siom79.japicmp - japicmp-maven-plugin - - - true - true - \d+\.\d+\.\d+ - - com.snowflake - net.snowflake - - - ${shadeBase} - - - - - - japicmp - - cmp - - verify - - - - - org.apache.maven.plugins - maven-clean-plugin - - - - - lib - - *.jar - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - true - true - javac - true - 8 - 8 - - -Xlint:all,-path - - - - - default-testCompile - - testCompile - - test-compile - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - analyze - - analyze-only - - - true - true - - javax.servlet:javax.servlet-api - - - - - install-jar - - copy - - install - - - - ${project.groupId} - ${project.artifactId} - ${project.version} - - - lib - - - - - - org.apache.maven.plugins - maven-enforcer-plugin - - - com.google.cloud.tools - linkage-checker-enforcer-rules - 1.5.13 - - - org.codehaus.mojo - extra-enforcer-rules - 1.3 - - - org.eclipse.aether - aether-util - - - - - - - enforce-best-practices - - enforce - - - - - true - true - - - arrow-memory-unsafe - - org.apache.arrow.memory.DefaultAllocationManagerFactory - - - - - - - - - - - - - enforce-maven - - enforce - - - - - ${version.maven} - - - - - - enforce-linkage-checker - - enforce - - verify - - - - true - linkage-checker-exclusion-rules.xml - - - - - - - - org.apache.maven.plugins - maven-install-plugin - 3.0.0-M1 - - - install-arrow-format - - install-file - - validate - - ${project.basedir}/dependencies/arrow-format-${arrow.version}.jar - org.apache.arrow - arrow-format - ${arrow.version} - jar - true - - - - install-arrow-memory-core - - install-file - - validate - - ${project.basedir}/dependencies/arrow-memory-core-${arrow.version}.jar - org.apache.arrow - arrow-memory-core - ${arrow.version} - jar - true - - - - install-arrow-memory-netty - - install-file - - validate - - ${project.basedir}/dependencies/arrow-memory-netty-${arrow.version}.jar - org.apache.arrow - arrow-memory-netty - ${arrow.version} - jar - true - - - - install-arrow-memory-pom - - install-file - - validate - - ${project.basedir}/dependencies/arrow-memory-${arrow.version}.pom - org.apache.arrow - arrow-memory - ${arrow.version} - pom - true - - - - - install-arrow-memory-unsafe - - install-file - - validate - - ${project.basedir}/dependencies/arrow-memory-unsafe-${arrow.version}.jar - org.apache.arrow - arrow-memory-unsafe - ${arrow.version} - jar - true - - - - - install-arrow-vector - - install-file - - validate - - ${project.basedir}/dependencies/arrow-vector-${arrow.version}.jar - org.apache.arrow - arrow-vector - ${arrow.version} - jar - true - - - - install-tika-core - - install-file - - validate - - ${project.basedir}/dependencies/tika-core-${tika.version}.jar - org.apache.tika - tika-core - ${tika.version} - jar - true - - - - - - org.apache.maven.plugins - maven-jar-plugin - - - - net.snowflake.client.jdbc.SnowflakeDriver - true - - - - - - - test-jar - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - 8 - ${project.basedir}/src/main/javadoc/overview.html - java - ${project.basedir}/src/main/javadoc/licenses.html - - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-source-plugin - - - attach-sources - - jar - - - - - - org.jacoco - jacoco-maven-plugin - - ${jacoco.skip.instrument} - - - - pre-unit-test - - prepare-agent - - - target/jacoco-ut.exec - - - - post-unit-test - - report - - test - - target/jacoco-ut.exec - target/jacoco-ut - - - - - - - - - - - org.apache.maven.plugins - maven-project-info-reports-plugin - - - - - - - check-style - - - - com.spotify.fmt - fmt-maven-plugin - - - - check - - - - - - - - - self-contained-jar - - - !not-self-contained-jar - - - - - - - - org.apache.maven.plugins - maven-antrun-plugin - - - repack - - run - - package - - - - - - - - - - - - - - - - - - org.apache.maven.plugins - maven-enforcer-plugin - - - enforce-linkage-checker - - enforce - - none - - - - - org.apache.maven.plugins - maven-shade-plugin - - - - - shade - - package - - - - net.snowflake.common - ${shadeBase}.snowflake.common - - - org.apache - ${shadeBase}.apache - - org.apache.log4j.* - - - - com.amazonaws - ${shadeBase}.amazonaws - - - software.amazon.ion - ${shadeBase}.software.amazon.ion - - - com.microsoft.azure - ${shadeBase}.microsoft.azure - - - com.fasterxml - ${shadeBase}.fasterxml - - - com.google - ${shadeBase}.google - - - google.geo - ${shadeBase}.google.geo - - - google.storage - ${shadeBase}.google.storage - - - org.joda - ${shadeBase}.joda - - - com.yammer - ${shadeBase}.yammer - - - javax.servlet - ${shadeBase}.javax.servlet - - - org.jsoup - ${shadeBase}.org.jsoup - - - org.bouncycastle - ${shadeBase}.org.bouncycastle - - - com.nimbusds - ${shadeBase}.com.nimbusds - - - javax.annotation - ${shadeBase}.javax.annotation - - - net.jcip - ${shadeBase}.net.jcip - - - net.minidev - ${shadeBase}.net.minidev - - - org.objectweb - ${shadeBase}.org.objectweb - - - com.sun - ${shadeBase}.com.sun - - com.sun.jna.** - - - - io.netty - ${shadeBase}.io.netty - - - com.carrotsearch - ${shadeBase}.com.carrotsearch - - - google.type - ${shadeBase}.google.type - - - google.rpc - ${shadeBase}.google.rpc - - - google.iam - ${shadeBase}.google.iam - - - io.opencensus - ${shadeBase}.opencensus - - - org.threeten - ${shadeBase}.threeten - - - google.protobuf - ${shadeBase}.google.protobuf - - - google.api - ${shadeBase}.google.api - - - io.grpc - ${shadeBase}.grpc - - - google.longrunning - ${shadeBase}.google.longrunning - - - google.cloud - ${shadeBase}.google.cloud - - - google.logging - ${shadeBase}.google.logging - - - org.checkerframework - ${shadeBase}.org.checkerframework - - - org.codehaus - ${shadeBase}.org.codehaus - - - io.perfmark - ${shadeBase}.io.perfmark - - - org.conscrypt - ${shadeBase}.org.conscrypt - - - opencensus - ${shadeBase}.opencensus - - - grpc - ${shadeBase}.grpc - - - android.annotation - ${shadeBase}.android.annotation - - - - - *:* - - META-INF/LICENSE* - META-INF/NOTICE* - META-INF/DEPENDENCIES - META-INF/maven/** - META-INF/services/com.fasterxml.* - META-INF/*.xml - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - .netbeans_automatic_build - git.properties - arrow-git.properties - google-http-client.properties - storage.v1.json - - pipes-fork-server-default-log4j2.xml - dependencies.properties - pipes-fork-server-default-log4j2.xml - - - - org.apache.arrow:arrow-vector - - - codegen/** - - - - com.google.guava:guava - - com/google/common/io/** - com/google/common/base/** - com/google/common/hash/** - com/google/common/collect/** - com/google/common/graph/** - com/google/common/math/** - com/google/common/util/concurrent/** - - - - commons-logging:commons-logging - - org/apache/commons/logging/impl/AvalonLogger.class - - - - - - - - - - - - - org.codehaus.mojo - buildnumber-maven-plugin - - yyyyMMddHHmmss - buildNumber.timestamp - false - - false - - - - - - create-timestamp - - package - - - - - org.codehaus.mojo - exec-maven-plugin - - - check-shaded-content - - exec - - verify - - ${basedir}/ci/scripts/check_content.sh - - - - - - - - - java-9 - - (9,) - - - - - maven-failsafe-plugin - - --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED - - - - maven-surefire-plugin - - --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED - - - - - - - jenkinsIT - - - jenkinsIT - - - - - - - com.github.siom79.japicmp - japicmp-maven-plugin - - - japicmp - - cmp - - none - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - analyze - - analyze-only - - none - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - ${testCategory} - - - - - verify - - - - DefaultIT - - integration-test - - - - **/DellBoomiCloudIT.java - - - net.snowflake.client.log.JDK14Logger - ${basedir}/src/test/resources/logging.properties - - - - - - - org.jacoco - jacoco-maven-plugin - - ${jacoco.skip.instrument} - - - - pre-integration-test - - prepare-agent - - pre-integration-test - - target/jacoco-it.exec - - - - post-integration-test - - report - - post-integration-test - - target/jacoco-it.exec - target/jacoco-it - - - - - - - - - qa1IT - - - qa1IT - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - verify - - - - ClientTelemetryIT - - integration-test - - - - **/ConnectionIT.java - **/SFTrustManagerIT.java - - - ${basedir}/src/test/resources/logback-test.xml - - - - - - - - - - DellBoomi - - - dellBoomiIT - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - verify - - - - DellBoomiIT - - integration-test - - - DellBoomiCloudIT.java - - - - - - - - - preprod3IT - - - preprod3IT - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - verify - - - - ClientTelemetryIT - - integration-test - - - - **/ConnectionIT.java - **/SFTrustManagerIT.java - - - ${basedir}/src/test/resources/logback-test.xml - - - - - - - - - - travisIT - - - travisIT - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - ${testCategory} - - - - - integration-test - - - - **/DellBoomiCloudIT.java - - - ${basedir}/src/test/resources/logback-test.xml - true - - - - - verify_travis_it - - verify - - verify - - - - - org.jacoco - jacoco-maven-plugin - - ${jacoco.skip.instrument} - - - - pre-integration-test - - prepare-agent - - pre-integration-test - - target/jacoco-it.exec - - - - post-integration-test - - report - - post-integration-test - - target/jacoco-it.exec - target/jacoco-it - - - - - - - - - ossrh-deploy - - - ossrhDeploy - - - - - - maven-deploy-plugin - - true - - - - org.apache.maven.plugins - maven-gpg-plugin - - - - sign-and-deploy-file - - deploy - - target/${project.artifactId}.jar - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2 - generated_public_pom.xml - target/${project.artifactId}-javadoc.jar - target/${project.artifactId}-sources.jar - ${env.GPG_KEY_ID} - ${env.GPG_KEY_PASSPHRASE} - - - - - - - - - \ No newline at end of file + + + 4.0.0 + + + net.snowflake + snowflake-jdbc-parent + 3.14.3 + ./parent-pom.xml + + + snowflake-jdbc + jar + + snowflake-jdbc + https://github.com/snowflakedb/snowflake-jdbc + + + scm:git:https://github.com/snowflakedb/snowflake-jdbc.git + https://github.com/snowflakedb/snowflake-jdbc + + + + + org.bouncycastle + bcpkix-jdk18on + + + org.bouncycastle + bcprov-jdk18on + + + + + ${project.artifactId} + + + true + src/main/resources + + + + + + com.github.ekryd.sortpom + sortpom-maven-plugin + ${version.plugin.sortpom} + + + com.github.siom79.japicmp + japicmp-maven-plugin + ${version.plugin.japicmp} + + + com.spotify.fmt + fmt-maven-plugin + ${version.plugin.fmt} + + + org.apache.maven.plugins + maven-antrun-plugin + ${version.plugin.antrun} + + + org.apache.maven.plugins + maven-clean-plugin + ${version.plugin.clean} + + + org.apache.maven.plugins + maven-compiler-plugin + ${version.plugin.compiler} + + + org.apache.maven.plugins + maven-dependency-plugin + ${version.plugin.dependency} + + + org.apache.maven.plugins + maven-deploy-plugin + ${version.plugin.deploy} + + + org.apache.maven.plugins + maven-enforcer-plugin + ${version.plugin.enforcer} + + + org.apache.maven.plugins + maven-failsafe-plugin + ${version.plugin.failsafe} + + + org.apache.maven.plugins + maven-gpg-plugin + ${version.plugin.gpg} + + + org.apache.maven.plugins + maven-install-plugin + ${version.plugin.install} + + + org.apache.maven.plugins + maven-jar-plugin + ${version.plugin.jar} + + + org.apache.maven.plugins + maven-javadoc-plugin + ${version.plugin.javadoc} + + + org.apache.maven.plugins + maven-project-info-reports-plugin + ${version.plugin.projectinforeports} + + + org.apache.maven.plugins + maven-shade-plugin + ${version.plugin.shade} + + + org.apache.maven.plugins + maven-source-plugin + ${version.plugin.source} + + + org.apache.maven.plugins + maven-surefire-plugin + ${version.plugin.surefire} + + + org.codehaus.mojo + buildnumber-maven-plugin + ${version.plugin.buildnumber} + + + org.codehaus.mojo + exec-maven-plugin + ${version.plugin.exec} + + + org.jacoco + jacoco-maven-plugin + ${version.plugin.jacoco} + + + + + + com.github.ekryd.sortpom + sortpom-maven-plugin + + false + false + true + scope,groupId,artifactId + groupId,artifactId + true + true + groupId,artifactId + true + stop + strict + + + + + verify + + validate + + + + + com.github.siom79.japicmp + japicmp-maven-plugin + + + true + true + \d+\.\d+\.\d+ + + com.snowflake + net.snowflake + + + ${shadeBase} + + + + + + japicmp + + cmp + + verify + + + + + org.apache.maven.plugins + maven-clean-plugin + + + + + lib + + *.jar + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + true + javac + true + 8 + 8 + + -Xlint:all,-path + + + + + default-testCompile + + testCompile + + test-compile + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + analyze + + analyze-only + + + true + true + + javax.servlet:javax.servlet-api + + + + + install-jar + + copy + + install + + + + ${project.groupId} + ${project.artifactId} + ${project.version} + + + lib + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + com.google.cloud.tools + linkage-checker-enforcer-rules + 1.5.13 + + + org.codehaus.mojo + extra-enforcer-rules + 1.3 + + + org.eclipse.aether + aether-util + + + + + + + enforce-best-practices + + enforce + + + + + true + true + + + arrow-memory-unsafe + + org.apache.arrow.memory.DefaultAllocationManagerFactory + + + + + + + + + + + + + enforce-maven + + enforce + + + + + ${version.maven} + + + + + + enforce-linkage-checker + + enforce + + verify + + + + true + linkage-checker-exclusion-rules.xml + + + + + + + + org.apache.maven.plugins + maven-install-plugin + + + install-arrow-format + + install-file + + validate + + ${project.basedir}/dependencies/arrow-format-${arrow.version}.jar + org.apache.arrow + arrow-format + ${arrow.version} + jar + true + + + + install-arrow-memory-core + + install-file + + validate + + ${project.basedir}/dependencies/arrow-memory-core-${arrow.version}.jar + org.apache.arrow + arrow-memory-core + ${arrow.version} + jar + true + + + + install-arrow-memory-netty + + install-file + + validate + + ${project.basedir}/dependencies/arrow-memory-netty-${arrow.version}.jar + org.apache.arrow + arrow-memory-netty + ${arrow.version} + jar + true + + + + install-arrow-memory-pom + + install-file + + validate + + ${project.basedir}/dependencies/arrow-memory-${arrow.version}.pom + org.apache.arrow + arrow-memory + ${arrow.version} + pom + true + + + + + install-arrow-memory-unsafe + + install-file + + validate + + ${project.basedir}/dependencies/arrow-memory-unsafe-${arrow.version}.jar + org.apache.arrow + arrow-memory-unsafe + ${arrow.version} + jar + true + + + + + install-arrow-vector + + install-file + + validate + + ${project.basedir}/dependencies/arrow-vector-${arrow.version}.jar + org.apache.arrow + arrow-vector + ${arrow.version} + jar + true + + + + install-tika-core + + install-file + + validate + + ${project.basedir}/dependencies/tika-core-${tika.version}.jar + org.apache.tika + tika-core + ${tika.version} + jar + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + net.snowflake.client.jdbc.SnowflakeDriver + true + + + + + + + test-jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + 8 + ${project.basedir}/src/main/javadoc/overview.html + java + ${project.basedir}/src/main/javadoc/licenses.html + + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + org.jacoco + jacoco-maven-plugin + + ${jacoco.skip.instrument} + + + + pre-unit-test + + prepare-agent + + + target/jacoco-ut.exec + + + + post-unit-test + + report + + test + + target/jacoco-ut.exec + target/jacoco-ut + + + + + + + + + + + org.apache.maven.plugins + maven-project-info-reports-plugin + + + + + + + check-style + + + + com.spotify.fmt + fmt-maven-plugin + + + + check + + + + + + + + + self-contained-jar + + + !not-self-contained-jar + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + repack + + run + + package + + + + + + + + + + + + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce-linkage-checker + + enforce + + none + + + + + org.apache.maven.plugins + maven-shade-plugin + + + + + shade + + package + + + + net.snowflake.common + ${shadeBase}.snowflake.common + + + org.apache + ${shadeBase}.apache + + org.apache.log4j.* + + + + com.amazonaws + ${shadeBase}.amazonaws + + + software.amazon.ion + ${shadeBase}.software.amazon.ion + + + com.microsoft.azure + ${shadeBase}.microsoft.azure + + + com.fasterxml + ${shadeBase}.fasterxml + + + com.google + ${shadeBase}.google + + + google.geo + ${shadeBase}.google.geo + + + google.storage + ${shadeBase}.google.storage + + + org.joda + ${shadeBase}.joda + + + com.yammer + ${shadeBase}.yammer + + + javax.servlet + ${shadeBase}.javax.servlet + + + org.jsoup + ${shadeBase}.org.jsoup + + + org.bouncycastle + ${shadeBase}.org.bouncycastle + + + com.nimbusds + ${shadeBase}.com.nimbusds + + + javax.annotation + ${shadeBase}.javax.annotation + + + net.jcip + ${shadeBase}.net.jcip + + + net.minidev + ${shadeBase}.net.minidev + + + org.objectweb + ${shadeBase}.org.objectweb + + + com.sun + ${shadeBase}.com.sun + + com.sun.jna.** + + + + io.netty + ${shadeBase}.io.netty + + + com.carrotsearch + ${shadeBase}.com.carrotsearch + + + google.type + ${shadeBase}.google.type + + + google.rpc + ${shadeBase}.google.rpc + + + google.iam + ${shadeBase}.google.iam + + + io.opencensus + ${shadeBase}.opencensus + + + org.threeten + ${shadeBase}.threeten + + + google.protobuf + ${shadeBase}.google.protobuf + + + google.api + ${shadeBase}.google.api + + + io.grpc + ${shadeBase}.grpc + + + google.longrunning + ${shadeBase}.google.longrunning + + + google.cloud + ${shadeBase}.google.cloud + + + google.logging + ${shadeBase}.google.logging + + + org.checkerframework + ${shadeBase}.org.checkerframework + + + org.codehaus + ${shadeBase}.org.codehaus + + + io.perfmark + ${shadeBase}.io.perfmark + + + org.conscrypt + ${shadeBase}.org.conscrypt + + + opencensus + ${shadeBase}.opencensus + + + grpc + ${shadeBase}.grpc + + + android.annotation + ${shadeBase}.android.annotation + + + + + *:* + + META-INF/LICENSE* + META-INF/NOTICE* + META-INF/DEPENDENCIES + META-INF/maven/** + META-INF/services/com.fasterxml.* + META-INF/*.xml + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + .netbeans_automatic_build + git.properties + arrow-git.properties + google-http-client.properties + storage.v1.json + + pipes-fork-server-default-log4j2.xml + dependencies.properties + pipes-fork-server-default-log4j2.xml + + + + org.apache.arrow:arrow-vector + + + codegen/** + + + + com.google.guava:guava + + com/google/common/io/** + com/google/common/base/** + com/google/common/hash/** + com/google/common/collect/** + com/google/common/graph/** + com/google/common/math/** + com/google/common/util/concurrent/** + + + + commons-logging:commons-logging + + org/apache/commons/logging/impl/AvalonLogger.class + + + + + + + + + + + + + org.codehaus.mojo + buildnumber-maven-plugin + + yyyyMMddHHmmss + buildNumber.timestamp + false + + false + + + + + + create-timestamp + + package + + + + + org.codehaus.mojo + exec-maven-plugin + + + check-shaded-content + + exec + + verify + + ${basedir}/ci/scripts/check_content.sh + + + + + + + + + java-9 + + (9,) + + + + + maven-failsafe-plugin + + --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED + + + + maven-surefire-plugin + + --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED + + + + + + + jenkinsIT + + + jenkinsIT + + + + + + + com.github.siom79.japicmp + japicmp-maven-plugin + + + japicmp + + cmp + + none + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + analyze + + analyze-only + + none + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + ${testCategory} + + + + + verify + + + + DefaultIT + + integration-test + + + + **/DellBoomiCloudIT.java + + + net.snowflake.client.log.JDK14Logger + ${basedir}/src/test/resources/logging.properties + + + + + + + org.jacoco + jacoco-maven-plugin + + ${jacoco.skip.instrument} + + + + pre-integration-test + + prepare-agent + + pre-integration-test + + target/jacoco-it.exec + + + + post-integration-test + + report + + post-integration-test + + target/jacoco-it.exec + target/jacoco-it + + + + + + + + + qa1IT + + + qa1IT + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + verify + + + + ClientTelemetryIT + + integration-test + + + + **/ConnectionIT.java + **/SFTrustManagerIT.java + + + ${basedir}/src/test/resources/logback-test.xml + + + + + + + + + + DellBoomi + + + dellBoomiIT + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + verify + + + + DellBoomiIT + + integration-test + + + DellBoomiCloudIT.java + + + + + + + + + preprod3IT + + + preprod3IT + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + verify + + + + ClientTelemetryIT + + integration-test + + + + **/ConnectionIT.java + **/SFTrustManagerIT.java + + + ${basedir}/src/test/resources/logback-test.xml + + + + + + + + + + travisIT + + + travisIT + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + ${testCategory} + + + + + integration-test + + + + **/DellBoomiCloudIT.java + + + ${basedir}/src/test/resources/logback-test.xml + true + + + + + verify_travis_it + + verify + + verify + + + + + org.jacoco + jacoco-maven-plugin + + ${jacoco.skip.instrument} + + + + pre-integration-test + + prepare-agent + + pre-integration-test + + target/jacoco-it.exec + + + + post-integration-test + + report + + post-integration-test + + target/jacoco-it.exec + target/jacoco-it + + + + + + + + + ossrh-deploy + + + ossrhDeploy + + + + + + maven-deploy-plugin + + true + + + + org.apache.maven.plugins + maven-gpg-plugin + + + + sign-and-deploy-file + + deploy + + target/${project.artifactId}.jar + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2 + generated_public_pom.xml + target/${project.artifactId}-javadoc.jar + target/${project.artifactId}-sources.jar + ${env.GPG_KEY_ID} + ${env.GPG_KEY_PASSPHRASE} + + + + + + + + + From 241d393d54d4fcd7490b0043d75611c9d2ff691f Mon Sep 17 00:00:00 2001 From: Dominik Przybysz <132913826+sfc-gh-dprzybysz@users.noreply.github.com> Date: Wed, 6 Dec 2023 17:44:26 +0100 Subject: [PATCH 08/17] SNOW-985489: Bump patch 3.14.3 to 3.14.4 (#1580) SNOW-985489: Bump patch 3.13.3 to 3.14.4 --- CHANGELOG.rst | 4 ++++ FIPS/pom.xml | 3 ++- parent-pom.xml | 2 +- pom.xml | 3 ++- src/main/java/net/snowflake/client/jdbc/SnowflakeDriver.java | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cbc211f7d..0b605e114 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,7 @@ +**JDBC Driver 3.14.4** + +- \||Please Refer to Release Notes at https://community.snowflake.com/s/article/JDBC-Driver-Release-Notes + **JDBC Driver 3.14.3** - \||Please Refer to Release Notes at https://community.snowflake.com/s/article/JDBC-Driver-Release-Notes diff --git a/FIPS/pom.xml b/FIPS/pom.xml index 533da6be1..df11ca3d7 100644 --- a/FIPS/pom.xml +++ b/FIPS/pom.xml @@ -5,11 +5,12 @@ net.snowflake snowflake-jdbc-parent - 3.14.3 + 3.14.4 ../parent-pom.xml snowflake-jdbc-fips + 3.14.4 jar snowflake-jdbc-fips diff --git a/parent-pom.xml b/parent-pom.xml index 4b7385c81..c5b59f475 100644 --- a/parent-pom.xml +++ b/parent-pom.xml @@ -5,7 +5,7 @@ net.snowflake snowflake-jdbc-parent - 3.14.3 + 3.14.4 pom diff --git a/pom.xml b/pom.xml index a7748dcf6..e58306a62 100644 --- a/pom.xml +++ b/pom.xml @@ -6,11 +6,12 @@ net.snowflake snowflake-jdbc-parent - 3.14.3 + 3.14.4 ./parent-pom.xml snowflake-jdbc + 3.14.4 jar snowflake-jdbc diff --git a/src/main/java/net/snowflake/client/jdbc/SnowflakeDriver.java b/src/main/java/net/snowflake/client/jdbc/SnowflakeDriver.java index a56dde291..13e6a3c6a 100644 --- a/src/main/java/net/snowflake/client/jdbc/SnowflakeDriver.java +++ b/src/main/java/net/snowflake/client/jdbc/SnowflakeDriver.java @@ -23,7 +23,7 @@ public class SnowflakeDriver implements Driver { static SnowflakeDriver INSTANCE; public static final Properties EMPTY_PROPERTIES = new Properties(); - public static String implementVersion = "3.14.3"; + public static String implementVersion = "3.14.4"; static int majorVersion = 0; static int minorVersion = 0; From 742c2f1ad3b3ac03e4eecd5a3b7954032f8ba0f9 Mon Sep 17 00:00:00 2001 From: ilesh garish <111810784+sfc-gh-igarish@users.noreply.github.com> Date: Thu, 7 Dec 2023 14:51:20 -0800 Subject: [PATCH 09/17] SNOW-877275 : Build the console login url (#1518) * Build the console login url * Added URL builder instead of building URL string manually * Fix formatting issue * Added proof key and rename port parameter name * Pass proofkey for second call to GS * Fix coding style issue --- .../snowflake/client/core/SFLoginInput.java | 11 ++++ .../net/snowflake/client/core/SFSession.java | 7 ++- .../client/core/SFSessionProperty.java | 1 + .../snowflake/client/core/SessionUtil.java | 1 + .../core/SessionUtilExternalBrowser.java | 56 ++++++++++++++++--- .../core/SessionUtilExternalBrowserTest.java | 1 + .../client/jdbc/SSOConnectionTest.java | 1 + 7 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/main/java/net/snowflake/client/core/SFLoginInput.java b/src/main/java/net/snowflake/client/core/SFLoginInput.java index 67a4b3aca..927638f99 100644 --- a/src/main/java/net/snowflake/client/core/SFLoginInput.java +++ b/src/main/java/net/snowflake/client/core/SFLoginInput.java @@ -50,6 +50,8 @@ public class SFLoginInput { private String privateKeyFilePwd; private String inFlightCtx; // Opaque string sent for Snowsight account activation + private boolean disableConsoleLogin = true; + // Additional headers to add for Snowsight. Map additionalHttpHeadersForSnowsight; @@ -64,6 +66,15 @@ SFLoginInput setServerUrl(String serverUrl) { return this; } + public boolean getDisableConsoleLogin() { + return disableConsoleLogin; + } + + SFLoginInput setDisableConsoleLogin(boolean disableConsoleLogin) { + this.disableConsoleLogin = disableConsoleLogin; + return this; + } + String getDatabaseName() { return databaseName; } diff --git a/src/main/java/net/snowflake/client/core/SFSession.java b/src/main/java/net/snowflake/client/core/SFSession.java index 7a48f1f11..9c10ebf1d 100644 --- a/src/main/java/net/snowflake/client/core/SFSession.java +++ b/src/main/java/net/snowflake/client/core/SFSession.java @@ -507,7 +507,12 @@ public synchronized void open() throws SFException, SnowflakeSQLException { .setApplication((String) connectionPropertiesMap.get(SFSessionProperty.APPLICATION)) .setServiceName(getServiceName()) .setOCSPMode(getOCSPMode()) - .setHttpClientSettingsKey(httpClientSettingsKey); + .setHttpClientSettingsKey(httpClientSettingsKey) + .setDisableConsoleLogin( + connectionPropertiesMap.get(SFSessionProperty.DISABLE_CONSOLE_LOGIN) != null + ? getBooleanValue( + connectionPropertiesMap.get(SFSessionProperty.DISABLE_CONSOLE_LOGIN)) + : true); // Enable or disable OOB telemetry based on connection parameter. Default is disabled. // The value may still change later when session parameters from the server are read. diff --git a/src/main/java/net/snowflake/client/core/SFSessionProperty.java b/src/main/java/net/snowflake/client/core/SFSessionProperty.java index 97a921c01..2a7687aa7 100644 --- a/src/main/java/net/snowflake/client/core/SFSessionProperty.java +++ b/src/main/java/net/snowflake/client/core/SFSessionProperty.java @@ -72,6 +72,7 @@ public enum SFSessionProperty { MAX_HTTP_RETRIES("maxHttpRetries", false, Integer.class), ENABLE_PUT_GET("enablePutGet", false, Boolean.class), + DISABLE_CONSOLE_LOGIN("disableConsoleLogin", false, Boolean.class), PUT_GET_MAX_RETRIES("putGetMaxRetries", false, Integer.class), diff --git a/src/main/java/net/snowflake/client/core/SessionUtil.java b/src/main/java/net/snowflake/client/core/SessionUtil.java index c2d1acc5d..3c416a8d4 100644 --- a/src/main/java/net/snowflake/client/core/SessionUtil.java +++ b/src/main/java/net/snowflake/client/core/SessionUtil.java @@ -55,6 +55,7 @@ public class SessionUtil { private static final String SF_PATH_LOGIN_REQUEST = "/session/v1/login-request"; private static final String SF_PATH_TOKEN_REQUEST = "/session/token-request"; public static final String SF_PATH_AUTHENTICATOR_REQUEST = "/session/authenticator-request"; + public static final String SF_PATH_CONSOLE_LOGIN_REQUEST = "/console/login"; public static final String SF_QUERY_SESSION_DELETE = "delete"; diff --git a/src/main/java/net/snowflake/client/core/SessionUtilExternalBrowser.java b/src/main/java/net/snowflake/client/core/SessionUtilExternalBrowser.java index b18d1ebcb..f322e3b98 100644 --- a/src/main/java/net/snowflake/client/core/SessionUtilExternalBrowser.java +++ b/src/main/java/net/snowflake/client/core/SessionUtilExternalBrowser.java @@ -13,6 +13,7 @@ import java.net.*; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; import java.text.SimpleDateFormat; import java.util.*; import net.snowflake.client.jdbc.ErrorCode; @@ -215,6 +216,34 @@ private String getSSOUrl(int port) throws SFException, SnowflakeSQLException { } } + private String getConsoleLoginUrl(int port) throws SFException { + try { + proofKey = generateProofKey(); + String serverUrl = loginInput.getServerUrl(); + + URIBuilder consoleLoginUriBuilder = new URIBuilder(serverUrl); + consoleLoginUriBuilder.setPath(SessionUtil.SF_PATH_CONSOLE_LOGIN_REQUEST); + consoleLoginUriBuilder.addParameter("login_name", loginInput.getUserName()); + consoleLoginUriBuilder.addParameter("browser_mode_redirect_port", Integer.toString(port)); + consoleLoginUriBuilder.addParameter("proof_key", proofKey); + + String consoleLoginUrl = consoleLoginUriBuilder.build().toURL().toString(); + + logger.debug("console login url: {}", consoleLoginUrl); + + return consoleLoginUrl; + } catch (Exception ex) { + throw new SFException(ex, ErrorCode.INTERNAL_ERROR, ex.getMessage()); + } + } + + private String generateProofKey() { + SecureRandom secureRandom = new SecureRandom(); + byte[] randomness = new byte[32]; + secureRandom.nextBytes(randomness); + return Base64.getEncoder().encodeToString(randomness); + } + /** * Authenticate * @@ -227,13 +256,26 @@ void authenticate() throws SFException, SnowflakeSQLException { // main procedure int port = this.getLocalPort(ssocket); logger.debug("Listening localhost:{}", port); - String ssoUrl = getSSOUrl(port); - this.handlers.output( - "Initiating login request with your identity provider. A " - + "browser window should have opened for you to complete the " - + "login. If you can't see it, check existing browser windows, " - + "or your OS settings. Press CTRL+C to abort and try again..."); - this.handlers.openBrowser(ssoUrl); + + if (loginInput.getDisableConsoleLogin()) { + // Access GS to get SSO URL + String ssoUrl = getSSOUrl(port); + this.handlers.output( + "Initiating login request with your identity provider. A " + + "browser window should have opened for you to complete the " + + "login. If you can't see it, check existing browser windows, " + + "or your OS settings. Press CTRL+C to abort and try again..."); + this.handlers.openBrowser(ssoUrl); + } else { + // Multiple SAML way to do authentication via console login + String consoleLoginUrl = getConsoleLoginUrl(port); + this.handlers.output( + "Initiating login request with your identity provider(s). A " + + "browser window should have opened for you to complete the " + + "login. If you can't see it, check existing browser windows, " + + "or your OS settings. Press CTRL+C to abort and try again..."); + this.handlers.openBrowser(consoleLoginUrl); + } while (true) { Socket socket = ssocket.accept(); // start accepting the request diff --git a/src/test/java/net/snowflake/client/core/SessionUtilExternalBrowserTest.java b/src/test/java/net/snowflake/client/core/SessionUtilExternalBrowserTest.java index 6b0d15c24..0092b925f 100644 --- a/src/test/java/net/snowflake/client/core/SessionUtilExternalBrowserTest.java +++ b/src/test/java/net/snowflake/client/core/SessionUtilExternalBrowserTest.java @@ -229,6 +229,7 @@ private SFLoginInput initMockLoginInput() { .thenReturn(ClientAuthnDTO.AuthenticatorType.EXTERNALBROWSER.name()); when(loginInput.getAccountName()).thenReturn("testaccount"); when(loginInput.getUserName()).thenReturn("testuser"); + when(loginInput.getDisableConsoleLogin()).thenReturn(true); return loginInput; } } diff --git a/src/test/java/net/snowflake/client/jdbc/SSOConnectionTest.java b/src/test/java/net/snowflake/client/jdbc/SSOConnectionTest.java index 22e846e5b..6a945fcc9 100644 --- a/src/test/java/net/snowflake/client/jdbc/SSOConnectionTest.java +++ b/src/test/java/net/snowflake/client/jdbc/SSOConnectionTest.java @@ -288,6 +288,7 @@ private SFLoginInput initMockLoginInput() { .thenReturn(ClientAuthnDTO.AuthenticatorType.EXTERNALBROWSER.name()); when(loginInput.getAccountName()).thenReturn("testaccount"); when(loginInput.getUserName()).thenReturn("testuser"); + when(loginInput.getDisableConsoleLogin()).thenReturn(true); return loginInput; } From a0b88eea5a6361793fa82c14b4c8099305721b8c Mon Sep 17 00:00:00 2001 From: Dominik Przybysz <132913826+sfc-gh-dprzybysz@users.noreply.github.com> Date: Fri, 8 Dec 2023 07:07:05 +0100 Subject: [PATCH 10/17] SNOW-909180: Set query id for failed statements (#1576) --- .../client/jdbc/QueryIdValidator.java | 12 +++ .../client/jdbc/SFAsyncResultSet.java | 3 +- .../client/jdbc/SnowflakeStatement.java | 3 +- .../client/jdbc/SnowflakeStatementV1.java | 22 +++++- .../java/net/snowflake/client/TestUtil.java | 13 ++++ .../client/jdbc/ConnectionLatestIT.java | 73 +++++++++++++++---- .../client/jdbc/ResultSetAsyncIT.java | 12 +-- .../client/jdbc/ResultSetLatestIT.java | 16 +--- .../snowflake/client/jdbc/StatementIT.java | 9 +++ .../client/jdbc/StatementLatestIT.java | 52 +++++++++++++ 10 files changed, 175 insertions(+), 40 deletions(-) create mode 100644 src/main/java/net/snowflake/client/jdbc/QueryIdValidator.java diff --git a/src/main/java/net/snowflake/client/jdbc/QueryIdValidator.java b/src/main/java/net/snowflake/client/jdbc/QueryIdValidator.java new file mode 100644 index 000000000..d657edd12 --- /dev/null +++ b/src/main/java/net/snowflake/client/jdbc/QueryIdValidator.java @@ -0,0 +1,12 @@ +package net.snowflake.client.jdbc; + +import java.util.regex.Pattern; + +class QueryIdValidator { + private static final Pattern QUERY_ID_REGEX = + Pattern.compile("[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}"); + + static boolean isValid(String queryId) { + return queryId != null && QUERY_ID_REGEX.matcher(queryId).matches(); + } +} diff --git a/src/main/java/net/snowflake/client/jdbc/SFAsyncResultSet.java b/src/main/java/net/snowflake/client/jdbc/SFAsyncResultSet.java index 77662cbd8..a7a94090c 100644 --- a/src/main/java/net/snowflake/client/jdbc/SFAsyncResultSet.java +++ b/src/main/java/net/snowflake/client/jdbc/SFAsyncResultSet.java @@ -11,7 +11,6 @@ import java.sql.*; import java.util.List; import java.util.TimeZone; -import java.util.regex.Pattern; import net.snowflake.client.core.QueryStatus; import net.snowflake.client.core.SFBaseResultSet; import net.snowflake.client.core.SFBaseSession; @@ -75,7 +74,7 @@ public SFAsyncResultSet(String queryID, Statement statement) throws SQLException super(statement); this.sfBaseResultSet = null; queryID.trim(); - if (!Pattern.matches("[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}", queryID)) { + if (!QueryIdValidator.isValid(queryID)) { throw new SQLException( "The provided query ID " + queryID + " is invalid.", SqlState.INVALID_PARAMETER_VALUE); } diff --git a/src/main/java/net/snowflake/client/jdbc/SnowflakeStatement.java b/src/main/java/net/snowflake/client/jdbc/SnowflakeStatement.java index 5751aaea8..f1f41d4d0 100644 --- a/src/main/java/net/snowflake/client/jdbc/SnowflakeStatement.java +++ b/src/main/java/net/snowflake/client/jdbc/SnowflakeStatement.java @@ -12,7 +12,8 @@ /** This interface defines Snowflake specific APIs for Statement */ public interface SnowflakeStatement { /** - * @return the Snowflake query ID of the latest executed query + * @return the Snowflake query ID of the latest executed query (even failed one) or null when the + * last query ID is not available */ String getQueryID() throws SQLException; diff --git a/src/main/java/net/snowflake/client/jdbc/SnowflakeStatementV1.java b/src/main/java/net/snowflake/client/jdbc/SnowflakeStatementV1.java index 7099a7994..b4f67bcf2 100644 --- a/src/main/java/net/snowflake/client/jdbc/SnowflakeStatementV1.java +++ b/src/main/java/net/snowflake/client/jdbc/SnowflakeStatementV1.java @@ -207,7 +207,6 @@ long executeUpdateInternal( ErrorCode.UNSUPPORTED_STATEMENT_TYPE_IN_EXECUTION_API, StmtUtil.truncateSQL(sql)); } - SFBaseResultSet sfResultSet; try { sfResultSet = @@ -217,7 +216,11 @@ long executeUpdateInternal( updateCount = ResultUtil.calculateUpdateCount(sfResultSet); queryID = sfResultSet.getQueryId(); resultSetMetadataHandler(sfResultSet); + } catch (SnowflakeSQLException ex) { + setQueryIdWhenValidOrNull(ex.getQueryId()); + throw ex; } catch (SFException ex) { + setQueryIdWhenValidOrNull(ex.getQueryId()); throw new SnowflakeSQLException( ex.getCause(), ex.getSqlState(), ex.getVendorCode(), ex.getParams()); } finally { @@ -237,6 +240,14 @@ long executeUpdateInternal( return updateCount; } + private void setQueryIdWhenValidOrNull(String queryId) { + if (QueryIdValidator.isValid(queryId)) { + this.queryID = queryId; + } else { + this.queryID = null; + } + } + /** * Internal method for executing a query with bindings accepted. * @@ -271,7 +282,11 @@ ResultSet executeQueryInternal( sfResultSet.setSession(this.connection.getSFBaseSession()); queryID = sfResultSet.getQueryId(); + } catch (SnowflakeSQLException ex) { + setQueryIdWhenValidOrNull(ex.getQueryId()); + throw ex; } catch (SFException ex) { + setQueryIdWhenValidOrNull(ex.getQueryId()); throw new SnowflakeSQLException( ex.getCause(), ex.getSqlState(), ex.getVendorCode(), ex.getParams()); } @@ -343,7 +358,11 @@ boolean executeInternal( updateCount = NO_UPDATES; return true; + } catch (SnowflakeSQLException ex) { + setQueryIdWhenValidOrNull(ex.getQueryId()); + throw ex; } catch (SFException ex) { + setQueryIdWhenValidOrNull(ex.getQueryId()); throw new SnowflakeSQLException( ex.getCause(), ex.getSqlState(), ex.getVendorCode(), ex.getParams()); } @@ -353,7 +372,6 @@ boolean executeInternal( * @return the query ID of the latest executed query */ public String getQueryID() { - // return the queryID for the query executed last time return queryID; } diff --git a/src/test/java/net/snowflake/client/TestUtil.java b/src/test/java/net/snowflake/client/TestUtil.java index 3c7f04958..68576fa30 100644 --- a/src/test/java/net/snowflake/client/TestUtil.java +++ b/src/test/java/net/snowflake/client/TestUtil.java @@ -5,7 +5,10 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import java.util.regex.Pattern; import net.snowflake.client.core.SFException; import net.snowflake.client.log.SFLogger; import net.snowflake.client.log.SFLoggerFactory; @@ -13,6 +16,10 @@ public class TestUtil { private static final SFLogger logger = SFLoggerFactory.getLogger(TestUtil.class); + + private static final Pattern QUERY_ID_REGEX = + Pattern.compile("[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}"); + /** * Util function to assert a piece will throw exception and assert on the error code * @@ -56,4 +63,10 @@ public static String systemGetEnv(String env) { } return null; } + + public static void assertValidQueryId(String queryId) { + assertNotNull(queryId); + assertTrue( + "Expecting " + queryId + " is a valid UUID", QUERY_ID_REGEX.matcher(queryId).matches()); + } } diff --git a/src/test/java/net/snowflake/client/jdbc/ConnectionLatestIT.java b/src/test/java/net/snowflake/client/jdbc/ConnectionLatestIT.java index 44f120026..b8a1875d9 100644 --- a/src/test/java/net/snowflake/client/jdbc/ConnectionLatestIT.java +++ b/src/test/java/net/snowflake/client/jdbc/ConnectionLatestIT.java @@ -25,9 +25,9 @@ import java.util.Map; import java.util.Properties; import java.util.concurrent.TimeUnit; -import java.util.regex.Pattern; import net.snowflake.client.ConditionalIgnoreRule; import net.snowflake.client.RunningOnGithubAction; +import net.snowflake.client.TestUtil; import net.snowflake.client.category.TestCategoryConnection; import net.snowflake.client.core.*; import net.snowflake.client.jdbc.telemetryOOB.TelemetryService; @@ -150,27 +150,72 @@ public void testHeartbeatFrequencyTooSmall() throws Exception { */ @Test @ConditionalIgnoreRule.ConditionalIgnore(condition = RunningOnGithubAction.class) - public void putStatementNullQueryID() throws Throwable { + public void putGetStatementsHaveQueryID() throws Throwable { Connection con = getConnection(); Statement statement = con.createStatement(); String sourceFilePath = getFullPathFileInResource(TEST_DATA_FILE); File destFolder = tmpFolder.newFolder(); String destFolderCanonicalPath = destFolder.getCanonicalPath(); statement.execute("CREATE OR REPLACE STAGE testPutGet_stage"); + SnowflakeStatement snowflakeStatement = statement.unwrap(SnowflakeStatement.class); + String createStageQueryId = snowflakeStatement.getQueryID(); + TestUtil.assertValidQueryId(createStageQueryId); String putStatement = "PUT file://" + sourceFilePath + " @testPutGet_stage"; - ResultSet resultSet = - statement.unwrap(SnowflakeStatement.class).executeAsyncQuery(putStatement); - String queryID = resultSet.unwrap(SnowflakeResultSet.class).getQueryID(); - assertTrue( - Pattern.matches("[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}", queryID)); + ResultSet resultSet = snowflakeStatement.executeAsyncQuery(putStatement); + String statementPutQueryId = snowflakeStatement.getQueryID(); + TestUtil.assertValidQueryId(statementPutQueryId); + assertNotEquals( + "create query id is override by put query id", createStageQueryId, statementPutQueryId); + String resultSetPutQueryId = resultSet.unwrap(SnowflakeResultSet.class).getQueryID(); + TestUtil.assertValidQueryId(resultSetPutQueryId); + assertEquals(resultSetPutQueryId, statementPutQueryId); resultSet = - statement - .unwrap(SnowflakeStatement.class) - .executeAsyncQuery( - "GET @testPutGet_stage 'file://" + destFolderCanonicalPath + "' parallel=8"); - queryID = resultSet.unwrap(SnowflakeResultSet.class).getQueryID(); - assertTrue( - Pattern.matches("[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}", queryID)); + snowflakeStatement.executeAsyncQuery( + "GET @testPutGet_stage 'file://" + destFolderCanonicalPath + "' parallel=8"); + String statementGetQueryId = snowflakeStatement.getQueryID(); + String resultSetGetQueryId = resultSet.unwrap(SnowflakeResultSet.class).getQueryID(); + TestUtil.assertValidQueryId(resultSetGetQueryId); + assertNotEquals( + "put and get query id should be different", resultSetGetQueryId, resultSetPutQueryId); + assertEquals(resultSetGetQueryId, statementGetQueryId); + } + + /** Added in > 3.14.4 */ + @Test + @ConditionalIgnoreRule.ConditionalIgnore(condition = RunningOnGithubAction.class) + public void putGetStatementsHaveQueryIDEvenWhenFail() throws Throwable { + try (Connection con = getConnection(); + Statement statement = con.createStatement()) { + String sourceFilePath = getFullPathFileInResource(TEST_DATA_FILE); + File destFolder = tmpFolder.newFolder(); + String destFolderCanonicalPath = destFolder.getCanonicalPath(); + SnowflakeStatement snowflakeStatement = statement.unwrap(SnowflakeStatement.class); + try { + statement.executeQuery("PUT file://" + sourceFilePath + " @not_existing_state"); + fail("PUT statement should fail"); + } catch (Exception __) { + TestUtil.assertValidQueryId(snowflakeStatement.getQueryID()); + } + String putQueryId = snowflakeStatement.getQueryID(); + try { + statement.executeQuery( + "GET @not_existing_state 'file://" + destFolderCanonicalPath + "' parallel=8"); + fail("GET statement should fail"); + } catch (Exception __) { + TestUtil.assertValidQueryId(snowflakeStatement.getQueryID()); + } + String getQueryId = snowflakeStatement.getQueryID(); + assertNotEquals("put and get query id should be different", putQueryId, getQueryId); + String stageName = "stage_" + SnowflakeUtil.randomAlphaNumeric(10); + statement.execute("CREATE OR REPLACE STAGE " + stageName); + TestUtil.assertValidQueryId(snowflakeStatement.getQueryID()); + try { + statement.executeQuery("PUT file://not_existing_file @" + stageName); + fail("PUT statement should fail"); + } catch (Exception __) { + assertNull(snowflakeStatement.getQueryID()); + } + } } @Test diff --git a/src/test/java/net/snowflake/client/jdbc/ResultSetAsyncIT.java b/src/test/java/net/snowflake/client/jdbc/ResultSetAsyncIT.java index 2c2b94506..ff9df32ca 100644 --- a/src/test/java/net/snowflake/client/jdbc/ResultSetAsyncIT.java +++ b/src/test/java/net/snowflake/client/jdbc/ResultSetAsyncIT.java @@ -13,7 +13,7 @@ import java.text.SimpleDateFormat; import java.util.List; import java.util.Map; -import java.util.regex.Pattern; +import net.snowflake.client.TestUtil; import net.snowflake.client.category.TestCategoryResultSet; import net.snowflake.common.core.SqlState; import org.junit.Test; @@ -71,10 +71,7 @@ public void testAsyncResultSetFunctionsWithNewSession() throws SQLException { assertEquals("COLB", colNames.get(1)); assertEquals(Types.DECIMAL, secretMetaData.getInternalColumnType(1)); assertEquals(Types.VARCHAR, secretMetaData.getInternalColumnType(2)); - assertTrue( - Pattern.matches( - "[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}", - secretMetaData.getQueryID())); + TestUtil.assertValidQueryId(secretMetaData.getQueryID()); assertEquals( secretMetaData.getQueryID(), resultSet.unwrap(SnowflakeResultSet.class).getQueryID()); resultSet.close(); @@ -118,10 +115,7 @@ public void testResultSetMetadata() throws SQLException { assertEquals("COLB", colNames.get(1)); assertEquals(Types.DECIMAL, secretMetaData.getInternalColumnType(1)); assertEquals(Types.VARCHAR, secretMetaData.getInternalColumnType(2)); - assertTrue( - Pattern.matches( - "[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}", - secretMetaData.getQueryID())); + TestUtil.assertValidQueryId(secretMetaData.getQueryID()); assertEquals( secretMetaData.getQueryID(), resultSet.unwrap(SnowflakeResultSet.class).getQueryID()); diff --git a/src/test/java/net/snowflake/client/jdbc/ResultSetLatestIT.java b/src/test/java/net/snowflake/client/jdbc/ResultSetLatestIT.java index 98e7befb5..e235d1b8c 100644 --- a/src/test/java/net/snowflake/client/jdbc/ResultSetLatestIT.java +++ b/src/test/java/net/snowflake/client/jdbc/ResultSetLatestIT.java @@ -18,6 +18,7 @@ import java.util.regex.Pattern; import net.snowflake.client.ConditionalIgnoreRule; import net.snowflake.client.RunningOnGithubAction; +import net.snowflake.client.TestUtil; import net.snowflake.client.category.TestCategoryResultSet; import net.snowflake.client.core.SFBaseSession; import net.snowflake.client.core.SessionUtil; @@ -186,10 +187,7 @@ public void testMetadataAPIMetricCollection() throws SQLException { TelemetryField.METADATA_METRICS.toString()); // Assert function name and params match and that query id exists assertEquals(logs.get(0).getMessage().get("function_name").textValue(), "getColumns"); - assertTrue( - Pattern.matches( - "[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}", - logs.get(0).getMessage().get("query_id").textValue())); + TestUtil.assertValidQueryId(logs.get(0).getMessage().get("query_id").textValue()); JsonNode parameterValues = logs.get(0).getMessage().get("function_parameters"); assertEquals(parameterValues.get("catalog").textValue(), "fakecatalog"); assertEquals(parameterValues.get("schema").textValue(), "fakeschema"); @@ -215,10 +213,7 @@ public void testMetadataAPIMetricCollection() throws SQLException { TelemetryField.METADATA_METRICS.toString()); // Assert function name and params match and that query id exists assertEquals(logs.get(1).getMessage().get("function_name").textValue(), "getColumns"); - assertTrue( - Pattern.matches( - "[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}", - logs.get(1).getMessage().get("query_id").textValue())); + TestUtil.assertValidQueryId(logs.get(1).getMessage().get("query_id").textValue()); parameterValues = logs.get(1).getMessage().get("function_parameters"); assertEquals(parameterValues.get("catalog").textValue(), catalog); assertEquals(parameterValues.get("schema").textValue(), schema); @@ -338,10 +333,7 @@ public void testResultSetMetadata() throws SQLException { assertEquals("COLB", colNames.get(1)); assertEquals(Types.DECIMAL, secretMetaData.getInternalColumnType(1)); assertEquals(Types.VARCHAR, secretMetaData.getInternalColumnType(2)); - assertTrue( - Pattern.matches( - "[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}", - secretMetaData.getQueryID())); + TestUtil.assertValidQueryId(secretMetaData.getQueryID()); statement.execute("drop table if exists test_rsmd"); statement.close(); diff --git a/src/test/java/net/snowflake/client/jdbc/StatementIT.java b/src/test/java/net/snowflake/client/jdbc/StatementIT.java index 2b4f1ae74..245b92ec5 100644 --- a/src/test/java/net/snowflake/client/jdbc/StatementIT.java +++ b/src/test/java/net/snowflake/client/jdbc/StatementIT.java @@ -624,4 +624,13 @@ public void testUnwrapper() throws Throwable { } } } + + @Test + public void testQueryIdIsNullOnFreshStatement() throws SQLException { + try (Connection con = getConnection()) { + try (Statement stmt = con.createStatement()) { + assertNull(stmt.unwrap(SnowflakeStatement.class).getQueryID()); + } + } + } } diff --git a/src/test/java/net/snowflake/client/jdbc/StatementLatestIT.java b/src/test/java/net/snowflake/client/jdbc/StatementLatestIT.java index 3349c5ee4..d05b767a3 100644 --- a/src/test/java/net/snowflake/client/jdbc/StatementLatestIT.java +++ b/src/test/java/net/snowflake/client/jdbc/StatementLatestIT.java @@ -16,6 +16,7 @@ import java.util.Map; import net.snowflake.client.ConditionalIgnoreRule; import net.snowflake.client.RunningOnGithubAction; +import net.snowflake.client.TestUtil; import net.snowflake.client.category.TestCategoryStatement; import net.snowflake.client.core.ParameterBindingDTO; import net.snowflake.client.core.SFSession; @@ -237,4 +238,55 @@ public void testSchemaWith255CharactersDoesNotCauseException() throws SQLExcepti } } } + + /** Added in > 3.14.4 */ + @Test + public void testQueryIdIsSetOnFailedQueryExecute() throws SQLException { + try (Connection con = getConnection()) { + try (Statement stmt = con.createStatement()) { + assertNull(stmt.unwrap(SnowflakeStatement.class).getQueryID()); + try { + stmt.execute("use database not_existing_database"); + fail("Statement should fail with exception"); + } catch (Exception __) { + String queryID = stmt.unwrap(SnowflakeStatement.class).getQueryID(); + TestUtil.assertValidQueryId(queryID); + } + } + } + } + + /** Added in > 3.14.4 */ + @Test + public void testQueryIdIsSetOnFailedExecuteUpdate() throws SQLException { + try (Connection con = getConnection()) { + try (Statement stmt = con.createStatement()) { + assertNull(stmt.unwrap(SnowflakeStatement.class).getQueryID()); + try { + stmt.executeUpdate("update not_existing_table set a = 1 where id = 42"); + fail("Statement should fail with exception"); + } catch (Exception __) { + String queryID = stmt.unwrap(SnowflakeStatement.class).getQueryID(); + TestUtil.assertValidQueryId(queryID); + } + } + } + } + + /** Added in > 3.14.4 */ + @Test + public void testQueryIdIsSetOnFailedExecuteQuery() throws SQLException { + try (Connection con = getConnection()) { + try (Statement stmt = con.createStatement()) { + assertNull(stmt.unwrap(SnowflakeStatement.class).getQueryID()); + try { + stmt.executeQuery("select * from not_existing_table"); + fail("Statement should fail with exception"); + } catch (Exception __) { + String queryID = stmt.unwrap(SnowflakeStatement.class).getQueryID(); + TestUtil.assertValidQueryId(queryID); + } + } + } + } } From bd37c849cd9e29176e3e08fa833ff2029cf3d4dd Mon Sep 17 00:00:00 2001 From: Piotr Fus Date: Fri, 8 Dec 2023 10:12:45 +0100 Subject: [PATCH 11/17] SNOW-980046 Implement queryStatusV2 (#1579) --- .../snowflake/client/core/SFBaseSession.java | 9 ++ .../net/snowflake/client/core/SFSession.java | 69 ++++++++- .../snowflake/client/core/SFStatement.java | 5 +- .../snowflake/client/jdbc/QueryStatusV2.java | 138 ++++++++++++++++++ .../client/jdbc/SFAsyncResultSet.java | 20 ++- .../client/jdbc/SnowflakeResultSet.java | 12 ++ .../client/jdbc/SnowflakeResultSetV1.java | 12 ++ .../client/jdbc/ConnectionLatestIT.java | 45 +++--- .../client/jdbc/MockConnectionTest.java | 5 + 9 files changed, 284 insertions(+), 31 deletions(-) create mode 100644 src/main/java/net/snowflake/client/jdbc/QueryStatusV2.java diff --git a/src/main/java/net/snowflake/client/core/SFBaseSession.java b/src/main/java/net/snowflake/client/core/SFBaseSession.java index a3ed040e5..810ce0398 100644 --- a/src/main/java/net/snowflake/client/core/SFBaseSession.java +++ b/src/main/java/net/snowflake/client/core/SFBaseSession.java @@ -819,10 +819,19 @@ public Object getSessionPropertyByKey(String propertyName) { /** * @param queryID query ID of the query whose status is being investigated * @return enum of type QueryStatus indicating the query's status + * @deprecated Use {@link #getQueryStatusV2(String)} * @throws SQLException */ + @Deprecated public abstract QueryStatus getQueryStatus(String queryID) throws SQLException; + /** + * @param queryID query ID of the query whose status is being investigated + * @return QueryStatusV2 indicating the query's status + * @throws SQLException + */ + public abstract QueryStatusV2 getQueryStatusV2(String queryID) throws SQLException; + /** * Validates the connection properties used by this session, and returns a list of missing * properties. diff --git a/src/main/java/net/snowflake/client/core/SFSession.java b/src/main/java/net/snowflake/client/core/SFSession.java index 9c10ebf1d..fec471492 100644 --- a/src/main/java/net/snowflake/client/core/SFSession.java +++ b/src/main/java/net/snowflake/client/core/SFSession.java @@ -164,13 +164,7 @@ public void addQueryToActiveQueryList(String queryID) { activeAsyncQueries.add(queryID); } - /** - * @param queryID query ID of the query whose status is being investigated - * @return enum of type QueryStatus indicating the query's status - * @throws SQLException - */ - @Override - public QueryStatus getQueryStatus(String queryID) throws SQLException { + private JsonNode getQueryMetadata(String queryID) throws SQLException { // create the URL to check the query monitoring endpoint String statusUrl = ""; String sessionUrl = getUrl(); @@ -245,7 +239,18 @@ else if (ex instanceof SFException) { } } } while (sessionRenewed); - JsonNode queryNode = jsonNode.path("data").path("queries"); + return jsonNode.path("data").path("queries"); + } + + /** + * @param queryID query ID of the query whose status is being investigated + * @return enum of type QueryStatus indicating the query's status + * @throws SQLException + * @deprecated the returned enum is error-prone, use {@link #getQueryStatusV2} instead + */ + @Deprecated + public QueryStatus getQueryStatus(String queryID) throws SQLException { + JsonNode queryNode = getQueryMetadata(queryID); String queryStatus = ""; String errorMessage = ""; int errorCode = 0; @@ -284,6 +289,54 @@ else if (isAnError(result)) { return result; } + /** + * @param queryID query ID of the query whose status is being investigated + * @return a QueryStatusV2 instance indicating the query's status + * @throws SQLException + */ + public QueryStatusV2 getQueryStatusV2(String queryID) throws SQLException { + JsonNode queryNode = getQueryMetadata(queryID); + logger.debug("Query status: {}", queryNode.asText()); + if (queryNode.isEmpty()) { + return QueryStatusV2.empty(); + } + JsonNode node = queryNode.get(0); + long endTime = node.path("endTime").asLong(0); + int errorCode = node.path("errorCode").asInt(0); + String errorMessage = node.path("errorMessage").asText("No error reported"); + String id = node.path("id").asText(""); + String name = node.path("status").asText(""); + long sessionId = node.path("sessionId").asLong(0); + String sqlText = node.path("sqlText").asText(""); + long startTime = node.path("startTime").asLong(0); + String state = node.path("state").asText(""); + int totalDuration = node.path("totalDuration").asInt(0); + String warehouseExternalSize = node.path("warehouseExternalSize").asText(null); + int warehouseId = node.path("warehouseId").asInt(0); + String warehouseName = node.path("warehouseName").asText(null); + String warehouseServerType = node.path("warehouseServerType").asText(null); + QueryStatusV2 result = + new QueryStatusV2( + endTime, + errorCode, + errorMessage, + id, + name, + sessionId, + sqlText, + startTime, + state, + totalDuration, + warehouseExternalSize, + warehouseId, + warehouseName, + warehouseServerType); + if (!result.isStillRunning()) { + activeAsyncQueries.remove(queryID); + } + return result; + } + /** * Add a property If a property is known for connection, add it to connection properties If not, * add it as a dynamic session parameters diff --git a/src/main/java/net/snowflake/client/core/SFStatement.java b/src/main/java/net/snowflake/client/core/SFStatement.java index 10f9c9559..5715da73a 100644 --- a/src/main/java/net/snowflake/client/core/SFStatement.java +++ b/src/main/java/net/snowflake/client/core/SFStatement.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.JsonNode; import java.sql.SQLException; import java.sql.Statement; +import java.util.*; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -649,8 +650,8 @@ public long getConservativeMemoryLimit() { */ @Override public String[] getChildQueryIds(String queryID) throws SQLException { - QueryStatus qs = session.getQueryStatus(queryID); - if (QueryStatus.isStillRunning(qs)) { + QueryStatusV2 qs = session.getQueryStatusV2(queryID); + if (qs.isStillRunning()) { throw new SQLException( "Status of query associated with resultSet is " + qs.getDescription() diff --git a/src/main/java/net/snowflake/client/jdbc/QueryStatusV2.java b/src/main/java/net/snowflake/client/jdbc/QueryStatusV2.java new file mode 100644 index 000000000..743447ef0 --- /dev/null +++ b/src/main/java/net/snowflake/client/jdbc/QueryStatusV2.java @@ -0,0 +1,138 @@ +package net.snowflake.client.jdbc; + +import net.snowflake.client.core.QueryStatus; + +public final class QueryStatusV2 { + private final long endTime; + private final int errorCode; + private final String errorMessage; + private final String id; + private final String name; + private final long sessionId; + private final String sqlText; + private final long startTime; + private final String state; + private final QueryStatus status; + private final int totalDuration; + private final String warehouseExternalSize; + private final int warehouseId; + private final String warehouseName; + private final String warehouseServerType; + + public QueryStatusV2( + long endTime, + int errorCode, + String errorMessage, + String id, + String name, + long sessionId, + String sqlText, + long startTime, + String state, + int totalDuration, + String warehouseExternalSize, + int warehouseId, + String warehouseName, + String warehouseServerType) { + this.endTime = endTime; + this.errorCode = errorCode; + this.errorMessage = errorMessage; + this.id = id; + this.name = name; + this.sessionId = sessionId; + this.sqlText = sqlText; + this.startTime = startTime; + this.state = state; + this.status = QueryStatus.getStatusFromString(name); + this.totalDuration = totalDuration; + this.warehouseExternalSize = warehouseExternalSize; + this.warehouseId = warehouseId; + this.warehouseName = warehouseName; + this.warehouseServerType = warehouseServerType; + } + + public static QueryStatusV2 empty() { + return new QueryStatusV2(0, 0, "", "", "", 0, "", 0, "", 0, "", 0, "", ""); + } + + public boolean isEmpty() { + return name.isEmpty(); + } + + public boolean isStillRunning() { + return QueryStatus.isStillRunning(status); + } + + public boolean isSuccess() { + return status == QueryStatus.SUCCESS; + } + + public boolean isAnError() { + return QueryStatus.isAnError(status); + } + + public long getEndTime() { + return endTime; + } + + public int getErrorCode() { + return errorCode; + } + + public String getErrorMessage() { + return errorMessage; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public long getSessionId() { + return sessionId; + } + + public String getSqlText() { + return sqlText; + } + + public long getStartTime() { + return startTime; + } + + public String getState() { + return state; + } + + public int getTotalDuration() { + return totalDuration; + } + + public String getWarehouseExternalSize() { + return warehouseExternalSize; + } + + public int getWarehouseId() { + return warehouseId; + } + + public String getWarehouseName() { + return warehouseName; + } + + public String getWarehouseServerType() { + return warehouseServerType; + } + + /** To preserve compatibility with {@link QueryStatus} */ + public String getDescription() { + return name; + } + + public QueryStatus getStatus() { + return status; + } +} diff --git a/src/main/java/net/snowflake/client/jdbc/SFAsyncResultSet.java b/src/main/java/net/snowflake/client/jdbc/SFAsyncResultSet.java index a7a94090c..8e2798db7 100644 --- a/src/main/java/net/snowflake/client/jdbc/SFAsyncResultSet.java +++ b/src/main/java/net/snowflake/client/jdbc/SFAsyncResultSet.java @@ -27,6 +27,7 @@ public class SFAsyncResultSet extends SnowflakeBaseResultSet private SFBaseSession session; private Statement extraStatement; private QueryStatus lastQueriedStatus = NO_DATA; + private QueryStatusV2 lastQueriedStatusV2 = QueryStatusV2.empty(); /** * Constructor takes an inputstream from the API response that we get from executing a SQL @@ -99,9 +100,9 @@ public QueryStatus getStatus() throws SQLException { if (this.lastQueriedStatus == QueryStatus.SUCCESS) { return this.lastQueriedStatus; } - this.lastQueriedStatus = session.getQueryStatus(this.queryID); // if query has completed successfully, cache its success status to avoid unnecessary future // server calls + this.lastQueriedStatus = session.getQueryStatus(this.queryID); return this.lastQueriedStatus; } @@ -110,6 +111,23 @@ public String getQueryErrorMessage() throws SQLException { return this.lastQueriedStatus.getErrorMessage(); } + @Override + public QueryStatusV2 getStatusV2() throws SQLException { + if (session == null) { + throw new SQLException("Session not set"); + } + if (this.queryID == null) { + throw new SQLException("QueryID unknown"); + } + if (this.lastQueriedStatusV2.isSuccess()) { + return this.lastQueriedStatusV2; + } + this.lastQueriedStatusV2 = session.getQueryStatusV2(this.queryID); + // if query has completed successfully, cache its metadata to avoid unnecessary future server + // calls + return this.lastQueriedStatusV2; + } + /** * helper function for next() and getMetaData(). Calls result_scan to get resultSet after * asynchronous query call diff --git a/src/main/java/net/snowflake/client/jdbc/SnowflakeResultSet.java b/src/main/java/net/snowflake/client/jdbc/SnowflakeResultSet.java index b858f2b1e..1fc7ff9ee 100644 --- a/src/main/java/net/snowflake/client/jdbc/SnowflakeResultSet.java +++ b/src/main/java/net/snowflake/client/jdbc/SnowflakeResultSet.java @@ -37,6 +37,18 @@ public interface SnowflakeResultSet { */ String getQueryErrorMessage() throws SQLException; + /** + * This function retrieves the status of an asynchronous query. An empty ResultSet object has + * already been returned, but the query may still be running. This function can be used to query + * whether it is possible to retrieve results from the ResultSet already. + * + *

status.isSuccess() means that results can be retrieved. + * + * @return an instance containing query metadata + * @throws SQLException + */ + QueryStatusV2 getStatusV2() throws SQLException; + /** * Get a list of ResultSetSerializables for the ResultSet in order to parallel processing * diff --git a/src/main/java/net/snowflake/client/jdbc/SnowflakeResultSetV1.java b/src/main/java/net/snowflake/client/jdbc/SnowflakeResultSetV1.java index cd8e5a787..e1068799b 100644 --- a/src/main/java/net/snowflake/client/jdbc/SnowflakeResultSetV1.java +++ b/src/main/java/net/snowflake/client/jdbc/SnowflakeResultSetV1.java @@ -50,6 +50,18 @@ public QueryStatus getStatus() throws SQLException { throw new SnowflakeLoggedFeatureNotSupportedException(session); } + /** + * This function is not supported for synchronous queries + * + * @return no return value; exception is always thrown + * @throws SQLFeatureNotSupportedException + */ + @Override + public QueryStatusV2 getStatusV2() throws SQLException { + throw new SnowflakeLoggedFeatureNotSupportedException( + session, "This function is only supported for asynchronous queries."); + } + /** * This function is not supported for synchronous queries * diff --git a/src/test/java/net/snowflake/client/jdbc/ConnectionLatestIT.java b/src/test/java/net/snowflake/client/jdbc/ConnectionLatestIT.java index b8a1875d9..5a983fc18 100644 --- a/src/test/java/net/snowflake/client/jdbc/ConnectionLatestIT.java +++ b/src/test/java/net/snowflake/client/jdbc/ConnectionLatestIT.java @@ -4,9 +4,7 @@ package net.snowflake.client.jdbc; import static net.snowflake.client.core.SessionUtil.CLIENT_SESSION_KEEP_ALIVE_HEARTBEAT_FREQUENCY; -import static net.snowflake.client.jdbc.ConnectionIT.BAD_REQUEST_GS_CODE; -import static net.snowflake.client.jdbc.ConnectionIT.INVALID_CONNECTION_INFO_CODE; -import static net.snowflake.client.jdbc.ConnectionIT.WAIT_FOR_TELEMETRY_REPORT_IN_MILLISECS; +import static net.snowflake.client.jdbc.ConnectionIT.*; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -37,7 +35,10 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.StringEntity; -import org.junit.*; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.rules.TemporaryFolder; @@ -227,21 +228,22 @@ public void testAsyncQueryOpenAndCloseConnection() ResultSet rs1 = statement .unwrap(SnowflakeStatement.class) - .executeAsyncQuery("select count(*) from table(generator(timeLimit => 40))"); + .executeAsyncQuery("CALL SYSTEM$WAIT(60, 'SECONDS')"); // Retrieve query ID for part 2 of test, check status of query String queryID = rs1.unwrap(SnowflakeResultSet.class).getQueryID(); - QueryStatus status = null; + QueryStatusV2 statusV2 = null; for (int retry = 0; retry < 5; ++retry) { Thread.sleep(100); - status = rs1.unwrap(SnowflakeResultSet.class).getStatus(); + statusV2 = rs1.unwrap(SnowflakeResultSet.class).getStatusV2(); // Sometimes 100 millis is too short for GS to get query status with provided queryID, in // which case we will get NO_DATA. - if (status != QueryStatus.NO_DATA) { + if (statusV2.getStatus() != QueryStatus.NO_DATA) { break; } } // Query should take 60 seconds so should be running - assertEquals(QueryStatus.RUNNING, status); + assertEquals(QueryStatus.RUNNING, statusV2.getStatus()); + assertEquals(QueryStatus.RUNNING.name(), statusV2.getName()); // close connection and wait for 1 minute while query finishes running statement.close(); con.close(); @@ -256,11 +258,11 @@ public void testAsyncQueryOpenAndCloseConnection() assertEquals(SqlState.INVALID_PARAMETER_VALUE, e.getSQLState()); } ResultSet rs = con.unwrap(SnowflakeConnection.class).createResultSet(queryID); - status = rs.unwrap(SnowflakeResultSet.class).getStatus(); + statusV2 = rs.unwrap(SnowflakeResultSet.class).getStatusV2(); // Assert status of query is a success - assertEquals(QueryStatus.SUCCESS, status); - assertEquals("No error reported", status.getErrorMessage()); - assertEquals(0, status.getErrorCode()); + assertEquals(QueryStatus.SUCCESS, statusV2.getStatus()); + assertEquals("No error reported", statusV2.getErrorMessage()); + assertEquals(0, statusV2.getErrorCode()); assertEquals(1, getSizeOfResultSet(rs)); statement = con.createStatement(); // Create another query that will not be successful (querying table that does not exist) @@ -269,23 +271,26 @@ public void testAsyncQueryOpenAndCloseConnection() .unwrap(SnowflakeStatement.class) .executeAsyncQuery("select * from nonexistentTable"); Thread.sleep(100); - status = rs1.unwrap(SnowflakeResultSet.class).getStatus(); + statusV2 = rs1.unwrap(SnowflakeResultSet.class).getStatusV2(); // when GS response is slow, allow up to 1 second of retries to get final query status int counter = 0; - while ((status == QueryStatus.NO_DATA || status == QueryStatus.RUNNING) && counter < 10) { + while ((statusV2.getStatus() == QueryStatus.NO_DATA + || statusV2.getStatus() == QueryStatus.RUNNING) + && counter < 10) { + counter++; Thread.sleep(100); - status = rs1.unwrap(SnowflakeResultSet.class).getStatus(); + statusV2 = rs1.unwrap(SnowflakeResultSet.class).getStatusV2(); } // If GS response is too slow to return data, do nothing to avoid flaky test failure. If // response has returned, // assert it is the error message that we are expecting. - if (status != QueryStatus.NO_DATA) { - assertEquals(QueryStatus.FAILED_WITH_ERROR, status); - assertEquals(2003, status.getErrorCode()); + if (statusV2.getStatus() != QueryStatus.NO_DATA) { + assertEquals(QueryStatus.FAILED_WITH_ERROR, statusV2.getStatus()); + assertEquals(2003, statusV2.getErrorCode()); assertEquals( "SQL compilation error:\n" + "Object 'NONEXISTENTTABLE' does not exist or not authorized.", - status.getErrorMessage()); + statusV2.getErrorMessage()); } statement.close(); con.close(); diff --git a/src/test/java/net/snowflake/client/jdbc/MockConnectionTest.java b/src/test/java/net/snowflake/client/jdbc/MockConnectionTest.java index 49bc6184f..3c10381be 100644 --- a/src/test/java/net/snowflake/client/jdbc/MockConnectionTest.java +++ b/src/test/java/net/snowflake/client/jdbc/MockConnectionTest.java @@ -675,6 +675,11 @@ public QueryStatus getQueryStatus(String queryID) { return null; } + @Override + public QueryStatusV2 getQueryStatusV2(String queryID) throws SQLException { + return null; + } + @Override public Telemetry getTelemetryClient() { return new Telemetry() { From 7b3c1f990d8f79598739f11cd3814bb56179e58e Mon Sep 17 00:00:00 2001 From: Joyce <115662568+sfc-gh-ext-simba-jl@users.noreply.github.com> Date: Mon, 11 Dec 2023 10:29:10 -0800 Subject: [PATCH 12/17] SNOW-946164: Upgrade grpc-netty-shaded version to 1.60.0 (#1582) --- parent-pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parent-pom.xml b/parent-pom.xml index c5b59f475..7c70e6bab 100644 --- a/parent-pom.xml +++ b/parent-pom.xml @@ -42,7 +42,7 @@ 1.43.3 3.0.2 3.23.3 - 1.59.0 + 1.60.0 2.2 2.4.3 2.15.3 From a590a49faee171a587813e609596fba28e2595ed Mon Sep 17 00:00:00 2001 From: Dominik Przybysz <132913826+sfc-gh-dprzybysz@users.noreply.github.com> Date: Tue, 12 Dec 2023 11:15:10 +0100 Subject: [PATCH 13/17] SNOW-989861: Update CLA github action (#1584) --- .github/workflows/cla_bot.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cla_bot.yml b/.github/workflows/cla_bot.yml index be1486f4c..c76e38a49 100644 --- a/.github/workflows/cla_bot.yml +++ b/.github/workflows/cla_bot.yml @@ -6,7 +6,7 @@ on: types: [opened,closed,synchronize] jobs: - CLAssistant: + CLAAssistant: runs-on: ubuntu-latest permissions: actions: write @@ -16,7 +16,7 @@ jobs: steps: - name: "CLA Assistant" if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' - uses: cla-assistant/github-action@master + uses: contributor-assistant/github-action/@master env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PERSONAL_ACCESS_TOKEN : ${{ secrets.CLA_BOT_TOKEN }} @@ -24,6 +24,7 @@ jobs: path-to-signatures: 'signatures/version1.json' path-to-document: 'https://github.com/snowflakedb/CLA/blob/main/README.md' branch: 'main' - allowlist: 'dependabot[bot],github-actions,Jenkins User,_jenkins' + allowlist: 'dependabot[bot],github-actions,Jenkins User,_jenkins,sfc-gh-snyk-sca-sa,snyk-bot' remote-organization-name: 'snowflakedb' remote-repository-name: 'cla-db' + From b5a688a31d7769150457052f4c64f2090b58c317 Mon Sep 17 00:00:00 2001 From: sfc-gh-snowflakedb-snyk-sa <130097897+sfc-gh-snowflakedb-snyk-sa@users.noreply.github.com> Date: Wed, 13 Dec 2023 12:37:56 +0530 Subject: [PATCH 14/17] [Snyk] Security upgrade org.codehaus.plexus:plexus-archiver from 4.8.0 to 4.9.0 (#1552) fix: TestOnly/pom.xml to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JAVA-ORGAPACHECOMMONS-5901530 Co-authored-by: snyk-bot --- TestOnly/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TestOnly/pom.xml b/TestOnly/pom.xml index ec1b298e1..0b194177b 100644 --- a/TestOnly/pom.xml +++ b/TestOnly/pom.xml @@ -122,7 +122,7 @@ org.codehaus.plexus plexus-archiver - 4.8.0 + 4.9.0 provided From 301005e84d77251020be799edb94fd2049edc9b3 Mon Sep 17 00:00:00 2001 From: Piotr Fus Date: Tue, 19 Dec 2023 11:03:56 +0100 Subject: [PATCH 15/17] SNOW-937168 Delete Centos6 usage in Jenkins and Github Actions (#1588) --- .github/workflows/build-test.yml | 4 +- ci/_init.sh | 2 - ci/image/Dockerfile.jdbc-centos6-default-test | 67 ------------------- 3 files changed, 2 insertions(+), 71 deletions(-) delete mode 100644 ci/image/Dockerfile.jdbc-centos6-default-test diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 92a7a8a54..40536225f 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -37,7 +37,7 @@ jobs: strategy: fail-fast: false matrix: - image: [ 'jdbc-centos6-default', 'jdbc-centos7-openjdk8', 'jdbc-centos7-openjdk11', 'jdbc-centos7-openjdk17' ] + image: [ 'jdbc-centos7-openjdk8', 'jdbc-centos7-openjdk11', 'jdbc-centos7-openjdk17' ] cloud: [ 'AWS' ] category: ['TestCategoryResultSet,TestCategoryOthers,TestCategoryLoader', 'TestCategoryConnection,TestCategoryStatement', 'TestCategoryArrow,TestCategoryCore', 'TestCategoryFips'] steps: @@ -57,7 +57,7 @@ jobs: strategy: fail-fast: false matrix: - image: [ 'jdbc-centos6-default' ] + image: [ 'jdbc-centos7-openjdk8' ] cloud: [ 'AWS' ] category: ['TestCategoryResultSet,TestCategoryOthers', 'TestCategoryConnection,TestCategoryStatement', 'TestCategoryCore,TestCategoryLoader'] is_old_driver: ['true'] diff --git a/ci/_init.sh b/ci/_init.sh index d1e585b85..c91f03c31 100755 --- a/ci/_init.sh +++ b/ci/_init.sh @@ -20,7 +20,6 @@ export DRIVER_NAME=jdbc TEST_IMAGE_VERSION=1 declare -A TEST_IMAGE_NAMES=( - [$DRIVER_NAME-centos6-default]=$DOCKER_REGISTRY_NAME/client-$DRIVER_NAME-centos6-default-test:$TEST_IMAGE_VERSION [$DRIVER_NAME-centos7-openjdk8]=$DOCKER_REGISTRY_NAME/client-$DRIVER_NAME-centos7-openjdk8-test:$TEST_IMAGE_VERSION [$DRIVER_NAME-centos7-openjdk11]=$DOCKER_REGISTRY_NAME/client-$DRIVER_NAME-centos7-openjdk11-test:$TEST_IMAGE_VERSION [$DRIVER_NAME-centos7-openjdk17]=$DOCKER_REGISTRY_NAME/client-$DRIVER_NAME-centos7-openjdk17-test:$TEST_IMAGE_VERSION @@ -28,7 +27,6 @@ declare -A TEST_IMAGE_NAMES=( export TEST_IMAGE_NAMES declare -A TEST_IMAGE_DOCKERFILES=( - [$DRIVER_NAME-centos6-default]=jdbc-centos6-default-test [$DRIVER_NAME-centos7-openjdk8]=jdbc-centos7-openjdk-test [$DRIVER_NAME-centos7-openjdk11]=jdbc-centos7-openjdk-test [$DRIVER_NAME-centos7-openjdk17]=jdbc-centos7-openjdk-test diff --git a/ci/image/Dockerfile.jdbc-centos6-default-test b/ci/image/Dockerfile.jdbc-centos6-default-test deleted file mode 100644 index 2ff9478e7..000000000 --- a/ci/image/Dockerfile.jdbc-centos6-default-test +++ /dev/null @@ -1,67 +0,0 @@ -FROM centos:6.10 - -# update OS -RUN yum -y update && \ - yum -y install epel-release && \ - yum -y install centos-release-scl - -# python -RUN yum -y install rh-python36 -COPY scripts/python3.6.sh /usr/local/bin/python3.6 -COPY scripts/python3.6.sh /usr/local/bin/python3 -RUN chmod a+x /usr/local/bin/python3.6 /usr/local/bin/python3 -COPY scripts/pip.sh /usr/local/bin/pip -RUN chmod a+x /usr/local/bin/pip -RUN pip install -U pip -RUN pip install -U snowflake-connector-python - -# aws -RUN pip install -U awscli -COPY scripts/aws.sh /usr/local/bin/aws -RUN chmod a+x /usr/local/bin/aws - -# install Development tools -RUN yum -y groupinstall "Development Tools" && \ - yum -y install zlib-devel - -# git -RUN curl -o - https://mirrors.edge.kernel.org/pub/software/scm/git/git-2.26.0.tar.gz | tar xfz - && \ - cd git-2.26.0 && \ - ./configure --prefix=/opt/git && make && make install && \ - ln -s /opt/git/bin/git /usr/local/bin/git - -# zstd -RUN yum -y install zstd - -# jq -RUN yum -y install jq - -# gosu -RUN curl -o /usr/local/bin/gosu -SL "https://github.com/tianon/gosu/releases/download/1.11/gosu-amd64" -RUN chmod +x /usr/local/bin/gosu -COPY scripts/entrypoint.sh /usr/local/bin/entrypoint.sh -RUN chmod +x /usr/local/bin/entrypoint.sh - -# Java -RUN yum -y install java-1.8.0-openjdk-devel - -# Maven -RUN curl -o - https://downloads.apache.org/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz | tar xfz - -C /opt && \ - ln -s /opt/apache-maven-3.6.3/bin/mvn /usr/local/bin/mvn - -# workspace -RUN mkdir -p /home/user && \ - chmod 777 /home/user -WORKDIR /home/user - -COPY pom.xml /root -COPY dependencies /root/dependencies - -RUN cd /root && \ - mvn -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn \ - -Dnot-self-contained-jar \ - --batch-mode --fail-never compile && \ - mv $HOME/.m2 /home/user && \ - chmod -R 777 /home/user/.m2 - -ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] From 55e5b15d98e97b521385db91362c677707065653 Mon Sep 17 00:00:00 2001 From: Dominik Przybysz <132913826+sfc-gh-dprzybysz@users.noreply.github.com> Date: Tue, 2 Jan 2024 12:35:32 +0100 Subject: [PATCH 16/17] SNOW-909180: Pass query id in exceptions from file agent (#1581) --- .../jdbc/SnowflakeFileTransferAgent.java | 309 +++++++++++++++--- .../client/jdbc/SnowflakeSQLException.java | 42 ++- .../jdbc/SnowflakeSQLLoggedException.java | 37 ++- .../jdbc/cloud/storage/QueryIdHelper.java | 12 + .../cloud/storage/SnowflakeAzureClient.java | 65 +++- .../cloud/storage/SnowflakeGCSClient.java | 99 ++++-- .../jdbc/cloud/storage/SnowflakeS3Client.java | 58 +++- .../cloud/storage/SnowflakeStorageClient.java | 225 ++++++++++++- .../client/jdbc/ConnectionLatestIT.java | 11 +- .../jdbc/FileUploaderExpandFileNamesTest.java | 4 +- ...akeAzureClientHandleExceptionLatestIT.java | 27 +- .../client/jdbc/SnowflakeDriverLatestIT.java | 3 +- ...flakeGcsClientHandleExceptionLatestIT.java | 31 +- ...wflakeS3ClientHandleExceptionLatestIT.java | 29 +- .../client/jdbc/StatementLatestIT.java | 9 +- .../storage/SnowflakeS3ClientLatestIT.java | 6 +- 16 files changed, 787 insertions(+), 180 deletions(-) create mode 100644 src/main/java/net/snowflake/client/jdbc/cloud/storage/QueryIdHelper.java diff --git a/src/main/java/net/snowflake/client/jdbc/SnowflakeFileTransferAgent.java b/src/main/java/net/snowflake/client/jdbc/SnowflakeFileTransferAgent.java index d6c06b910..cf4996715 100644 --- a/src/main/java/net/snowflake/client/jdbc/SnowflakeFileTransferAgent.java +++ b/src/main/java/net/snowflake/client/jdbc/SnowflakeFileTransferAgent.java @@ -87,7 +87,7 @@ public class SnowflakeFileTransferAgent extends SFBaseFileTransferAgent { private String localLocation; // Query ID of PUT or GET statement - private String queryID = ""; + private String queryID = null; // default parallelism private int parallel = DEFAULT_PARALLEL; @@ -295,7 +295,7 @@ static class InputStreamWithMetadata { * @throws SnowflakeSQLException if encountered exception when compressing */ private static InputStreamWithMetadata compressStreamWithGZIP( - InputStream inputStream, SFBaseSession session) throws SnowflakeSQLException { + InputStream inputStream, SFBaseSession session, String queryId) throws SnowflakeSQLException { FileBackedOutputStream tempStream = new FileBackedOutputStream(MAX_BUFFER_SIZE, true); try { @@ -335,6 +335,7 @@ private static InputStreamWithMetadata compressStreamWithGZIP( logger.error("Exception compressing input stream", ex); throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), @@ -353,7 +354,7 @@ private static InputStreamWithMetadata compressStreamWithGZIP( */ @Deprecated private static InputStreamWithMetadata compressStreamWithGZIPNoDigest( - InputStream inputStream, SFBaseSession session) throws SnowflakeSQLException { + InputStream inputStream, SFBaseSession session, String queryId) throws SnowflakeSQLException { try { FileBackedOutputStream tempStream = new FileBackedOutputStream(MAX_BUFFER_SIZE, true); @@ -383,6 +384,7 @@ private static InputStreamWithMetadata compressStreamWithGZIPNoDigest( logger.error("Exception compressing input stream", ex); throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), @@ -425,6 +427,9 @@ private static InputStreamWithMetadata computeDigest(InputStream is, boolean res * *

The callable does compression if needed and upload the result to the table's staging area. * + * @deprecated use {@link #getUploadFileCallable(StageInfo, String, FileMetadata, + * SnowflakeStorageClient, SFSession, String, InputStream, boolean, int, File, + * RemoteStoreFileEncryptionMaterial, String)} * @param stage information about the stage * @param srcFilePath source file path * @param metadata file metadata @@ -438,6 +443,7 @@ private static InputStreamWithMetadata computeDigest(InputStream is, boolean res * @param encMat not null if encryption is required * @return a callable that uploading file to the remote store */ + @Deprecated public static Callable getUploadFileCallable( final StageInfo stage, final String srcFilePath, @@ -450,6 +456,53 @@ public static Callable getUploadFileCallable( final int parallel, final File srcFile, final RemoteStoreFileEncryptionMaterial encMat) { + return getUploadFileCallable( + stage, + srcFilePath, + metadata, + client, + session, + command, + inputStream, + sourceFromStream, + parallel, + srcFile, + encMat, + null); + } + + /** + * A callable that can be executed in a separate thread using executor service. + * + *

The callable does compression if needed and upload the result to the table's staging area. + * + * @param stage information about the stage + * @param srcFilePath source file path + * @param metadata file metadata + * @param client client object used to communicate with c3 + * @param session session object + * @param command command string + * @param inputStream null if upload source is file + * @param sourceFromStream whether upload source is file or stream + * @param parallel number of threads for parallel uploading + * @param srcFile source file name + * @param encMat not null if encryption is required + * @param queryId last executed query id (for forwarding in possible exceptions) + * @return a callable that uploading file to the remote store + */ + public static Callable getUploadFileCallable( + final StageInfo stage, + final String srcFilePath, + final FileMetadata metadata, + final SnowflakeStorageClient client, + final SFSession session, + final String command, + final InputStream inputStream, + final boolean sourceFromStream, + final int parallel, + final File srcFile, + final RemoteStoreFileEncryptionMaterial encMat, + final String queryId) { return new Callable() { public Void call() throws Exception { @@ -484,6 +537,7 @@ public Void call() throws Exception { // this shouldn't happen if (metadata == null) { throw new SnowflakeSQLLoggedException( + queryId, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -507,8 +561,8 @@ public Void call() throws Exception { if (metadata.requireCompress) { InputStreamWithMetadata compressedSizeAndStream = (encMat == null - ? compressStreamWithGZIPNoDigest(uploadStream, session) - : compressStreamWithGZIP(uploadStream, session)); + ? compressStreamWithGZIPNoDigest(uploadStream, session, queryId) + : compressStreamWithGZIP(uploadStream, session, queryId)); fileBackedOutputStream = compressedSizeAndStream.fileBackedOutputStream; @@ -572,7 +626,8 @@ public Void call() throws Exception { destFileName, uploadStream, fileBackedOutputStream, - session); + session, + queryId); break; case S3: @@ -594,7 +649,8 @@ public Void call() throws Exception { (fileToUpload == null), encMat, null, - null); + null, + queryId); metadata.isEncrypted = encMat != null; break; } @@ -640,6 +696,9 @@ public Void call() throws Exception { * *

The callable download files from a stage location to a local location * + * @deprecated use {@link #getDownloadFileCallable(StageInfo, String, String, Map, + * SnowflakeStorageClient, SFSession, String, int, RemoteStoreFileEncryptionMaterial, String, + * String)} * @param stage stage information * @param srcFilePath path that stores the downloaded file * @param localLocation local location @@ -652,6 +711,7 @@ public Void call() throws Exception { * @param presignedUrl Presigned URL for file download * @return a callable responsible for downloading files */ + @Deprecated public static Callable getDownloadFileCallable( final StageInfo stage, final String srcFilePath, @@ -663,6 +723,49 @@ public static Callable getDownloadFileCallable( final int parallel, final RemoteStoreFileEncryptionMaterial encMat, final String presignedUrl) { + return getDownloadFileCallable( + stage, + srcFilePath, + localLocation, + fileMetadataMap, + client, + session, + command, + parallel, + encMat, + presignedUrl, + null); + } + + /** + * A callable that can be executed in a separate thread using executor service. + * + *

The callable download files from a stage location to a local location + * + * @param stage stage information + * @param srcFilePath path that stores the downloaded file + * @param localLocation local location + * @param fileMetadataMap file metadata map + * @param client remote store client + * @param session session object + * @param command command string + * @param encMat remote store encryption material + * @param parallel number of parallel threads for downloading + * @param presignedUrl Presigned URL for file download + * @return a callable responsible for downloading files + */ + public static Callable getDownloadFileCallable( + final StageInfo stage, + final String srcFilePath, + final String localLocation, + final Map fileMetadataMap, + final SnowflakeStorageClient client, + final SFSession session, + final String command, + final int parallel, + final RemoteStoreFileEncryptionMaterial encMat, + final String presignedUrl, + final String queryId) { return new Callable() { public Void call() throws Exception { @@ -676,6 +779,7 @@ public Void call() throws Exception { // this shouldn't happen if (metadata == null) { throw new SnowflakeSQLLoggedException( + queryId, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -695,7 +799,7 @@ public Void call() throws Exception { switch (stage.getStageType()) { case LOCAL_FS: pullFileFromLocal( - stage.getLocation(), srcFilePath, localLocation, destFileName, session); + stage.getLocation(), srcFilePath, localLocation, destFileName, session, queryId); break; case AZURE: @@ -711,7 +815,8 @@ public Void call() throws Exception { command, parallel, encMat, - presignedUrl); + presignedUrl, + queryId); metadata.isEncrypted = encMat != null; break; } @@ -800,6 +905,7 @@ private void parseCommand() throws SnowflakeSQLException { initPresignedUrls(commandType, jsonNode); } catch (Exception ex) { throw new SnowflakeSQLException( + queryID, ex, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), @@ -826,7 +932,7 @@ private void parseCommand() throws SnowflakeSQLException { localFilePathFromGS = src_locations[0]; } - sourceFiles = expandFileNames(src_locations); + sourceFiles = expandFileNames(src_locations, queryID); autoCompress = jsonNode.path("data").path("autoCompress").asBoolean(true); @@ -867,6 +973,7 @@ private void parseCommand() throws SnowflakeSQLException { // it should not start with any ~ after the above replacement if (localLocation.startsWith("~")) { throw new SnowflakeSQLLoggedException( + queryID, session, ErrorCode.PATH_NOT_DIRECTORY.getMessageCode(), SqlState.IO_ERROR, @@ -890,6 +997,7 @@ private void parseCommand() throws SnowflakeSQLException { // local location should be a directory if ((new File(localLocation)).isFile()) { throw new SnowflakeSQLLoggedException( + queryID, session, ErrorCode.PATH_NOT_DIRECTORY.getMessageCode(), SqlState.IO_ERROR, @@ -938,6 +1046,7 @@ private void parseCommand() throws SnowflakeSQLException { * @throws SnowflakeSQLException */ static StageInfo getStageInfo(JsonNode jsonNode, SFSession session) throws SnowflakeSQLException { + String queryId = jsonNode.path("data").path("queryId").asText(); // more parameters common to upload/download String stageLocation = jsonNode.path("data").path("stageInfo").path("location").asText(); @@ -1005,7 +1114,7 @@ static StageInfo getStageInfo(JsonNode jsonNode, SFSession session) throws Snowf } } - Map stageCredentials = extractStageCreds(jsonNode); + Map stageCredentials = extractStageCreds(jsonNode, queryId); StageInfo stageInfo = StageInfo.createStageInfo( @@ -1048,6 +1157,7 @@ static StageInfo getStageInfo(JsonNode jsonNode, SFSession session) throws Snowf return stageInfo; } + /** * A helper method to verify if the local file path from GS matches what's parsed locally. This is * for security purpose as documented in SNOW-15153. @@ -1061,6 +1171,7 @@ private void verifyLocalFilePath(String localFilePathFromGS) throws SnowflakeSQL if (!localFilePath.isEmpty() && !localFilePath.equals(localFilePathFromGS)) { throw new SnowflakeSQLLoggedException( + queryID, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -1155,7 +1266,8 @@ private static JsonNode parseCommandInGS(SFStatement statement, String command) false, // async new ExecTimeTelemetryData()); // OOB telemetry timing queries } catch (SFException ex) { - throw new SnowflakeSQLException(ex, ex.getSqlState(), ex.getVendorCode(), ex.getParams()); + throw new SnowflakeSQLException( + ex.getQueryId(), ex, ex.getSqlState(), ex.getVendorCode(), ex.getParams()); } JsonNode jsonNode = (JsonNode) result; @@ -1169,7 +1281,8 @@ private static JsonNode parseCommandInGS(SFStatement statement, String command) * @param rootNode JSON doc returned by GS * @throws SnowflakeSQLException Will be thrown if we fail to parse the stage credentials */ - private static Map extractStageCreds(JsonNode rootNode) throws SnowflakeSQLException { + private static Map extractStageCreds(JsonNode rootNode, String queryId) + throws SnowflakeSQLException { JsonNode credsNode = rootNode.path("data").path("stageInfo").path("creds"); Map stageCredentials = null; @@ -1180,6 +1293,7 @@ private static JsonNode parseCommandInGS(SFStatement statement, String command) } catch (Exception ex) { throw new SnowflakeSQLException( + queryId, ex, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), @@ -1207,6 +1321,7 @@ public List getFileTransferMetadatas() && stageInfo.getStageType() != StageInfo.StageType.AZURE && stageInfo.getStageType() != StageInfo.StageType.S3) { throw new SnowflakeSQLLoggedException( + queryID, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -1215,6 +1330,7 @@ public List getFileTransferMetadatas() if (commandType != CommandType.UPLOAD) { throw new SnowflakeSQLLoggedException( + queryID, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -1250,19 +1366,37 @@ public List getFileTransferMetadatas() */ public static List getFileTransferMetadatas(JsonNode jsonNode) throws SnowflakeSQLException { + return getFileTransferMetadatas(jsonNode, null); + } + + /** + * This is API function to parse the File Transfer Metadatas from a supplied PUT call response. + * + *

NOTE: It only supports PUT on S3/AZURE/GCS (i.e. NOT LOCAL_FS) + * + *

It also assumes there is no active SFSession + * + * @param jsonNode JSON doc returned by GS from PUT call + * @param queryId String last executed query id if available + * @return The file transfer metadatas for to-be-transferred files. + * @throws SnowflakeSQLException if any error occurs + */ + public static List getFileTransferMetadatas( + JsonNode jsonNode, String queryId) throws SnowflakeSQLException { CommandType commandType = !jsonNode.path("data").path("command").isMissingNode() ? CommandType.valueOf(jsonNode.path("data").path("command").asText()) : CommandType.UPLOAD; if (commandType != CommandType.UPLOAD) { throw new SnowflakeSQLException( - ErrorCode.INTERNAL_ERROR, "This API only supports PUT commands"); + queryId, ErrorCode.INTERNAL_ERROR, "This API only supports PUT commands"); } JsonNode locationsNode = jsonNode.path("data").path("src_locations"); if (!locationsNode.isArray()) { - throw new SnowflakeSQLException(ErrorCode.INTERNAL_ERROR, "src_locations must be an array"); + throw new SnowflakeSQLException( + queryId, ErrorCode.INTERNAL_ERROR, "src_locations must be an array"); } final String[] srcLocations; @@ -1271,13 +1405,16 @@ public static List getFileTransferMetadatas(JsonN srcLocations = mapper.readValue(locationsNode.toString(), String[].class); } catch (Exception ex) { throw new SnowflakeSQLException( - ErrorCode.INTERNAL_ERROR, "Failed to parse the locations due to: " + ex.getMessage()); + queryId, + ErrorCode.INTERNAL_ERROR, + "Failed to parse the locations due to: " + ex.getMessage()); } try { encryptionMaterial = getEncryptionMaterial(commandType, jsonNode); } catch (Exception ex) { throw new SnowflakeSQLException( + queryId, ErrorCode.INTERNAL_ERROR, "Failed to parse encryptionMaterial due to: " + ex.getMessage()); } @@ -1285,7 +1422,7 @@ public static List getFileTransferMetadatas(JsonN // For UPLOAD we expect encryptionMaterial to have length 1 assert encryptionMaterial.size() == 1; - final Set sourceFiles = expandFileNames(srcLocations); + final Set sourceFiles = expandFileNames(srcLocations, queryId); StageInfo stageInfo = getStageInfo(jsonNode, null /*SFSession*/); @@ -1294,6 +1431,7 @@ public static List getFileTransferMetadatas(JsonN && stageInfo.getStageType() != StageInfo.StageType.AZURE && stageInfo.getStageType() != StageInfo.StageType.S3) { throw new SnowflakeSQLException( + queryId, ErrorCode.INTERNAL_ERROR, "This API only supports S3/AZURE/GCS, received=" + stageInfo.getStageType()); } @@ -1431,10 +1569,11 @@ private void uploadStream() throws SnowflakeSQLException { true, parallel, null, - encMat)); + encMat, + queryID)); } else if (commandType == CommandType.DOWNLOAD) { throw new SnowflakeSQLLoggedException( - session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR); + queryID, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR); } threadExecutor.shutdown(); @@ -1451,10 +1590,13 @@ private void uploadStream() throws SnowflakeSQLException { uploadTask.get(); } catch (InterruptedException ex) { throw new SnowflakeSQLLoggedException( - session, ErrorCode.INTERRUPTED.getMessageCode(), SqlState.QUERY_CANCELED); + queryID, session, ErrorCode.INTERRUPTED.getMessageCode(), SqlState.QUERY_CANCELED); } catch (ExecutionException ex) { throw new SnowflakeSQLException( - ex.getCause(), SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode()); + queryID, + ex.getCause(), + SqlState.INTERNAL_ERROR, + ErrorCode.INTERNAL_ERROR.getMessageCode()); } logger.debug("Done with uploading from a stream"); } finally { @@ -1472,6 +1614,7 @@ public InputStream downloadStream(String fileName) throws SnowflakeSQLException logger.error("downloadStream function doesn't support local file system", false); throw new SnowflakeSQLException( + queryID, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), session, @@ -1498,7 +1641,8 @@ public InputStream downloadStream(String fileName) throws SnowflakeSQLException remoteLocation.location, stageFilePath, stageInfo.getRegion(), - presignedUrl); + presignedUrl, + queryID); } /** Helper to download files from remote */ @@ -1535,7 +1679,8 @@ private void downloadFiles() throws SnowflakeSQLException { command, parallel, encMat, - presignedUrl)); + presignedUrl, + queryID)); logger.debug("submitted download job for: {}", srcFile); } @@ -1547,7 +1692,7 @@ private void downloadFiles() throws SnowflakeSQLException { threadExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); } catch (InterruptedException ex) { throw new SnowflakeSQLLoggedException( - session, ErrorCode.INTERRUPTED.getMessageCode(), SqlState.QUERY_CANCELED); + queryID, session, ErrorCode.INTERRUPTED.getMessageCode(), SqlState.QUERY_CANCELED); } logger.debug("Done with downloading"); } finally { @@ -1630,7 +1775,8 @@ private void uploadFiles(Set fileList, int parallel) throws SnowflakeSQL false, (parallel > 1 ? 1 : this.parallel), srcFileObj, - encryptionMaterial.get(0))); + encryptionMaterial.get(0), + queryID)); logger.debug("submitted copy job for: {}", srcFile); } @@ -1643,7 +1789,7 @@ private void uploadFiles(Set fileList, int parallel) throws SnowflakeSQL threadExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); } catch (InterruptedException ex) { throw new SnowflakeSQLLoggedException( - session, ErrorCode.INTERRUPTED.getMessageCode(), SqlState.QUERY_CANCELED); + queryID, session, ErrorCode.INTERRUPTED.getMessageCode(), SqlState.QUERY_CANCELED); } logger.debug("Done with uploading"); @@ -1692,7 +1838,8 @@ public void cancel() { * @return a set of file names that is matched * @throws SnowflakeSQLException if cannot find the file */ - static Set expandFileNames(String[] filePathList) throws SnowflakeSQLException { + static Set expandFileNames(String[] filePathList, String queryId) + throws SnowflakeSQLException { Set result = new HashSet(); // a location to file pattern map so that we only need to list the @@ -1773,6 +1920,7 @@ static Set expandFileNames(String[] filePathList) throws SnowflakeSQLExc } } catch (Exception ex) { throw new SnowflakeSQLException( + queryId, ex, SqlState.DATA_EXCEPTION, ErrorCode.FAIL_LIST_FILES.getMessageCode(), @@ -1800,7 +1948,8 @@ private static boolean pushFileToLocal( String destFileName, InputStream inputStream, FileBackedOutputStream fileBackedOutStr, - SFBaseSession session) + SFBaseSession session, + String queryId) throws SQLException { // replace ~ with user home @@ -1821,6 +1970,7 @@ private static boolean pushFileToLocal( FileUtils.copyInputStreamToFile(inputStream, destFile); } catch (Exception ex) { throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), @@ -1836,7 +1986,8 @@ private static boolean pullFileFromLocal( String filePath, String destLocation, String destFileName, - SFBaseSession session) + SFBaseSession session, + String queryId) throws SQLException { try { logger.debug( @@ -1851,6 +2002,7 @@ private static boolean pullFileFromLocal( FileUtils.copyFileToDirectory(srcFile, new File(destLocation)); } catch (Exception ex) { throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), @@ -1877,7 +2029,8 @@ private static void pushFileToRemoteStore( boolean uploadFromStream, RemoteStoreFileEncryptionMaterial encMat, String streamingIngestClientName, - String streamingIngestClientKey) + String streamingIngestClientKey, + String queryId) throws SQLException, IOException { remoteLocation remoteLocation = extractLocationAndPath(stage.getLocation()); @@ -1936,7 +2089,8 @@ private static void pushFileToRemoteStore( fileBackedOutStr, meta, stage.getRegion(), - presignedUrl); + presignedUrl, + queryId); } finally { if (uploadFromStream && inputStream != null) { inputStream.close(); @@ -1994,8 +2148,8 @@ public static void uploadWithoutConnection(SnowflakeFileTransferConfig config) t if (requireCompress) { InputStreamWithMetadata compressedSizeAndStream = (encMat == null - ? compressStreamWithGZIPNoDigest(uploadStream, /* session = */ null) - : compressStreamWithGZIP(uploadStream, /* session = */ null)); + ? compressStreamWithGZIPNoDigest(uploadStream, /* session= */ null, null) + : compressStreamWithGZIP(uploadStream, /* session= */ null, encMat.getQueryId())); fileBackedOutputStream = compressedSizeAndStream.fileBackedOutputStream; @@ -2031,13 +2185,14 @@ public static void uploadWithoutConnection(SnowflakeFileTransferConfig config) t uploadSize); SnowflakeStorageClient initialClient = - storageFactory.createClient(stageInfo, 1, encMat, /*session = */ null); + storageFactory.createClient(stageInfo, 1, encMat, /* session= */ null); // Normal flow will never hit here. This is only for testing purposes if (isInjectedFileTransferExceptionEnabled()) { throw (Exception) SnowflakeFileTransferAgent.injectedFileTransferException; } + String queryId = encMat != null && encMat.getQueryId() != null ? encMat.getQueryId() : null; switch (stageInfo.getStageType()) { case S3: case AZURE: @@ -2057,7 +2212,8 @@ public static void uploadWithoutConnection(SnowflakeFileTransferConfig config) t (fileToUpload == null), encMat, streamingIngestClientName, - streamingIngestClientKey); + streamingIngestClientKey, + queryId); break; case GCS: // If the down-scoped token is used to upload file, one metadata may be used to upload @@ -2084,7 +2240,8 @@ public static void uploadWithoutConnection(SnowflakeFileTransferConfig config) t encMat, metadata.getPresignedUrl(), streamingIngestClientName, - streamingIngestClientKey); + streamingIngestClientKey, + queryId); break; } } catch (Exception ex) { @@ -2124,7 +2281,8 @@ private static void pushFileToRemoteStoreWithPresignedUrl( RemoteStoreFileEncryptionMaterial encMat, String presignedUrl, String streamingIngestClientName, - String streamingIngestClientKey) + String streamingIngestClientKey, + String queryId) throws SQLException, IOException { remoteLocation remoteLocation = extractLocationAndPath(stage.getLocation()); @@ -2169,7 +2327,8 @@ private static void pushFileToRemoteStoreWithPresignedUrl( fileBackedOutStr, meta, stage.getRegion(), - presignedUrl); + presignedUrl, + queryId); } finally { if (uploadFromStream && inputStream != null) { inputStream.close(); @@ -2192,7 +2351,8 @@ public static void renewExpiredToken( throws SnowflakeSQLException { SFStatement statement = new SFStatement(session); JsonNode jsonNode = parseCommandInGS(statement, command); - Map stageCredentials = extractStageCreds(jsonNode); + String queryId = jsonNode.path("data").path("queryId").asText(); + Map stageCredentials = extractStageCreds(jsonNode, queryId); // renew client with the fresh token logger.debug("Renewing expired access token"); @@ -2209,7 +2369,8 @@ private static void pullFileFromRemoteStore( String command, int parallel, RemoteStoreFileEncryptionMaterial encMat, - String presignedUrl) + String presignedUrl, + String queryId) throws SQLException { remoteLocation remoteLocation = extractLocationAndPath(stage.getLocation()); @@ -2236,7 +2397,8 @@ private static void pullFileFromRemoteStore( remoteLocation.location, stageFilePath, stage.getRegion(), - presignedUrl); + presignedUrl, + queryId); } /** @@ -2339,7 +2501,8 @@ private void filterExistingFiles() throws SnowflakeSQLException { (Exception) ex.getCause(); // Cause of StorageProviderException is always an Exception } - storageClient.handleStorageException(ex, ++retryCount, "listObjects", session, command); + storageClient.handleStorageException( + ex, ++retryCount, "listObjects", session, command, queryID); continue; } @@ -2359,7 +2522,7 @@ private void filterExistingFiles() throws SnowflakeSQLException { ex.getCause(); // Cause of StorageProviderException is always an Exception } storageClient.handleStorageException( - ex, ++retryCount, "compareRemoteFiles", session, command); + ex, ++retryCount, "compareRemoteFiles", session, command, queryID); } } while (retryCount <= storageClient.getMaxRetries()); } else if (stageInfo.getStageType() == StageInfo.StageType.LOCAL_FS) { @@ -2418,7 +2581,7 @@ private void filterExistingFiles() throws SnowflakeSQLException { if (fileMetadataMap.get(mappedSrcFile).requireCompress) { logger.debug("Compressing stream for digest check"); - InputStreamWithMetadata res = compressStreamWithGZIP(localFileStream, session); + InputStreamWithMetadata res = compressStreamWithGZIP(localFileStream, session, queryID); fileBackedOutputStreams.add(res.fileBackedOutputStream); localFileStream = res.fileBackedOutputStream.asByteSource().openStream(); @@ -2429,6 +2592,7 @@ private void filterExistingFiles() throws SnowflakeSQLException { fileBackedOutputStreams.add(res.fileBackedOutputStream); } catch (IOException | NoSuchAlgorithmException ex) { throw new SnowflakeSQLLoggedException( + queryID, session, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), @@ -2459,6 +2623,7 @@ private void filterExistingFiles() throws SnowflakeSQLException { } catch (IOException | NoSuchAlgorithmException ex) { throw new SnowflakeSQLLoggedException( + queryID, session, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), @@ -2600,7 +2765,7 @@ private void compareAndSkipRemoteFiles( if (fileMetadataMap.get(mappedSrcFile).requireCompress) { logger.debug("Compressing stream for digest check"); - InputStreamWithMetadata res = compressStreamWithGZIP(fileStream, session); + InputStreamWithMetadata res = compressStreamWithGZIP(fileStream, session, queryID); fileStream = res.fileBackedOutputStream.asByteSource().openStream(); fileBackedOutputStreams.add(res.fileBackedOutputStream); @@ -2655,6 +2820,7 @@ private void compareAndSkipRemoteFiles( } } catch (IOException | NoSuchAlgorithmException ex) { throw new SnowflakeSQLLoggedException( + queryID, session, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), @@ -2713,6 +2879,7 @@ private void initFileMetadata() throws SnowflakeSQLException { logger.debug("File doesn't exist: {}", sourceFile); throw new SnowflakeSQLLoggedException( + queryID, session, ErrorCode.FILE_NOT_FOUND.getMessageCode(), SqlState.DATA_EXCEPTION, @@ -2721,6 +2888,7 @@ private void initFileMetadata() throws SnowflakeSQLException { logger.debug("Not a file, but directory: {}", sourceFile); throw new SnowflakeSQLLoggedException( + queryID, session, ErrorCode.FILE_IS_DIRECTORY.getMessageCode(), SqlState.DATA_EXCEPTION, @@ -2786,6 +2954,7 @@ private void processFileCompressionTypes() throws SnowflakeSQLException { FileCompressionType.lookupByMimeSubType(sourceCompression.toLowerCase()); if (!foundCompType.isPresent()) { throw new SnowflakeSQLLoggedException( + queryID, session, ErrorCode.COMPRESSION_TYPE_NOT_KNOWN.getMessageCode(), SqlState.FEATURE_NOT_SUPPORTED, @@ -2795,6 +2964,7 @@ private void processFileCompressionTypes() throws SnowflakeSQLException { if (!userSpecifiedSourceCompression.isSupported()) { throw new SnowflakeSQLLoggedException( + queryID, session, ErrorCode.COMPRESSION_TYPE_NOT_SUPPORTED.getMessageCode(), SqlState.FEATURE_NOT_SUPPORTED, @@ -2880,6 +3050,7 @@ private void processFileCompressionTypes() throws SnowflakeSQLException { } else { // error if not supported throw new SnowflakeSQLLoggedException( + queryID, session, ErrorCode.COMPRESSION_TYPE_NOT_SUPPORTED.getMessageCode(), SqlState.FEATURE_NOT_SUPPORTED, @@ -3073,15 +3244,30 @@ public CommandType getCommandType() { return commandType; } - /* - * Handles an InvalidKeyException which indicates that the JCE component - * is not installed properly + /** + * Handles an InvalidKeyException which indicates that the JCE component is not installed properly + * + * @deprecated use {@link #throwJCEMissingError(String, Exception, String)} * @param operation a string indicating the operation type, e.g. upload/download * @param ex The exception to be handled - * @throws throws the error as a SnowflakeSQLException + * @throws SnowflakeSQLException throws the error as a SnowflakeSQLException */ + @Deprecated public static void throwJCEMissingError(String operation, Exception ex) throws SnowflakeSQLException { + throwJCEMissingError(operation, ex, null); + } + + /** + * Handles an InvalidKeyException which indicates that the JCE component is not installed properly + * + * @param operation a string indicating the operation type, e.g. upload/download + * @param ex The exception to be handled + * @param queryId last query id if available + * @throws SnowflakeSQLException throws the error as a SnowflakeSQLException + */ + public static void throwJCEMissingError(String operation, Exception ex, String queryId) + throws SnowflakeSQLException { // Most likely cause: Unlimited strength policy files not installed String msg = "Strong encryption with Java JRE requires JCE " @@ -3101,23 +3287,46 @@ public static void throwJCEMissingError(String operation, Exception ex) logger.error(msg); } throw new SnowflakeSQLException( - ex, SqlState.SYSTEM_ERROR, ErrorCode.AWS_CLIENT_ERROR.getMessageCode(), operation, msg); + queryId, + ex, + SqlState.SYSTEM_ERROR, + ErrorCode.AWS_CLIENT_ERROR.getMessageCode(), + operation, + msg); } /** * For handling IOException: No space left on device when attempting to download a file to a * location where there is not enough space. We don't want to retry on this exception. * + * @deprecated use {@link #throwNoSpaceLeftError(SFSession, String, Exception, String)} * @param session the current session * @param operation the operation i.e. GET * @param ex the exception caught * @throws SnowflakeSQLLoggedException */ + @Deprecated public static void throwNoSpaceLeftError(SFSession session, String operation, Exception ex) throws SnowflakeSQLLoggedException { + throwNoSpaceLeftError(session, operation, ex, null); + } + + /** + * For handling IOException: No space left on device when attempting to download a file to a + * location where there is not enough space. We don't want to retry on this exception. + * + * @param session the current session + * @param operation the operation i.e. GET + * @param ex the exception caught + * @throws SnowflakeSQLLoggedException + */ + public static void throwNoSpaceLeftError( + SFSession session, String operation, Exception ex, String queryId) + throws SnowflakeSQLLoggedException { String exMessage = SnowflakeUtil.getRootCause(ex).getMessage(); if (exMessage != null && exMessage.equals(NO_SPACE_LEFT_ON_DEVICE_ERR)) { throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.SYSTEM_ERROR, ErrorCode.IO_ERROR.getMessageCode(), diff --git a/src/main/java/net/snowflake/client/jdbc/SnowflakeSQLException.java b/src/main/java/net/snowflake/client/jdbc/SnowflakeSQLException.java index 6d5ca18ed..00e8a3b64 100644 --- a/src/main/java/net/snowflake/client/jdbc/SnowflakeSQLException.java +++ b/src/main/java/net/snowflake/client/jdbc/SnowflakeSQLException.java @@ -51,18 +51,24 @@ public SnowflakeSQLException(String queryId, String reason, String sqlState, int queryId); } - public SnowflakeSQLException(String reason, String SQLState) { - super(reason, SQLState); + public SnowflakeSQLException(String reason, String sqlState) { + super(reason, sqlState); // log user error from GS at fine level - logger.debug("Snowflake exception: {}, sqlState:{}", reason, SQLState); + logger.debug("Snowflake exception: {}, sqlState:{}", reason, sqlState); } + /** use {@link SnowflakeSQLException#SnowflakeSQLException(String, String, int)} */ + @Deprecated public SnowflakeSQLException(String sqlState, int vendorCode) { + this((String) null, sqlState, vendorCode); + } + + public SnowflakeSQLException(String queryId, String sqlState, int vendorCode) { super( errorResourceBundleManager.getLocalizedMessage(String.valueOf(vendorCode)), sqlState, vendorCode); - + this.queryId = queryId; logger.debug( "Snowflake exception: {}, sqlState:{}, vendorCode:{}", errorResourceBundleManager.getLocalizedMessage(String.valueOf(vendorCode)), @@ -70,12 +76,18 @@ public SnowflakeSQLException(String sqlState, int vendorCode) { vendorCode); } + /** use {@link SnowflakeSQLException#SnowflakeSQLException(String, String, int, Object...)} */ + @Deprecated public SnowflakeSQLException(String sqlState, int vendorCode, Object... params) { + this((String) null, sqlState, vendorCode, params); + } + + public SnowflakeSQLException(String queryId, String sqlState, int vendorCode, Object... params) { super( errorResourceBundleManager.getLocalizedMessage(String.valueOf(vendorCode), params), sqlState, vendorCode); - + this.queryId = queryId; logger.debug( "Snowflake exception: {}, sqlState:{}, vendorCode:{}", errorResourceBundleManager.getLocalizedMessage(String.valueOf(vendorCode), params), @@ -100,12 +112,23 @@ public SnowflakeSQLException(Throwable ex, ErrorCode errorCode, Object... params this(ex, errorCode.getSqlState(), errorCode.getMessageCode(), params); } + /** + * @deprecated use {@link SnowflakeSQLException#SnowflakeSQLException(String, Throwable, String, + * int, Object...)} + */ + @Deprecated public SnowflakeSQLException(Throwable ex, String sqlState, int vendorCode, Object... params) { + this(null, ex, sqlState, vendorCode, params); + } + + public SnowflakeSQLException( + String queryId, Throwable ex, String sqlState, int vendorCode, Object... params) { super( errorResourceBundleManager.getLocalizedMessage(String.valueOf(vendorCode), params), sqlState, vendorCode, ex); + this.queryId = queryId; logger.debug( "Snowflake exception: " @@ -121,6 +144,15 @@ public SnowflakeSQLException(ErrorCode errorCode, Object... params) { errorCode.getMessageCode()); } + public SnowflakeSQLException(String queryId, ErrorCode errorCode, Object... params) { + super( + errorResourceBundleManager.getLocalizedMessage( + String.valueOf(errorCode.getMessageCode()), params), + errorCode.getSqlState(), + errorCode.getMessageCode()); + this.queryId = queryId; + } + public SnowflakeSQLException( ErrorCode errorCode, int retryCount, boolean issocketTimeoutNoBackoff, long elapsedSeconds) { super( diff --git a/src/main/java/net/snowflake/client/jdbc/SnowflakeSQLLoggedException.java b/src/main/java/net/snowflake/client/jdbc/SnowflakeSQLLoggedException.java index b776be26c..d9d741a8c 100644 --- a/src/main/java/net/snowflake/client/jdbc/SnowflakeSQLLoggedException.java +++ b/src/main/java/net/snowflake/client/jdbc/SnowflakeSQLLoggedException.java @@ -232,6 +232,12 @@ public SnowflakeSQLLoggedException(SFBaseSession session, int vendorCode, String sendTelemetryData(null, SQLState, vendorCode, session, this); } + public SnowflakeSQLLoggedException( + String queryId, SFBaseSession session, int vendorCode, String SQLState) { + super(queryId, SQLState, vendorCode); + sendTelemetryData(queryId, SQLState, vendorCode, session, this); + } + public SnowflakeSQLLoggedException(SFBaseSession session, String SQLState, String reason) { super(reason, SQLState); sendTelemetryData(null, SQLState, -1, session, this); @@ -239,40 +245,45 @@ public SnowflakeSQLLoggedException(SFBaseSession session, String SQLState, Strin public SnowflakeSQLLoggedException( SFBaseSession session, int vendorCode, String SQLState, Object... params) { - super(SQLState, vendorCode, params); - String reason = - errorResourceBundleManager.getLocalizedMessage(String.valueOf(vendorCode), params); - sendTelemetryData(null, SQLState, vendorCode, session, this); + this(null, session, vendorCode, SQLState, params); + } + + public SnowflakeSQLLoggedException( + String queryId, SFBaseSession session, int vendorCode, String SQLState, Object... params) { + super(queryId, SQLState, vendorCode, params); + sendTelemetryData(queryId, SQLState, vendorCode, session, this); } public SnowflakeSQLLoggedException( SFBaseSession session, ErrorCode errorCode, Throwable ex, Object... params) { super(ex, errorCode, params); - // add telemetry sendTelemetryData(null, errorCode.getSqlState(), errorCode.getMessageCode(), session, this); } public SnowflakeSQLLoggedException( SFBaseSession session, String SQLState, int vendorCode, Throwable ex, Object... params) { super(ex, SQLState, vendorCode, params); - // add telemetry - String reason = - errorResourceBundleManager.getLocalizedMessage(String.valueOf(vendorCode), params); sendTelemetryData(null, SQLState, vendorCode, session, this); } + public SnowflakeSQLLoggedException( + String queryId, + SFBaseSession session, + String SQLState, + int vendorCode, + Throwable ex, + Object... params) { + super(queryId, ex, SQLState, vendorCode, params); + sendTelemetryData(queryId, SQLState, vendorCode, session, this); + } + public SnowflakeSQLLoggedException(SFBaseSession session, ErrorCode errorCode, Object... params) { super(errorCode, params); - // add telemetry - String reason = - errorResourceBundleManager.getLocalizedMessage( - String.valueOf(errorCode.getMessageCode()), params); sendTelemetryData(null, null, -1, session, this); } public SnowflakeSQLLoggedException(SFBaseSession session, SFException e) { super(e); - // add telemetry sendTelemetryData(null, null, -1, session, this); } diff --git a/src/main/java/net/snowflake/client/jdbc/cloud/storage/QueryIdHelper.java b/src/main/java/net/snowflake/client/jdbc/cloud/storage/QueryIdHelper.java new file mode 100644 index 000000000..34520cabd --- /dev/null +++ b/src/main/java/net/snowflake/client/jdbc/cloud/storage/QueryIdHelper.java @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. + */ +package net.snowflake.client.jdbc.cloud.storage; + +import net.snowflake.common.core.RemoteStoreFileEncryptionMaterial; + +class QueryIdHelper { + static String queryIdFromEncMatOr(RemoteStoreFileEncryptionMaterial encMat, String queryId) { + return encMat != null && encMat.getQueryId() != null ? encMat.getQueryId() : queryId; + } +} diff --git a/src/main/java/net/snowflake/client/jdbc/cloud/storage/SnowflakeAzureClient.java b/src/main/java/net/snowflake/client/jdbc/cloud/storage/SnowflakeAzureClient.java index 7ff7f9e41..4622e7cb5 100644 --- a/src/main/java/net/snowflake/client/jdbc/cloud/storage/SnowflakeAzureClient.java +++ b/src/main/java/net/snowflake/client/jdbc/cloud/storage/SnowflakeAzureClient.java @@ -108,6 +108,7 @@ private void setupAzureClient( if (encryptionKeySize != 128 && encryptionKeySize != 192 && encryptionKeySize != 256) { throw new SnowflakeSQLLoggedException( + QueryIdHelper.queryIdFromEncMatOr(encMat, null), session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -272,6 +273,7 @@ public StorageObjectMetadata getObjectMetadata(String remoteStorageLocation, Str * @param stageFilePath stage file path * @param stageRegion region name where the stage persists * @param presignedUrl Unused in Azure + * @param queryId last query id * @throws SnowflakeSQLException download failure */ @Override @@ -284,7 +286,8 @@ public void download( String remoteStorageLocation, String stageFilePath, String stageRegion, - String presignedUrl) + String presignedUrl, + String queryId) throws SnowflakeSQLException { int retryCount = 0; do { @@ -305,7 +308,7 @@ public void download( // Get the user-defined BLOB metadata Map userDefinedMetadata = blob.getMetadata(); AbstractMap.SimpleEntry encryptionData = - parseEncryptionData(userDefinedMetadata.get(AZ_ENCRYPTIONDATAPROP)); + parseEncryptionData(userDefinedMetadata.get(AZ_ENCRYPTIONDATAPROP), queryId); String key = encryptionData.getKey(); String iv = encryptionData.getValue(); @@ -313,6 +316,7 @@ public void download( if (this.isEncrypting() && this.getEncryptionKeySize() <= 256) { if (key == null || iv == null) { throw new SnowflakeSQLLoggedException( + queryId, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -331,11 +335,12 @@ public void download( } catch (Exception ex) { logger.debug("Download unsuccessful {}", ex); - handleAzureException(ex, ++retryCount, "download", session, command, this); + handleAzureException(ex, ++retryCount, "download", session, command, this, queryId); } } while (retryCount <= getMaxRetries()); throw new SnowflakeSQLLoggedException( + queryId, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -352,6 +357,7 @@ public void download( * @param stageFilePath stage file path * @param stageRegion region name where the stage persists * @param presignedUrl Unused in Azure + * @param queryId last query id * @return input file stream * @throws SnowflakeSQLException when download failure */ @@ -363,7 +369,8 @@ public InputStream downloadToStream( String remoteStorageLocation, String stageFilePath, String stageRegion, - String presignedUrl) + String presignedUrl, + String queryId) throws SnowflakeSQLException { int retryCount = 0; @@ -378,7 +385,7 @@ public InputStream downloadToStream( Map userDefinedMetadata = blob.getMetadata(); AbstractMap.SimpleEntry encryptionData = - parseEncryptionData(userDefinedMetadata.get(AZ_ENCRYPTIONDATAPROP)); + parseEncryptionData(userDefinedMetadata.get(AZ_ENCRYPTIONDATAPROP), queryId); String key = encryptionData.getKey(); @@ -387,6 +394,7 @@ public InputStream downloadToStream( if (this.isEncrypting() && this.getEncryptionKeySize() <= 256) { if (key == null || iv == null) { throw new SnowflakeSQLLoggedException( + queryId, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -408,11 +416,12 @@ public InputStream downloadToStream( } catch (Exception ex) { logger.debug("Downloading unsuccessful {}", ex); - handleAzureException(ex, ++retryCount, "download", session, command, this); + handleAzureException(ex, ++retryCount, "download", session, command, this, queryId); } } while (retryCount < getMaxRetries()); throw new SnowflakeSQLLoggedException( + queryId, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -434,6 +443,7 @@ public InputStream downloadToStream( * @param meta object meta data * @param stageRegion region name where the stage persists * @param presignedUrl Unused in Azure + * @param queryId last query id * @throws SnowflakeSQLException if upload failed even after retry */ @Override @@ -449,7 +459,8 @@ public void upload( FileBackedOutputStream fileBackedOutputStream, StorageObjectMetadata meta, String stageRegion, - String presignedUrl) + String presignedUrl, + String queryId) throws SnowflakeSQLException { final List toClose = new ArrayList<>(); long originalContentLength = meta.getContentLength(); @@ -462,7 +473,8 @@ public void upload( meta, originalContentLength, fileBackedOutputStream, - toClose); + toClose, + queryId); if (!(meta instanceof CommonObjectMetadata)) { throw new IllegalArgumentException("Unexpected metadata object type"); @@ -497,10 +509,11 @@ public void upload( return; } catch (Exception ex) { - handleAzureException(ex, ++retryCount, "upload", session, command, this); + handleAzureException(ex, ++retryCount, "upload", session, command, this, queryId); if (uploadFromStream && fileBackedOutputStream == null) { throw new SnowflakeSQLException( + queryId, ex, SqlState.SYSTEM_ERROR, ErrorCode.IO_ERROR.getMessageCode(), @@ -516,7 +529,8 @@ public void upload( meta, originalContentLength, fileBackedOutputStream, - toClose); + toClose, + queryId); } } while (retryCount <= getMaxRetries()); @@ -524,6 +538,7 @@ public void upload( for (FileInputStream is : toClose) IOUtils.closeQuietly(is); throw new SnowflakeSQLException( + queryId, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), "Unexpected: upload unsuccessful without exception!"); @@ -538,13 +553,19 @@ public void upload( * exception was raised, for example "upload" * @param session the current SFSession object used by the client * @param command the command attempted at the time of the exception + * @param queryId last query id * @throws SnowflakeSQLException exceptions not handled */ @Override public void handleStorageException( - Exception ex, int retryCount, String operation, SFSession session, String command) + Exception ex, + int retryCount, + String operation, + SFSession session, + String command, + String queryId) throws SnowflakeSQLException { - handleAzureException(ex, retryCount, operation, session, command, this); + handleAzureException(ex, retryCount, operation, session, command, this, queryId); } private SFPair createUploadStream( @@ -554,7 +575,8 @@ private SFPair createUploadStream( StorageObjectMetadata meta, long originalContentLength, FileBackedOutputStream fileBackedOutputStream, - List toClose) + List toClose, + String queryId) throws SnowflakeSQLException { logger.debug( "createUploadStream({}, {}, {}, {}, {}, {})", @@ -586,6 +608,7 @@ private SFPair createUploadStream( } catch (Exception ex) { logger.error("Failed to encrypt input", ex); throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), @@ -609,6 +632,7 @@ private SFPair createUploadStream( } catch (FileNotFoundException ex) { logger.error("Failed to open input file", ex); throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), @@ -618,6 +642,7 @@ private SFPair createUploadStream( } catch (IOException ex) { logger.error("Failed to open input stream", ex); throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), @@ -648,20 +673,21 @@ private static void handleAzureException( String operation, SFSession session, String command, - SnowflakeAzureClient azClient) + SnowflakeAzureClient azClient, + String queryId) throws SnowflakeSQLException { // no need to retry if it is invalid key exception if (ex.getCause() instanceof InvalidKeyException) { // Most likely cause is that the unlimited strength policy files are not installed // Log the error and throw a message that explains the cause - SnowflakeFileTransferAgent.throwJCEMissingError(operation, ex); + SnowflakeFileTransferAgent.throwJCEMissingError(operation, ex, queryId); } // If there is no space left in the download location, java.io.IOException is thrown. // Don't retry. if (SnowflakeUtil.getRootCause(ex) instanceof IOException) { - SnowflakeFileTransferAgent.throwNoSpaceLeftError(session, operation, ex); + SnowflakeFileTransferAgent.throwNoSpaceLeftError(session, operation, ex, queryId); } if (ex instanceof StorageException) { @@ -675,6 +701,7 @@ private static void handleAzureException( } else { // If session is null we cannot renew the token so throw the ExpiredToken exception throw new SnowflakeSQLException( + queryId, se.getErrorCode(), CLOUD_STORAGE_CREDENTIALS_EXPIRED, "Azure credentials may have expired"); @@ -685,6 +712,7 @@ private static void handleAzureException( if (retryCount > azClient.getMaxRetries() || ((StorageException) ex).getHttpStatusCode() == 404) { throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.SYSTEM_ERROR, ErrorCode.AZURE_SERVICE_ERROR.getMessageCode(), @@ -728,6 +756,7 @@ private static void handleAzureException( || SnowflakeUtil.getRootCause(ex) instanceof SocketTimeoutException) { if (retryCount > azClient.getMaxRetries()) { throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.SYSTEM_ERROR, ErrorCode.IO_ERROR.getMessageCode(), @@ -742,6 +771,7 @@ private static void handleAzureException( } } else { throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.SYSTEM_ERROR, ErrorCode.IO_ERROR.getMessageCode(), @@ -822,7 +852,7 @@ private String buildEncryptionMetadataJSON(String iv64, String key64) { * Takes the json string in the encryptiondata metadata field of the encrypted * blob and parses out the key and iv. Returns the pair as key = key, iv = value. */ - private SimpleEntry parseEncryptionData(String jsonEncryptionData) + private SimpleEntry parseEncryptionData(String jsonEncryptionData, String queryId) throws SnowflakeSQLException { ObjectMapper mapper = ObjectMapperFactory.getObjectMapper(); JsonFactory factory = mapper.getFactory(); @@ -836,6 +866,7 @@ private SimpleEntry parseEncryptionData(String jsonEncryptionDat return new SimpleEntry(key, iv); } catch (Exception ex) { throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.SYSTEM_ERROR, ErrorCode.IO_ERROR.getMessageCode(), diff --git a/src/main/java/net/snowflake/client/jdbc/cloud/storage/SnowflakeGCSClient.java b/src/main/java/net/snowflake/client/jdbc/cloud/storage/SnowflakeGCSClient.java index 74435a571..40e8582cc 100644 --- a/src/main/java/net/snowflake/client/jdbc/cloud/storage/SnowflakeGCSClient.java +++ b/src/main/java/net/snowflake/client/jdbc/cloud/storage/SnowflakeGCSClient.java @@ -199,6 +199,7 @@ public StorageObjectMetadata getObjectMetadata(String remoteStorageLocation, Str * @param stageFilePath stage file path * @param stageRegion region name where the stage persists * @param presignedUrl Credential to use for download + * @param queryId last query id * @throws SnowflakeSQLException download failure */ @Override @@ -211,7 +212,8 @@ public void download( String remoteStorageLocation, String stageFilePath, String stageRegion, - String presignedUrl) + String presignedUrl, + String queryId) throws SnowflakeSQLException { int retryCount = 0; String localFilePath = localLocation + localFileSep + destFileName; @@ -271,7 +273,7 @@ public void download( .getName() .equalsIgnoreCase(GCS_METADATA_PREFIX + GCS_ENCRYPTIONDATAPROP)) { AbstractMap.SimpleEntry encryptionData = - parseEncryptionData(header.getValue()); + parseEncryptionData(header.getValue(), queryId); key = encryptionData.getKey(); iv = encryptionData.getValue(); @@ -282,14 +284,14 @@ public void download( logger.debug("Download successful", false); } catch (IOException ex) { logger.debug("Download unsuccessful {}", ex); - handleStorageException(ex, ++retryCount, "download", session, command); + handleStorageException(ex, ++retryCount, "download", session, command, queryId); } } else { Exception ex = new HttpResponseException( response.getStatusLine().getStatusCode(), EntityUtils.toString(response.getEntity())); - handleStorageException(ex, ++retryCount, "download", session, command); + handleStorageException(ex, ++retryCount, "download", session, command, queryId); } } else { BlobId blobId = BlobId.of(remoteStorageLocation, stageFilePath); @@ -311,7 +313,7 @@ public void download( if (isEncrypting()) { if (userDefinedMetadata != null) { AbstractMap.SimpleEntry encryptionData = - parseEncryptionData(userDefinedMetadata.get(GCS_ENCRYPTIONDATAPROP)); + parseEncryptionData(userDefinedMetadata.get(GCS_ENCRYPTIONDATAPROP), queryId); key = encryptionData.getKey(); iv = encryptionData.getValue(); @@ -325,6 +327,7 @@ public void download( && this.getEncryptionKeySize() <= 256) { if (key == null || iv == null) { throw new SnowflakeSQLLoggedException( + queryId, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -337,6 +340,7 @@ public void download( } catch (Exception ex) { logger.error("Error decrypting file", ex); throw new SnowflakeSQLLoggedException( + queryId, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -346,11 +350,12 @@ public void download( return; } catch (Exception ex) { logger.debug("Download unsuccessful {}", ex); - handleStorageException(ex, ++retryCount, "download", session, command); + handleStorageException(ex, ++retryCount, "download", session, command, queryId); } } while (retryCount <= getMaxRetries()); throw new SnowflakeSQLLoggedException( + queryId, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -367,6 +372,7 @@ public void download( * @param stageFilePath stage file path * @param stageRegion region name where the stage persists * @param presignedUrl Signed credential for download + * @param queryId last query id * @return input file stream * @throws SnowflakeSQLException when download failure */ @@ -378,7 +384,8 @@ public InputStream downloadToStream( String remoteStorageLocation, String stageFilePath, String stageRegion, - String presignedUrl) + String presignedUrl, + String queryId) throws SnowflakeSQLException { int retryCount = 0; InputStream inputStream = null; @@ -429,7 +436,7 @@ public InputStream downloadToStream( .getName() .equalsIgnoreCase(GCS_METADATA_PREFIX + GCS_ENCRYPTIONDATAPROP)) { AbstractMap.SimpleEntry encryptionData = - parseEncryptionData(header.getValue()); + parseEncryptionData(header.getValue(), queryId); key = encryptionData.getKey(); iv = encryptionData.getValue(); @@ -440,14 +447,14 @@ public InputStream downloadToStream( logger.debug("Download successful", false); } catch (IOException ex) { logger.debug("Download unsuccessful {}", ex); - handleStorageException(ex, ++retryCount, "download", session, command); + handleStorageException(ex, ++retryCount, "download", session, command, queryId); } } else { Exception ex = new HttpResponseException( response.getStatusLine().getStatusCode(), EntityUtils.toString(response.getEntity())); - handleStorageException(ex, ++retryCount, "download", session, command); + handleStorageException(ex, ++retryCount, "download", session, command, queryId); } } else { BlobId blobId = BlobId.of(remoteStorageLocation, stageFilePath); @@ -463,7 +470,7 @@ public InputStream downloadToStream( // Get the user-defined BLOB metadata Map userDefinedMetadata = blob.getMetadata(); AbstractMap.SimpleEntry encryptionData = - parseEncryptionData(userDefinedMetadata.get(GCS_ENCRYPTIONDATAPROP)); + parseEncryptionData(userDefinedMetadata.get(GCS_ENCRYPTIONDATAPROP), queryId); key = encryptionData.getKey(); iv = encryptionData.getValue(); @@ -473,6 +480,7 @@ public InputStream downloadToStream( if (this.isEncrypting() && this.getEncryptionKeySize() <= 256) { if (key == null || iv == null) { throw new SnowflakeSQLException( + queryId, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), "File metadata incomplete"); @@ -487,6 +495,7 @@ public InputStream downloadToStream( } catch (Exception ex) { logger.error("Error decrypting file", ex); throw new SnowflakeSQLLoggedException( + queryId, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -495,11 +504,12 @@ public InputStream downloadToStream( } } catch (Exception ex) { logger.debug("Download unsuccessful {}", ex); - handleStorageException(ex, ++retryCount, "download", session, command); + handleStorageException(ex, ++retryCount, "download", session, command, queryId); } } while (retryCount <= getMaxRetries()); throw new SnowflakeSQLLoggedException( + queryId, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -521,6 +531,7 @@ public InputStream downloadToStream( * @param meta object meta data * @param stageRegion region name where the stage persists * @param presignedUrl presigned URL for upload. Used by GCP. + * @param queryId last query id * @throws SnowflakeSQLException if upload failed */ @Override @@ -536,7 +547,8 @@ public void uploadWithPresignedUrlWithoutConnection( FileBackedOutputStream fileBackedOutputStream, StorageObjectMetadata meta, String stageRegion, - String presignedUrl) + String presignedUrl, + String queryId) throws SnowflakeSQLException { final List toClose = new ArrayList<>(); long originalContentLength = meta.getContentLength(); @@ -549,7 +561,8 @@ public void uploadWithPresignedUrlWithoutConnection( meta, originalContentLength, fileBackedOutputStream, - toClose); + toClose, + queryId); if (!(meta instanceof CommonObjectMetadata)) { throw new IllegalArgumentException("Unexpected metadata object type"); @@ -575,7 +588,8 @@ public void uploadWithPresignedUrlWithoutConnection( meta.getUserMetadata(), uploadStreamInfo.left, presignedUrl, - ocspModeAndProxyKey); + ocspModeAndProxyKey, + queryId); logger.debug("Upload successfully with presigned url"); } @@ -600,6 +614,7 @@ public void uploadWithPresignedUrlWithoutConnection( * @param meta object meta data * @param stageRegion region name where the stage persists * @param presignedUrl Credential used for upload of a file + * @param queryId last query id * @throws SnowflakeSQLException if upload failed even after retry */ @Override @@ -615,7 +630,8 @@ public void upload( FileBackedOutputStream fileBackedOutputStream, StorageObjectMetadata meta, String stageRegion, - String presignedUrl) + String presignedUrl, + String queryId) throws SnowflakeSQLException { final List toClose = new ArrayList<>(); long originalContentLength = meta.getContentLength(); @@ -628,7 +644,8 @@ public void upload( meta, originalContentLength, fileBackedOutputStream, - toClose); + toClose, + queryId); if (!(meta instanceof CommonObjectMetadata)) { throw new IllegalArgumentException("Unexpected metadata object type"); @@ -644,7 +661,8 @@ public void upload( meta.getUserMetadata(), uploadStreamInfo.left, presignedUrl, - session.getHttpClientKey()); + session.getHttpClientKey(), + queryId); logger.debug("Upload successful", false); // close any open streams in the "toClose" list and return @@ -673,10 +691,11 @@ public void upload( return; } catch (Exception ex) { - handleStorageException(ex, ++retryCount, "upload", session, command); + handleStorageException(ex, ++retryCount, "upload", session, command, queryId); if (uploadFromStream && fileBackedOutputStream == null) { throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.SYSTEM_ERROR, ErrorCode.IO_ERROR.getMessageCode(), @@ -693,7 +712,8 @@ public void upload( meta, originalContentLength, fileBackedOutputStream, - toClose); + toClose, + queryId); } } while (retryCount <= getMaxRetries()); @@ -701,6 +721,7 @@ public void upload( for (FileInputStream is : toClose) IOUtils.closeQuietly(is); throw new SnowflakeSQLLoggedException( + queryId, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -752,7 +773,8 @@ private void uploadWithPresignedUrl( Map metadata, InputStream content, String presignedUrl, - HttpClientSettingsKey ocspAndProxyKey) + HttpClientSettingsKey ocspAndProxyKey, + String queryId) throws SnowflakeSQLException { try { URIBuilder uriBuilder = new URIBuilder(presignedUrl); @@ -806,16 +828,18 @@ private void uploadWithPresignedUrl( new HttpResponseException( response.getStatusLine().getStatusCode(), EntityUtils.toString(response.getEntity())); - handleStorageException(ex, 0, "upload", session, null); + handleStorageException(ex, 0, "upload", session, null, queryId); } } catch (URISyntaxException e) { throw new SnowflakeSQLLoggedException( + queryId, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, "Unexpected: upload presigned URL invalid"); } catch (Exception e) { throw new SnowflakeSQLLoggedException( + queryId, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -845,7 +869,8 @@ private SFPair createUploadStream( StorageObjectMetadata meta, long originalContentLength, FileBackedOutputStream fileBackedOutputStream, - List toClose) + List toClose, + String queryId) throws SnowflakeSQLException { logger.debug( "createUploadStream({}, {}, {}, {}, {}, {})", @@ -877,6 +902,7 @@ private SFPair createUploadStream( } catch (Exception ex) { logger.error("Failed to encrypt input", ex); throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), @@ -900,6 +926,7 @@ private SFPair createUploadStream( } catch (FileNotFoundException ex) { logger.error("Failed to open input file", ex); throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), @@ -909,6 +936,7 @@ private SFPair createUploadStream( } catch (IOException ex) { logger.error("Failed to open input stream", ex); throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), @@ -922,19 +950,24 @@ private SFPair createUploadStream( @Override public void handleStorageException( - Exception ex, int retryCount, String operation, SFSession session, String command) + Exception ex, + int retryCount, + String operation, + SFSession session, + String command, + String queryId) throws SnowflakeSQLException { // no need to retry if it is invalid key exception if (ex.getCause() instanceof InvalidKeyException) { // Most likely cause is that the unlimited strength policy files are not installed // Log the error and throw a message that explains the cause - SnowflakeFileTransferAgent.throwJCEMissingError(operation, ex); + SnowflakeFileTransferAgent.throwJCEMissingError(operation, ex, queryId); } // If there is no space left in the download location, java.io.IOException is thrown. // Don't retry. if (SnowflakeUtil.getRootCause(ex) instanceof IOException) { - SnowflakeFileTransferAgent.throwNoSpaceLeftError(session, operation, ex); + SnowflakeFileTransferAgent.throwNoSpaceLeftError(session, operation, ex, queryId); } if (ex instanceof StorageException) { @@ -946,6 +979,7 @@ public void handleStorageException( // If we have exceeded the max number of retries, propagate the error if (retryCount > getMaxRetries()) { throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.SYSTEM_ERROR, ErrorCode.GCP_SERVICE_ERROR.getMessageCode(), @@ -984,7 +1018,10 @@ public void handleStorageException( SnowflakeFileTransferAgent.renewExpiredToken(session, command, this); } else { throw new SnowflakeSQLException( - se.getMessage(), CLOUD_STORAGE_CREDENTIALS_EXPIRED, "GCS credentials have expired"); + queryId, + se.getMessage(), + CLOUD_STORAGE_CREDENTIALS_EXPIRED, + "GCS credentials have expired"); } } } @@ -992,6 +1029,7 @@ public void handleStorageException( || SnowflakeUtil.getRootCause(ex) instanceof SocketTimeoutException) { if (retryCount > getMaxRetries()) { throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.SYSTEM_ERROR, ErrorCode.IO_ERROR.getMessageCode(), @@ -1006,6 +1044,7 @@ public void handleStorageException( } } else { throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.SYSTEM_ERROR, ErrorCode.IO_ERROR.getMessageCode(), @@ -1059,8 +1098,8 @@ private String buildEncryptionMetadataJSON(String iv64, String key64) { * Takes the json string in the encryptiondata metadata field of the encrypted * blob and parses out the key and iv. Returns the pair as key = key, iv = value. */ - private AbstractMap.SimpleEntry parseEncryptionData(String jsonEncryptionData) - throws SnowflakeSQLException { + private AbstractMap.SimpleEntry parseEncryptionData( + String jsonEncryptionData, String queryId) throws SnowflakeSQLException { ObjectMapper mapper = ObjectMapperFactory.getObjectMapper(); JsonFactory factory = mapper.getFactory(); try { @@ -1073,6 +1112,7 @@ private AbstractMap.SimpleEntry parseEncryptionData(String jsonE return new AbstractMap.SimpleEntry<>(key, iv); } catch (Exception ex) { throw new SnowflakeSQLException( + queryId, ex, SqlState.SYSTEM_ERROR, ErrorCode.IO_ERROR.getMessageCode(), @@ -1140,6 +1180,7 @@ private void setupGCSClient( if (encryptionKeySize != 128 && encryptionKeySize != 192 && encryptionKeySize != 256) { throw new SnowflakeSQLException( + QueryIdHelper.queryIdFromEncMatOr(encMat, null), SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), "unsupported key size", diff --git a/src/main/java/net/snowflake/client/jdbc/cloud/storage/SnowflakeS3Client.java b/src/main/java/net/snowflake/client/jdbc/cloud/storage/SnowflakeS3Client.java index 6aed5d317..86c571ee2 100644 --- a/src/main/java/net/snowflake/client/jdbc/cloud/storage/SnowflakeS3Client.java +++ b/src/main/java/net/snowflake/client/jdbc/cloud/storage/SnowflakeS3Client.java @@ -174,6 +174,7 @@ private void setupSnowflakeS3Client( .withClientConfiguration(clientConfig); } else { throw new SnowflakeSQLLoggedException( + QueryIdHelper.queryIdFromEncMatOr(encMat, null), session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -294,6 +295,7 @@ public StorageObjectMetadata getObjectMetadata(String remoteStorageLocation, Str * @param stageFilePath stage file path * @param stageRegion region name where the stage persists * @param presignedUrl Not used in S3 + * @param queryId last query id * @throws SnowflakeSQLException if download failed without an exception * @throws SnowflakeSQLException if failed to decrypt downloaded file * @throws SnowflakeSQLException if file metadata is incomplete @@ -308,7 +310,8 @@ public void download( String remoteStorageLocation, String stageFilePath, String stageRegion, - String presignedUrl) + String presignedUrl, + String queryId) throws SnowflakeSQLException { TransferManager tx = null; int retryCount = 0; @@ -347,6 +350,7 @@ public ExecutorService newExecutor() { if (this.isEncrypting() && this.getEncryptionKeySize() < 256) { if (key == null || iv == null) { throw new SnowflakeSQLLoggedException( + queryId, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -365,7 +369,7 @@ public ExecutorService newExecutor() { return; } catch (Exception ex) { - handleS3Exception(ex, ++retryCount, "download", session, command, this); + handleS3Exception(ex, ++retryCount, "download", session, command, this, queryId); } finally { if (tx != null) { @@ -375,6 +379,7 @@ public ExecutorService newExecutor() { } while (retryCount <= getMaxRetries()); throw new SnowflakeSQLLoggedException( + queryId, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -391,6 +396,7 @@ public ExecutorService newExecutor() { * @param stageFilePath stage file path * @param stageRegion region name where the stage persists * @param presignedUrl Not used in S3 + * @param queryId last query id * @return input file stream * @throws SnowflakeSQLException when download failure */ @@ -402,7 +408,8 @@ public InputStream downloadToStream( String remoteStorageLocation, String stageFilePath, String stageRegion, - String presignedUrl) + String presignedUrl, + String queryId) throws SnowflakeSQLException { int retryCount = 0; do { @@ -421,6 +428,7 @@ public InputStream downloadToStream( if (this.isEncrypting() && this.getEncryptionKeySize() < 256) { if (key == null || iv == null) { throw new SnowflakeSQLLoggedException( + queryId, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -439,11 +447,12 @@ public InputStream downloadToStream( return stream; } } catch (Exception ex) { - handleS3Exception(ex, ++retryCount, "download", session, command, this); + handleS3Exception(ex, ++retryCount, "download", session, command, this, queryId); } } while (retryCount <= getMaxRetries()); throw new SnowflakeSQLLoggedException( + queryId, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -465,6 +474,7 @@ public InputStream downloadToStream( * @param meta object meta data * @param stageRegion region name where the stage persists * @param presignedUrl Not used in S3 + * @param queryId last query id * @throws SnowflakeSQLException if upload failed even after retry */ @Override @@ -480,7 +490,8 @@ public void upload( FileBackedOutputStream fileBackedOutputStream, StorageObjectMetadata meta, String stageRegion, - String presignedUrl) + String presignedUrl, + String queryId) throws SnowflakeSQLException { final long originalContentLength = meta.getContentLength(); final List toClose = new ArrayList<>(); @@ -492,7 +503,8 @@ public void upload( fileBackedOutputStream, ((S3ObjectMetadata) meta).getS3ObjectMetadata(), originalContentLength, - toClose); + toClose, + queryId); ObjectMetadata s3Meta; if (meta instanceof S3ObjectMetadata) { @@ -548,9 +560,10 @@ public ExecutorService newExecutor() { return; } catch (Exception ex) { - handleS3Exception(ex, ++retryCount, "upload", session, command, this); + handleS3Exception(ex, ++retryCount, "upload", session, command, this, queryId); if (uploadFromStream && fileBackedOutputStream == null) { throw new SnowflakeSQLException( + queryId, ex, SqlState.SYSTEM_ERROR, ErrorCode.IO_ERROR.getMessageCode(), @@ -566,7 +579,8 @@ public ExecutorService newExecutor() { fileBackedOutputStream, s3Meta, originalContentLength, - toClose); + toClose, + queryId); } finally { if (tx != null) { tx.shutdownNow(false); @@ -577,6 +591,7 @@ public ExecutorService newExecutor() { for (FileInputStream is : toClose) IOUtils.closeQuietly(is); throw new SnowflakeSQLLoggedException( + queryId, session, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -590,7 +605,8 @@ private SFPair createUploadStream( FileBackedOutputStream fileBackedOutputStream, ObjectMetadata meta, long originalContentLength, - List toClose) + List toClose, + String queryId) throws SnowflakeSQLException { logger.debug( "createUploadStream({}, {}, {}, {}, {}, {}, {}) " + "keySize={}", @@ -623,6 +639,7 @@ private SFPair createUploadStream( } catch (Exception ex) { logger.error("Failed to encrypt input", ex); throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), @@ -649,6 +666,7 @@ private SFPair createUploadStream( } catch (FileNotFoundException ex) { logger.error("Failed to open input file", ex); throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), @@ -658,6 +676,7 @@ private SFPair createUploadStream( } catch (IOException ex) { logger.error("Failed to open input stream", ex); throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR.getMessageCode(), @@ -671,9 +690,14 @@ private SFPair createUploadStream( @Override public void handleStorageException( - Exception ex, int retryCount, String operation, SFSession session, String command) + Exception ex, + int retryCount, + String operation, + SFSession session, + String command, + String queryId) throws SnowflakeSQLException { - handleS3Exception(ex, retryCount, operation, session, command, this); + handleS3Exception(ex, retryCount, operation, session, command, this, queryId); } private static void handleS3Exception( @@ -682,19 +706,20 @@ private static void handleS3Exception( String operation, SFSession session, String command, - SnowflakeS3Client s3Client) + SnowflakeS3Client s3Client, + String queryId) throws SnowflakeSQLException { // no need to retry if it is invalid key exception if (ex.getCause() instanceof InvalidKeyException) { // Most likely cause is that the unlimited strength policy files are not installed // Log the error and throw a message that explains the cause - SnowflakeFileTransferAgent.throwJCEMissingError(operation, ex); + SnowflakeFileTransferAgent.throwJCEMissingError(operation, ex, queryId); } // If there is no space left in the download location, java.io.IOException is thrown. // Don't retry. if (SnowflakeUtil.getRootCause(ex) instanceof IOException) { - SnowflakeFileTransferAgent.throwNoSpaceLeftError(session, operation, ex); + SnowflakeFileTransferAgent.throwNoSpaceLeftError(session, operation, ex, queryId); } // Don't retry if max retries has been reached or the error code is 404/400 @@ -718,6 +743,7 @@ private static void handleS3Exception( SnowflakeFileTransferAgent.renewExpiredToken(session, command, s3Client); } else { throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.SYSTEM_ERROR, ErrorCode.S3_OPERATION_ERROR.getMessageCode(), @@ -732,6 +758,7 @@ private static void handleS3Exception( } else { throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.SYSTEM_ERROR, ErrorCode.AWS_CLIENT_ERROR.getMessageCode(), @@ -772,6 +799,7 @@ private static void handleS3Exception( SnowflakeFileTransferAgent.renewExpiredToken(session, command, s3Client); } else { throw new SnowflakeSQLException( + queryId, s3ex.getErrorCode(), CLOUD_STORAGE_CREDENTIALS_EXPIRED, "S3 credentials have expired"); @@ -784,6 +812,7 @@ private static void handleS3Exception( || SnowflakeUtil.getRootCause(ex) instanceof SocketTimeoutException) { if (retryCount > s3Client.getMaxRetries()) { throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.SYSTEM_ERROR, ErrorCode.IO_ERROR.getMessageCode(), @@ -798,6 +827,7 @@ private static void handleS3Exception( } } else { throw new SnowflakeSQLLoggedException( + queryId, session, SqlState.SYSTEM_ERROR, ErrorCode.IO_ERROR.getMessageCode(), diff --git a/src/main/java/net/snowflake/client/jdbc/cloud/storage/SnowflakeStorageClient.java b/src/main/java/net/snowflake/client/jdbc/cloud/storage/SnowflakeStorageClient.java index 90eea4cd0..1bd4127f6 100644 --- a/src/main/java/net/snowflake/client/jdbc/cloud/storage/SnowflakeStorageClient.java +++ b/src/main/java/net/snowflake/client/jdbc/cloud/storage/SnowflakeStorageClient.java @@ -90,6 +90,8 @@ StorageObjectMetadata getObjectMetadata(String remoteStorageLocation, String pre /** * Download a file from remote storage. * + * @deprecated use {@link #download(SFSession, String, String, String, int, String, String, + * String, String, String)} * @param connection connection object * @param command command to download file * @param localLocation local file path @@ -101,7 +103,8 @@ StorageObjectMetadata getObjectMetadata(String remoteStorageLocation, String pre * @param presignedUrl presigned URL for download. Used by GCP. * @throws SnowflakeSQLException download failure */ - void download( + @Deprecated + default void download( SFSession connection, String command, String localLocation, @@ -111,11 +114,53 @@ void download( String stageFilePath, String stageRegion, String presignedUrl) + throws SnowflakeSQLException { + download( + connection, + command, + localLocation, + destFileName, + parallelism, + remoteStorageLocation, + stageFilePath, + stageRegion, + presignedUrl, + null); + } + + /** + * Download a file from remote storage. + * + * @param connection connection object + * @param command command to download file + * @param localLocation local file path + * @param destFileName destination file name + * @param parallelism number of threads for parallel downloading + * @param remoteStorageLocation remote storage location, i.e. bucket for S3 + * @param stageFilePath stage file path + * @param stageRegion region name where the stage persists + * @param presignedUrl presigned URL for download. Used by GCP. + * @param queryId last query id + * @throws SnowflakeSQLException download failure + */ + void download( + SFSession connection, + String command, + String localLocation, + String destFileName, + int parallelism, + String remoteStorageLocation, + String stageFilePath, + String stageRegion, + String presignedUrl, + String queryId) throws SnowflakeSQLException; /** * Download a file from remote storage * + * @deprecated use {@link #download(SFSession, String, String, String, int, String, String, + * String, String, String)} * @param connection connection object * @param command command to download file * @param parallelism number of threads for parallel downloading @@ -126,7 +171,8 @@ void download( * @return input file stream * @throws SnowflakeSQLException when download failure */ - InputStream downloadToStream( + @Deprecated + default InputStream downloadToStream( SFSession connection, String command, int parallelism, @@ -134,11 +180,48 @@ InputStream downloadToStream( String stageFilePath, String stageRegion, String presignedUrl) + throws SnowflakeSQLException { + return downloadToStream( + connection, + command, + parallelism, + remoteStorageLocation, + stageFilePath, + stageRegion, + presignedUrl, + null); + } + + /** + * Download a file from remote storage + * + * @param connection connection object + * @param command command to download file + * @param parallelism number of threads for parallel downloading + * @param remoteStorageLocation remote storage location, i.e. bucket for s3 + * @param stageFilePath stage file path + * @param stageRegion region name where the stage persists + * @param presignedUrl presigned URL for download. Used by GCP. + * @param queryId last query id + * @return input file stream + * @throws SnowflakeSQLException when download failure + */ + InputStream downloadToStream( + SFSession connection, + String command, + int parallelism, + String remoteStorageLocation, + String stageFilePath, + String stageRegion, + String presignedUrl, + String queryId) throws SnowflakeSQLException; /** * Upload a file (-stream) to remote storage * + * @deprecated user {@link #upload(SFSession, String, int, boolean, String, File, String, + * InputStream, FileBackedOutputStream, StorageObjectMetadata, String, String, String)} * @param connection connection object * @param command upload command * @param parallelism number of threads do parallel uploading @@ -153,7 +236,8 @@ InputStream downloadToStream( * @param presignedUrl presigned URL for upload. Used by GCP. * @throws SnowflakeSQLException if upload failed even after retry */ - void upload( + @Deprecated + default void upload( SFSession connection, String command, int parallelism, @@ -166,6 +250,55 @@ void upload( StorageObjectMetadata meta, String stageRegion, String presignedUrl) + throws SnowflakeSQLException { + upload( + connection, + command, + parallelism, + uploadFromStream, + remoteStorageLocation, + srcFile, + destFileName, + inputStream, + fileBackedOutputStream, + meta, + stageRegion, + presignedUrl, + null); + } + + /** + * Upload a file (-stream) to remote storage + * + * @param connection connection object + * @param command upload command + * @param parallelism number of threads do parallel uploading + * @param uploadFromStream true if upload source is stream + * @param remoteStorageLocation s3 bucket name + * @param srcFile source file if not uploading from a stream + * @param destFileName file name on remote storage after upload + * @param inputStream stream used for uploading if fileBackedOutputStream is null + * @param fileBackedOutputStream stream used for uploading if not null + * @param meta object meta data + * @param stageRegion region name where the stage persists + * @param presignedUrl presigned URL for upload. Used by GCP. + * @param queryId last query id + * @throws SnowflakeSQLException if upload failed even after retry + */ + void upload( + SFSession connection, + String command, + int parallelism, + boolean uploadFromStream, + String remoteStorageLocation, + File srcFile, + String destFileName, + InputStream inputStream, + FileBackedOutputStream fileBackedOutputStream, + StorageObjectMetadata meta, + String stageRegion, + String presignedUrl, + String queryId) throws SnowflakeSQLException; /** @@ -173,6 +306,10 @@ void upload( * *

NOTE: This function is only supported when pre-signed URL is used. * + * @deprecated use {@link #uploadWithPresignedUrlWithoutConnection(int, HttpClientSettingsKey, + * int, boolean, String, File, String, InputStream, FileBackedOutputStream, + * StorageObjectMetadata, String, String, String)} This method was left to keep backward + * compatibility * @param networkTimeoutInMilli Network timeout for the upload * @param ocspModeAndProxyKey OCSP mode and proxy settings for the upload. * @param parallelism number of threads do parallel uploading @@ -187,6 +324,7 @@ void upload( * @param presignedUrl presigned URL for upload. Used by GCP. * @throws SnowflakeSQLException if upload failed even after retry */ + @Deprecated default void uploadWithPresignedUrlWithoutConnection( int networkTimeoutInMilli, HttpClientSettingsKey ocspModeAndProxyKey, @@ -201,8 +339,60 @@ default void uploadWithPresignedUrlWithoutConnection( String stageRegion, String presignedUrl) throws SnowflakeSQLException { + uploadWithPresignedUrlWithoutConnection( + networkTimeoutInMilli, + ocspModeAndProxyKey, + parallelism, + uploadFromStream, + remoteStorageLocation, + srcFile, + destFileName, + inputStream, + fileBackedOutputStream, + meta, + stageRegion, + presignedUrl, + null); + } + + /** + * Upload a file (-stream) to remote storage with Pre-signed URL without JDBC connection. + * + *

NOTE: This function is only supported when pre-signed URL is used. + * + * @param networkTimeoutInMilli Network timeout for the upload + * @param ocspModeAndProxyKey OCSP mode and proxy settings for the upload. + * @param parallelism number of threads do parallel uploading + * @param uploadFromStream true if upload source is stream + * @param remoteStorageLocation s3 bucket name + * @param srcFile source file if not uploading from a stream + * @param destFileName file name on remote storage after upload + * @param inputStream stream used for uploading if fileBackedOutputStream is null + * @param fileBackedOutputStream stream used for uploading if not null + * @param meta object meta data + * @param stageRegion region name where the stage persists + * @param presignedUrl presigned URL for upload. Used by GCP. + * @param queryId last query id + * @throws SnowflakeSQLException if upload failed even after retry + */ + default void uploadWithPresignedUrlWithoutConnection( + int networkTimeoutInMilli, + HttpClientSettingsKey ocspModeAndProxyKey, + int parallelism, + boolean uploadFromStream, + String remoteStorageLocation, + File srcFile, + String destFileName, + InputStream inputStream, + FileBackedOutputStream fileBackedOutputStream, + StorageObjectMetadata meta, + String stageRegion, + String presignedUrl, + String queryId) + throws SnowflakeSQLException { if (!requirePresignedUrl()) { throw new SnowflakeSQLLoggedException( + queryId, null, ErrorCode.INTERNAL_ERROR.getMessageCode(), SqlState.INTERNAL_ERROR, @@ -214,6 +404,8 @@ default void uploadWithPresignedUrlWithoutConnection( /** * Handles exceptions thrown by the remote storage provider * + * @deprecated use {@link #handleStorageException(Exception, int, String, SFSession, String, + * String)} * @param ex the exception to handle * @param retryCount current number of retries, incremented by the caller before each call * @param operation string that indicates the function/operation that was taking place, when the @@ -223,8 +415,33 @@ default void uploadWithPresignedUrlWithoutConnection( * @throws SnowflakeSQLException exceptions that were not handled, or retried past what the retry * policy allows, are propagated */ - void handleStorageException( + @Deprecated + default void handleStorageException( Exception ex, int retryCount, String operation, SFSession connection, String command) + throws SnowflakeSQLException { + handleStorageException(ex, retryCount, operation, connection, command, null); + } + + /** + * Handles exceptions thrown by the remote storage provider + * + * @param ex the exception to handle + * @param retryCount current number of retries, incremented by the caller before each call + * @param operation string that indicates the function/operation that was taking place, when the + * exception was raised, for example "upload" + * @param connection the current SFSession object used by the client + * @param command the command attempted at the time of the exception + * @param queryId last query id + * @throws SnowflakeSQLException exceptions that were not handled, or retried past what the retry + * policy allows, are propagated + */ + void handleStorageException( + Exception ex, + int retryCount, + String operation, + SFSession connection, + String command, + String queryId) throws SnowflakeSQLException; /** diff --git a/src/test/java/net/snowflake/client/jdbc/ConnectionLatestIT.java b/src/test/java/net/snowflake/client/jdbc/ConnectionLatestIT.java index 5a983fc18..ae896b477 100644 --- a/src/test/java/net/snowflake/client/jdbc/ConnectionLatestIT.java +++ b/src/test/java/net/snowflake/client/jdbc/ConnectionLatestIT.java @@ -194,16 +194,18 @@ public void putGetStatementsHaveQueryIDEvenWhenFail() throws Throwable { try { statement.executeQuery("PUT file://" + sourceFilePath + " @not_existing_state"); fail("PUT statement should fail"); - } catch (Exception __) { + } catch (SnowflakeSQLException e) { TestUtil.assertValidQueryId(snowflakeStatement.getQueryID()); + assertEquals(snowflakeStatement.getQueryID(), e.getQueryId()); } String putQueryId = snowflakeStatement.getQueryID(); try { statement.executeQuery( "GET @not_existing_state 'file://" + destFolderCanonicalPath + "' parallel=8"); fail("GET statement should fail"); - } catch (Exception __) { + } catch (SnowflakeSQLException e) { TestUtil.assertValidQueryId(snowflakeStatement.getQueryID()); + assertEquals(snowflakeStatement.getQueryID(), e.getQueryId()); } String getQueryId = snowflakeStatement.getQueryID(); assertNotEquals("put and get query id should be different", putQueryId, getQueryId); @@ -213,8 +215,9 @@ public void putGetStatementsHaveQueryIDEvenWhenFail() throws Throwable { try { statement.executeQuery("PUT file://not_existing_file @" + stageName); fail("PUT statement should fail"); - } catch (Exception __) { - assertNull(snowflakeStatement.getQueryID()); + } catch (SnowflakeSQLException e) { + TestUtil.assertValidQueryId(snowflakeStatement.getQueryID()); + assertEquals(snowflakeStatement.getQueryID(), e.getQueryId()); } } } diff --git a/src/test/java/net/snowflake/client/jdbc/FileUploaderExpandFileNamesTest.java b/src/test/java/net/snowflake/client/jdbc/FileUploaderExpandFileNamesTest.java index fefd55e13..9f329b01c 100644 --- a/src/test/java/net/snowflake/client/jdbc/FileUploaderExpandFileNamesTest.java +++ b/src/test/java/net/snowflake/client/jdbc/FileUploaderExpandFileNamesTest.java @@ -36,7 +36,7 @@ public void testProcessFileNames() throws Exception { folderName + "/TestFileE~" }; - Set files = SnowflakeFileTransferAgent.expandFileNames(locations); + Set files = SnowflakeFileTransferAgent.expandFileNames(locations, null); assertTrue(files.contains(folderName + "/TestFileA")); assertTrue(files.contains(folderName + "/TestFileB")); @@ -52,7 +52,7 @@ public void testProcessFileNamesException() { String[] locations = {"/Tes*Fil*A", "/TestFil?B", "~/TestFileC", "TestFileD"}; try { - SnowflakeFileTransferAgent.expandFileNames(locations); + SnowflakeFileTransferAgent.expandFileNames(locations, null); } catch (SnowflakeSQLException err) { Assert.assertEquals(200007, err.getErrorCode()); Assert.assertEquals("22000", err.getSQLState()); diff --git a/src/test/java/net/snowflake/client/jdbc/SnowflakeAzureClientHandleExceptionLatestIT.java b/src/test/java/net/snowflake/client/jdbc/SnowflakeAzureClientHandleExceptionLatestIT.java index 9d59a39f9..d562cff34 100644 --- a/src/test/java/net/snowflake/client/jdbc/SnowflakeAzureClientHandleExceptionLatestIT.java +++ b/src/test/java/net/snowflake/client/jdbc/SnowflakeAzureClientHandleExceptionLatestIT.java @@ -65,7 +65,8 @@ public void error403RenewExpired() throws SQLException, InterruptedException { 0, "upload", sfSession, - command); + command, + null); Mockito.verify(spyingClient, Mockito.times(2)).renew(Mockito.anyMap()); // Unauthenticated, backoff with interrupt, renew is called @@ -86,7 +87,8 @@ public void run() { maxRetry, "upload", sfSession, - command); + command, + null); } catch (SnowflakeSQLException e) { exceptionContainer[0] = e; } @@ -108,7 +110,8 @@ public void error403OverMaxRetryThrow() throws SQLException { overMaxRetry, "upload", sfSession, - command); + command, + null); } @Test(expected = SnowflakeSQLException.class) @@ -120,14 +123,15 @@ public void error403NullSession() throws SQLException { 0, "upload", null, - command); + command, + null); } @Test(expected = SnowflakeSQLException.class) @ConditionalIgnoreRule.ConditionalIgnore(condition = RunningOnGithubAction.class) public void errorInvalidKey() throws SQLException { spyingClient.handleStorageException( - new Exception(new InvalidKeyException()), 0, "upload", sfSession, command); + new Exception(new InvalidKeyException()), 0, "upload", sfSession, command, null); } @Test(expected = SnowflakeSQLException.class) @@ -136,13 +140,13 @@ public void errorInterruptedException() throws SQLException { // Can still retry, no error thrown try { spyingClient.handleStorageException( - new InterruptedException(), 0, "upload", sfSession, command); + new InterruptedException(), 0, "upload", sfSession, command, null); } catch (Exception e) { Assert.fail("Should not have exception here"); } Mockito.verify(spyingClient, Mockito.never()).renew(Mockito.anyMap()); spyingClient.handleStorageException( - new InterruptedException(), 26, "upload", sfSession, command); + new InterruptedException(), 26, "upload", sfSession, command, null); } @Test(expected = SnowflakeSQLException.class) @@ -151,19 +155,19 @@ public void errorSocketTimeoutException() throws SQLException { // Can still retry, no error thrown try { spyingClient.handleStorageException( - new SocketTimeoutException(), 0, "upload", sfSession, command); + new SocketTimeoutException(), 0, "upload", sfSession, command, null); } catch (Exception e) { Assert.fail("Should not have exception here"); } Mockito.verify(spyingClient, Mockito.never()).renew(Mockito.anyMap()); spyingClient.handleStorageException( - new SocketTimeoutException(), 26, "upload", sfSession, command); + new SocketTimeoutException(), 26, "upload", sfSession, command, null); } @Test(expected = SnowflakeSQLException.class) @ConditionalIgnoreRule.ConditionalIgnore(condition = RunningOnGithubAction.class) public void errorUnknownException() throws SQLException { - spyingClient.handleStorageException(new Exception(), 0, "upload", sfSession, command); + spyingClient.handleStorageException(new Exception(), 0, "upload", sfSession, command, null); } @Test(expected = SnowflakeSQLException.class) @@ -181,7 +185,8 @@ public void errorNoSpaceLeftOnDevice() throws SQLException, IOException { 0, "download", null, - getCommand); + getCommand, + null); } @After diff --git a/src/test/java/net/snowflake/client/jdbc/SnowflakeDriverLatestIT.java b/src/test/java/net/snowflake/client/jdbc/SnowflakeDriverLatestIT.java index da29b287e..93ec59580 100644 --- a/src/test/java/net/snowflake/client/jdbc/SnowflakeDriverLatestIT.java +++ b/src/test/java/net/snowflake/client/jdbc/SnowflakeDriverLatestIT.java @@ -1529,7 +1529,8 @@ public void testNoSpaceLeftOnDeviceException() throws SQLException { client.getMaxRetries(), "download", null, - command); + command, + null); } finally { if (connection != null) { connection.createStatement().execute("DROP STAGE if exists testPutGet_stage"); diff --git a/src/test/java/net/snowflake/client/jdbc/SnowflakeGcsClientHandleExceptionLatestIT.java b/src/test/java/net/snowflake/client/jdbc/SnowflakeGcsClientHandleExceptionLatestIT.java index 92763f083..effa3c9c1 100644 --- a/src/test/java/net/snowflake/client/jdbc/SnowflakeGcsClientHandleExceptionLatestIT.java +++ b/src/test/java/net/snowflake/client/jdbc/SnowflakeGcsClientHandleExceptionLatestIT.java @@ -59,12 +59,12 @@ public void setup() throws SQLException { public void error401RenewExpired() throws SQLException, InterruptedException { // Unauthenticated, renew is called. spyingClient.handleStorageException( - new StorageException(401, "Unauthenticated"), 0, "upload", sfSession, command); + new StorageException(401, "Unauthenticated"), 0, "upload", sfSession, command, null); Mockito.verify(spyingClient, Mockito.times(1)).renew(Mockito.anyMap()); // Unauthenticated, command null, not renew, renew called remaining 1 spyingClient.handleStorageException( - new StorageException(401, "Unauthenticated"), 0, "upload", sfSession, null); + new StorageException(401, "Unauthenticated"), 0, "upload", sfSession, null, null); Mockito.verify(spyingClient, Mockito.times(1)).renew(Mockito.anyMap()); // Unauthenticated, backoff with interrupt, renew is called @@ -80,7 +80,8 @@ public void run() { maxRetry, "upload", sfSession, - command); + command, + null); } catch (SnowflakeSQLException e) { exceptionContainer[0] = e; } @@ -97,7 +98,12 @@ public void run() { @ConditionalIgnoreRule.ConditionalIgnore(condition = RunningOnGithubAction.class) public void error401OverMaxRetryThrow() throws SQLException { spyingClient.handleStorageException( - new StorageException(401, "Unauthenticated"), overMaxRetry, "upload", sfSession, command); + new StorageException(401, "Unauthenticated"), + overMaxRetry, + "upload", + sfSession, + command, + null); } @Test(expected = SnowflakeSQLException.class) @@ -105,7 +111,7 @@ public void error401OverMaxRetryThrow() throws SQLException { public void errorInvalidKey() throws SQLException { // Unauthenticated, renew is called. spyingClient.handleStorageException( - new Exception(new InvalidKeyException()), 0, "upload", sfSession, command); + new Exception(new InvalidKeyException()), 0, "upload", sfSession, command, null); } @Test(expected = SnowflakeSQLException.class) @@ -114,13 +120,13 @@ public void errorInterruptedException() throws SQLException { // Can still retry, no error thrown try { spyingClient.handleStorageException( - new InterruptedException(), 0, "upload", sfSession, command); + new InterruptedException(), 0, "upload", sfSession, command, null); } catch (Exception e) { Assert.fail("Should not have exception here"); } Mockito.verify(spyingClient, Mockito.never()).renew(Mockito.anyMap()); spyingClient.handleStorageException( - new InterruptedException(), 26, "upload", sfSession, command); + new InterruptedException(), 26, "upload", sfSession, command, null); } @Test(expected = SnowflakeSQLException.class) @@ -129,27 +135,27 @@ public void errorSocketTimeoutException() throws SQLException { // Can still retry, no error thrown try { spyingClient.handleStorageException( - new SocketTimeoutException(), 0, "upload", sfSession, command); + new SocketTimeoutException(), 0, "upload", sfSession, command, null); } catch (Exception e) { Assert.fail("Should not have exception here"); } Mockito.verify(spyingClient, Mockito.never()).renew(Mockito.anyMap()); spyingClient.handleStorageException( - new SocketTimeoutException(), 26, "upload", sfSession, command); + new SocketTimeoutException(), 26, "upload", sfSession, command, null); } @Test(expected = SnowflakeSQLException.class) @ConditionalIgnoreRule.ConditionalIgnore(condition = RunningOnGithubAction.class) public void errorUnknownException() throws SQLException { // Unauthenticated, renew is called. - spyingClient.handleStorageException(new Exception(), 0, "upload", sfSession, command); + spyingClient.handleStorageException(new Exception(), 0, "upload", sfSession, command, null); } @Test(expected = SnowflakeSQLException.class) @ConditionalIgnoreRule.ConditionalIgnore(condition = RunningOnGithubAction.class) public void errorWithNullSession() throws SQLException { spyingClient.handleStorageException( - new StorageException(401, "Unauthenticated"), 0, "upload", null, command); + new StorageException(401, "Unauthenticated"), 0, "upload", null, command, null); } @Test(expected = SnowflakeSQLException.class) @@ -167,7 +173,8 @@ public void errorNoSpaceLeftOnDevice() throws SQLException, IOException { 0, "download", null, - getCommand); + getCommand, + null); } @After diff --git a/src/test/java/net/snowflake/client/jdbc/SnowflakeS3ClientHandleExceptionLatestIT.java b/src/test/java/net/snowflake/client/jdbc/SnowflakeS3ClientHandleExceptionLatestIT.java index 5cbdfc4dd..523475251 100644 --- a/src/test/java/net/snowflake/client/jdbc/SnowflakeS3ClientHandleExceptionLatestIT.java +++ b/src/test/java/net/snowflake/client/jdbc/SnowflakeS3ClientHandleExceptionLatestIT.java @@ -75,7 +75,7 @@ public void setup() throws SQLException { public void errorRenewExpired() throws SQLException, InterruptedException { AmazonS3Exception ex = new AmazonS3Exception("unauthenticated"); ex.setErrorCode(EXPIRED_AWS_TOKEN_ERROR_CODE); - spyingClient.handleStorageException(ex, 0, "upload", sfSession, command); + spyingClient.handleStorageException(ex, 0, "upload", sfSession, command, null); Mockito.verify(spyingClient, Mockito.times(1)).renew(Mockito.anyMap()); // Unauthenticated, backoff with interrupt, renew is called @@ -86,7 +86,8 @@ public void errorRenewExpired() throws SQLException, InterruptedException { @Override public void run() { try { - spyingClient.handleStorageException(ex, maxRetry, "upload", sfSession, command); + spyingClient.handleStorageException( + ex, maxRetry, "upload", sfSession, command, null); } catch (SnowflakeSQLException e) { exceptionContainer[0] = e; } @@ -103,7 +104,7 @@ public void run() { @ConditionalIgnoreRule.ConditionalIgnore(condition = RunningOnGithubAction.class) public void errorNotFound() throws SQLException { spyingClient.handleStorageException( - new AmazonS3Exception("Not found"), overMaxRetry, "upload", sfSession, command); + new AmazonS3Exception("Not found"), overMaxRetry, "upload", sfSession, command, null); } @Test @@ -115,7 +116,7 @@ public void errorBadRequestTokenExpired() throws SQLException { ex.setErrorCode("400 Bad Request"); ex.setErrorType(AmazonServiceException.ErrorType.Client); Mockito.doReturn(true).when(spyingClient).isClientException400Or404(ex); - spyingClient.handleStorageException(ex, 0, "download", sfSession, command); + spyingClient.handleStorageException(ex, 0, "download", sfSession, command, null); // renew token Mockito.verify(spyingClient, Mockito.times(1)).isClientException400Or404(ex); Mockito.verify(spyingClient, Mockito.times(1)).renew(Mockito.anyMap()); @@ -129,7 +130,8 @@ public void errorClientUnknown() throws SQLException { overMaxRetry, "upload", sfSession, - command); + command, + null); } @Test(expected = SnowflakeSQLException.class) @@ -137,7 +139,7 @@ public void errorClientUnknown() throws SQLException { public void errorInvalidKey() throws SQLException { // Unauthenticated, renew is called. spyingClient.handleStorageException( - new Exception(new InvalidKeyException()), 0, "upload", sfSession, command); + new Exception(new InvalidKeyException()), 0, "upload", sfSession, command, null); } @Test(expected = SnowflakeSQLException.class) @@ -146,13 +148,13 @@ public void errorInterruptedException() throws SQLException { // Can still retry, no error thrown try { spyingClient.handleStorageException( - new InterruptedException(), 0, "upload", sfSession, command); + new InterruptedException(), 0, "upload", sfSession, command, null); } catch (Exception e) { Assert.fail("Should not have exception here"); } Mockito.verify(spyingClient, Mockito.never()).renew(Mockito.anyMap()); spyingClient.handleStorageException( - new InterruptedException(), 26, "upload", sfSession, command); + new InterruptedException(), 26, "upload", sfSession, command, null); } @Test(expected = SnowflakeSQLException.class) @@ -161,19 +163,19 @@ public void errorSocketTimeoutException() throws SQLException { // Can still retry, no error thrown try { spyingClient.handleStorageException( - new SocketTimeoutException(), 0, "upload", sfSession, command); + new SocketTimeoutException(), 0, "upload", sfSession, command, null); } catch (Exception e) { Assert.fail("Should not have exception here"); } Mockito.verify(spyingClient, Mockito.never()).renew(Mockito.anyMap()); spyingClient.handleStorageException( - new SocketTimeoutException(), 26, "upload", sfSession, command); + new SocketTimeoutException(), 26, "upload", sfSession, command, null); } @Test(expected = SnowflakeSQLException.class) @ConditionalIgnoreRule.ConditionalIgnore(condition = RunningOnGithubAction.class) public void errorUnknownException() throws SQLException { - spyingClient.handleStorageException(new Exception(), 0, "upload", sfSession, command); + spyingClient.handleStorageException(new Exception(), 0, "upload", sfSession, command, null); } @Test(expected = SnowflakeSQLException.class) @@ -182,7 +184,7 @@ public void errorRenewExpiredNullSession() throws SQLException { // Unauthenticated, renew is called. AmazonS3Exception ex = new AmazonS3Exception("unauthenticated"); ex.setErrorCode(EXPIRED_AWS_TOKEN_ERROR_CODE); - spyingClient.handleStorageException(ex, 0, "upload", null, command); + spyingClient.handleStorageException(ex, 0, "upload", null, command, null); } @Test(expected = SnowflakeSQLException.class) @@ -200,7 +202,8 @@ public void errorNoSpaceLeftOnDevice() throws SQLException, IOException { 0, "download", null, - getCommand); + getCommand, + null); } @After diff --git a/src/test/java/net/snowflake/client/jdbc/StatementLatestIT.java b/src/test/java/net/snowflake/client/jdbc/StatementLatestIT.java index d05b767a3..35d2a65d5 100644 --- a/src/test/java/net/snowflake/client/jdbc/StatementLatestIT.java +++ b/src/test/java/net/snowflake/client/jdbc/StatementLatestIT.java @@ -248,9 +248,10 @@ public void testQueryIdIsSetOnFailedQueryExecute() throws SQLException { try { stmt.execute("use database not_existing_database"); fail("Statement should fail with exception"); - } catch (Exception __) { + } catch (SnowflakeSQLException e) { String queryID = stmt.unwrap(SnowflakeStatement.class).getQueryID(); TestUtil.assertValidQueryId(queryID); + assertEquals(queryID, e.getQueryId()); } } } @@ -265,9 +266,10 @@ public void testQueryIdIsSetOnFailedExecuteUpdate() throws SQLException { try { stmt.executeUpdate("update not_existing_table set a = 1 where id = 42"); fail("Statement should fail with exception"); - } catch (Exception __) { + } catch (SnowflakeSQLException e) { String queryID = stmt.unwrap(SnowflakeStatement.class).getQueryID(); TestUtil.assertValidQueryId(queryID); + assertEquals(queryID, e.getQueryId()); } } } @@ -282,9 +284,10 @@ public void testQueryIdIsSetOnFailedExecuteQuery() throws SQLException { try { stmt.executeQuery("select * from not_existing_table"); fail("Statement should fail with exception"); - } catch (Exception __) { + } catch (SnowflakeSQLException e) { String queryID = stmt.unwrap(SnowflakeStatement.class).getQueryID(); TestUtil.assertValidQueryId(queryID); + assertEquals(queryID, e.getQueryId()); } } } diff --git a/src/test/java/net/snowflake/client/jdbc/cloud/storage/SnowflakeS3ClientLatestIT.java b/src/test/java/net/snowflake/client/jdbc/cloud/storage/SnowflakeS3ClientLatestIT.java index df5bf635a..fb3d2005e 100644 --- a/src/test/java/net/snowflake/client/jdbc/cloud/storage/SnowflakeS3ClientLatestIT.java +++ b/src/test/java/net/snowflake/client/jdbc/cloud/storage/SnowflakeS3ClientLatestIT.java @@ -158,12 +158,14 @@ public void testPutGetMaxRetries() throws SQLException { // Should retry one time, then throw error try { - spy.handleStorageException(new InterruptedException(), 0, "download", sfSession, command); + spy.handleStorageException( + new InterruptedException(), 0, "download", sfSession, command, null); } catch (Exception e) { Assert.fail("Should not have exception here"); } Mockito.verify(spy, Mockito.never()).renew(Mockito.anyMap()); - spy.handleStorageException(new InterruptedException(), 1, "download", sfSession, command); + spy.handleStorageException( + new InterruptedException(), 1, "download", sfSession, command, null); } } } From 6e6e5cd40c5157bc56fddded103d63846e9451e1 Mon Sep 17 00:00:00 2001 From: Piotr Fus Date: Wed, 3 Jan 2024 10:52:31 +0100 Subject: [PATCH 17/17] SNOW-974576 Extract type converters for JSON results (#1587) * SNOW-974576 Extract type converters for JSON result Co-authored-by: Przemyslaw Motacki --- .../client/core/SFFixedViewResultSet.java | 19 + .../client/core/SFJsonResultSet.java | 579 ++---------------- .../snowflake/client/core/SFResultSet.java | 44 +- .../client/core/json/BooleanConverter.java | 37 ++ .../client/core/json/BytesConverter.java | 68 ++ .../client/core/json/Converters.java | 74 +++ .../client/core/json/DateTimeConverter.java | 195 ++++++ .../client/core/json/NumberConverter.java | 192 ++++++ .../client/core/json/StringConverter.java | 155 +++++ .../snowflake/client/jdbc/RestRequest.java | 9 +- .../SnowflakeResultSetSerializableV1.java | 4 +- .../core/json/BooleanConverterTest.java | 51 ++ .../client/core/json/BytesConverterTest.java | 51 ++ .../core/json/DateTimeConverterTest.java | 147 +++++ .../client/core/json/NumberConverterTest.java | 163 +++++ .../client/core/json/StringConverterTest.java | 108 ++++ .../client/jdbc/MockConnectionTest.java | 18 + 17 files changed, 1367 insertions(+), 547 deletions(-) create mode 100644 src/main/java/net/snowflake/client/core/json/BooleanConverter.java create mode 100644 src/main/java/net/snowflake/client/core/json/BytesConverter.java create mode 100644 src/main/java/net/snowflake/client/core/json/Converters.java create mode 100644 src/main/java/net/snowflake/client/core/json/DateTimeConverter.java create mode 100644 src/main/java/net/snowflake/client/core/json/NumberConverter.java create mode 100644 src/main/java/net/snowflake/client/core/json/StringConverter.java create mode 100644 src/test/java/net/snowflake/client/core/json/BooleanConverterTest.java create mode 100644 src/test/java/net/snowflake/client/core/json/BytesConverterTest.java create mode 100644 src/test/java/net/snowflake/client/core/json/DateTimeConverterTest.java create mode 100644 src/test/java/net/snowflake/client/core/json/NumberConverterTest.java create mode 100644 src/test/java/net/snowflake/client/core/json/StringConverterTest.java diff --git a/src/main/java/net/snowflake/client/core/SFFixedViewResultSet.java b/src/main/java/net/snowflake/client/core/SFFixedViewResultSet.java index c4145649e..671857b93 100644 --- a/src/main/java/net/snowflake/client/core/SFFixedViewResultSet.java +++ b/src/main/java/net/snowflake/client/core/SFFixedViewResultSet.java @@ -6,16 +6,19 @@ import java.sql.SQLException; import java.util.List; +import net.snowflake.client.core.json.Converters; import net.snowflake.client.jdbc.*; import net.snowflake.client.jdbc.SFBaseFileTransferAgent.CommandType; import net.snowflake.client.log.SFLogger; import net.snowflake.client.log.SFLoggerFactory; +import net.snowflake.common.core.SFBinaryFormat; import net.snowflake.common.core.SqlState; /** * Fixed view result set. This class iterates through any fixed view implementation and return the * objects as rows */ +// Works only for strings, numbers, etc, does not work for timestamps, dates, times etc. public class SFFixedViewResultSet extends SFJsonResultSet { private static final SFLogger logger = SFLoggerFactory.getLogger(SFFixedViewResultSet.class); @@ -26,6 +29,22 @@ public class SFFixedViewResultSet extends SFJsonResultSet { public SFFixedViewResultSet(SnowflakeFixedView fixedView, CommandType commandType, String queryID) throws SnowflakeSQLException { + super( + null, + new Converters( + null, + new SFSession(), + 0, + false, + false, + false, + false, + SFBinaryFormat.BASE64, + null, + null, + null, + null, + null)); this.fixedView = fixedView; this.commandType = commandType; this.queryID = queryID; diff --git a/src/main/java/net/snowflake/client/core/SFJsonResultSet.java b/src/main/java/net/snowflake/client/core/SFJsonResultSet.java index a896388cf..95a220560 100644 --- a/src/main/java/net/snowflake/client/core/SFJsonResultSet.java +++ b/src/main/java/net/snowflake/client/core/SFJsonResultSet.java @@ -5,35 +5,27 @@ package net.snowflake.client.core; import java.math.BigDecimal; -import java.math.RoundingMode; -import java.nio.ByteBuffer; import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; import java.util.TimeZone; -import net.snowflake.client.core.arrow.ArrowResultUtil; -import net.snowflake.client.jdbc.*; -import net.snowflake.client.log.ArgSupplier; +import net.snowflake.client.core.json.Converters; +import net.snowflake.client.jdbc.ErrorCode; import net.snowflake.client.log.SFLogger; import net.snowflake.client.log.SFLoggerFactory; -import net.snowflake.common.core.SFBinary; -import net.snowflake.common.core.SFBinaryFormat; -import net.snowflake.common.core.SFTime; -import net.snowflake.common.core.SFTimestamp; -import org.apache.arrow.vector.Float8Vector; /** Abstract class used to represent snowflake result set in json format */ public abstract class SFJsonResultSet extends SFBaseResultSet { private static final SFLogger logger = SFLoggerFactory.getLogger(SFJsonResultSet.class); - TimeZone sessionTimeZone; + protected final TimeZone sessionTimeZone; + protected final Converters converters; - // Precision of maximum long value in Java (2^63-1). Precision is 19 - private static final int LONG_PRECISION = 19; - - private static final BigDecimal MAX_LONG_VAL = new BigDecimal(Long.MAX_VALUE); - private static final BigDecimal MIN_LONG_VAL = new BigDecimal(Long.MIN_VALUE); + protected SFJsonResultSet(TimeZone sessionTimeZone, Converters converters) { + this.sessionTimeZone = sessionTimeZone; + this.converters = converters; + } /** * Given a column index, get current row's value as an object @@ -100,570 +92,121 @@ public Object getObject(int columnIndex) throws SFException { * @throws SFException */ private Object getBigInt(int columnIndex, Object obj) throws SFException { - // If precision is < precision of max long precision, we can automatically convert to long. - // Otherwise, do a check to ensure it doesn't overflow max long value. - String numberAsString = obj.toString(); - if (numberAsString.length() >= LONG_PRECISION) { - BigDecimal bigNum = getBigDecimal(columnIndex); - if (bigNum.compareTo(MAX_LONG_VAL) == 1 || bigNum.compareTo(MIN_LONG_VAL) == -1) { - return bigNum; - } - } - return getLong(columnIndex); + return converters.getNumberConverter().getBigInt(obj, columnIndex); } @Override public String getString(int columnIndex) throws SFException { logger.debug("public String getString(int columnIndex)", false); - - // Column index starts from 1, not 0. Object obj = getObjectInternal(columnIndex); - if (obj == null) { - return null; - } - - // print timestamp in string format int columnType = resultSetMetaData.getInternalColumnType(columnIndex); - switch (columnType) { - case Types.BOOLEAN: - return ResultUtil.getBooleanAsString(ResultUtil.getBoolean(obj.toString())); - - case Types.TIMESTAMP: - case SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_LTZ: - case SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_TZ: - SFTimestamp sfTS = getSFTimestamp(columnIndex); - int columnScale = resultSetMetaData.getScale(columnIndex); - - String timestampStr = - ResultUtil.getSFTimestampAsString( - sfTS, - columnType, - columnScale, - timestampNTZFormatter, - timestampLTZFormatter, - timestampTZFormatter, - session); - - logger.debug( - "Converting timestamp to string from: {} to: {}", - (ArgSupplier) obj::toString, - timestampStr); - - return timestampStr; - - case Types.DATE: - Date date = getDate(columnIndex); - - if (dateFormatter == null) { - throw new SFException(ErrorCode.INTERNAL_ERROR, "missing date formatter"); - } - - String dateStr = ResultUtil.getDateAsString(date, dateFormatter); - - logger.debug( - "Converting date to string from: {} to: {}", (ArgSupplier) obj::toString, dateStr); - - return dateStr; - - case Types.TIME: - SFTime sfTime = getSFTime(columnIndex); - - if (timeFormatter == null) { - throw new SFException(ErrorCode.INTERNAL_ERROR, "missing time formatter"); - } - - int scale = resultSetMetaData.getScale(columnIndex); - String timeStr = ResultUtil.getSFTimeAsString(sfTime, scale, timeFormatter); - - logger.debug( - "Converting time to string from: {} to: {}", (ArgSupplier) obj::toString, timeStr); - - return timeStr; - - case Types.BINARY: - if (binaryFormatter == null) { - throw new SFException(ErrorCode.INTERNAL_ERROR, "missing binary formatter"); - } - - if (binaryFormatter == SFBinaryFormat.HEX) { - // Shortcut: the values are already passed with hex encoding, so just - // return the string unchanged rather than constructing an SFBinary. - return obj.toString(); - } - - SFBinary sfb = new SFBinary(getBytes(columnIndex)); - return binaryFormatter.format(sfb); - - default: - break; - } - - return obj.toString(); + int columnSubType = resultSetMetaData.getInternalColumnType(columnIndex); + int scale = resultSetMetaData.getScale(columnIndex); + return converters.getStringConverter().getString(obj, columnType, columnSubType, scale); } @Override public boolean getBoolean(int columnIndex) throws SFException { logger.debug("public boolean getBoolean(int columnIndex)", false); - Object obj = getObjectInternal(columnIndex); - if (obj == null) { - return false; - } - if (obj instanceof Boolean) { - return (Boolean) obj; - } int columnType = resultSetMetaData.getColumnType(columnIndex); - // if type is an approved type that can be converted to Boolean, do this - if (columnType == Types.BOOLEAN - || columnType == Types.INTEGER - || columnType == Types.SMALLINT - || columnType == Types.TINYINT - || columnType == Types.BIGINT - || columnType == Types.BIT - || columnType == Types.VARCHAR - || columnType == Types.CHAR) { - String type = obj.toString(); - if ("1".equals(type) || Boolean.TRUE.toString().equalsIgnoreCase(type)) { - return true; - } - if ("0".equals(type) || Boolean.FALSE.toString().equalsIgnoreCase(type)) { - return false; - } - } - throw new SFException( - ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.BOOLEAN_STR, obj); + return converters.getBooleanConverter().getBoolean(getObjectInternal(columnIndex), columnType); } @Override public byte getByte(int columnIndex) throws SFException { logger.debug("public short getByte(int columnIndex)", false); - - // Column index starts from 1, not 0. Object obj = getObjectInternal(columnIndex); - - if (obj == null) { - return 0; - } - - if (obj instanceof String) { - return Byte.parseByte((String) obj); - } else { - return ((Number) obj).byteValue(); - } + return converters.getNumberConverter().getByte(obj); } @Override public short getShort(int columnIndex) throws SFException { logger.debug("public short getShort(int columnIndex)", false); - - // Column index starts from 1, not 0. Object obj = getObjectInternal(columnIndex); - - if (obj == null) { - return 0; - } int columnType = resultSetMetaData.getColumnType(columnIndex); - try { - - if (obj instanceof String) { - String objString = (String) obj; - if (objString.contains(".") && (columnType == Types.FLOAT || columnType == Types.DOUBLE)) { - objString = objString.substring(0, objString.indexOf(".")); - } - return Short.parseShort(objString); - } else { - return ((Number) obj).shortValue(); - } - } catch (NumberFormatException ex) { - throw new SFException( - ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.SHORT_STR, obj); - } + return converters.getNumberConverter().getShort(obj, columnType); } @Override public int getInt(int columnIndex) throws SFException { logger.debug("public int getInt(int columnIndex)", false); - - // Column index starts from 1, not 0. Object obj = getObjectInternal(columnIndex); - - if (obj == null) { - return 0; - } int columnType = resultSetMetaData.getColumnType(columnIndex); - try { - if (obj instanceof String) { - String objString = (String) obj; - if (objString.contains(".") && (columnType == Types.FLOAT || columnType == Types.DOUBLE)) { - objString = objString.substring(0, objString.indexOf(".")); - } - return Integer.parseInt(objString); - } else { - return ((Number) obj).intValue(); - } - } catch (NumberFormatException ex) { - throw new SFException( - ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.INT_STR, obj); - } + return converters.getNumberConverter().getInt(obj, columnType); } @Override public long getLong(int columnIndex) throws SFException { logger.debug("public long getLong(int columnIndex)", false); - - // Column index starts from 1, not 0. Object obj = getObjectInternal(columnIndex); - - if (obj == null) { - return 0; - } int columnType = resultSetMetaData.getColumnType(columnIndex); - try { - if (obj instanceof String) { - String objString = (String) obj; - if (objString.contains(".") && (columnType == Types.FLOAT || columnType == Types.DOUBLE)) { - objString = objString.substring(0, objString.indexOf(".")); - } - return Long.parseLong(objString); - } else { - return ((Number) obj).longValue(); - } - } catch (NumberFormatException nfe) { - - if (Types.INTEGER == columnType || Types.SMALLINT == columnType) { - throw new SFException( - ErrorCode.INTERNAL_ERROR, SnowflakeUtil.LONG_STR + ": " + obj.toString()); - } else { - throw new SFException( - ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.LONG_STR, obj); - } - } + return converters.getNumberConverter().getLong(obj, columnType); } @Override public BigDecimal getBigDecimal(int columnIndex) throws SFException { logger.debug("public BigDecimal getBigDecimal(int columnIndex)", false); - - // Column index starts from 1, not 0. Object obj = getObjectInternal(columnIndex); - - if (obj == null) { - return null; - } int columnType = resultSetMetaData.getColumnType(columnIndex); - try { - if (columnType != Types.TIME - && columnType != Types.TIMESTAMP - && columnType != Types.TIMESTAMP_WITH_TIMEZONE) { - return new BigDecimal(obj.toString()); - } - throw new SFException( - ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.BIG_DECIMAL_STR, obj); - - } catch (NumberFormatException ex) { - throw new SFException( - ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.BIG_DECIMAL_STR, obj); - } + return converters.getNumberConverter().getBigDecimal(obj, columnType); } @Override public BigDecimal getBigDecimal(int columnIndex, int scale) throws SFException { logger.debug("public BigDecimal getBigDecimal(int columnIndex)", false); - Object obj = getObjectInternal(columnIndex); - - if (obj == null) { - return null; - } - BigDecimal value = new BigDecimal(obj.toString()); - - value = value.setScale(scale, RoundingMode.HALF_UP); - - return value; - } - - private TimeZone adjustTimezoneForTimestampTZ(int columnIndex) throws SFException { - // If the timestamp is of type timestamp_tz, use the associated offset timezone instead of the - // session timezone for formatting - Object obj = getObjectInternal(columnIndex); - int subType = resultSetMetaData.getInternalColumnType(columnIndex); - if (obj != null && subType == SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_TZ && resultVersion > 0) { - String timestampStr = obj.toString(); - int indexForSeparator = timestampStr.indexOf(' '); - String timezoneIndexStr = timestampStr.substring(indexForSeparator + 1); - return SFTimestamp.convertTimezoneIndexToTimeZone(Integer.parseInt(timezoneIndexStr)); - } - // By default, return session timezone - return sessionTimeZone; - } - - private SFTimestamp getSFTimestamp(int columnIndex) throws SFException { - logger.debug("public Timestamp getTimestamp(int columnIndex)", false); - - Object obj = getObjectInternal(columnIndex); - - if (obj == null) { - return null; - } - - return ResultUtil.getSFTimestamp( - obj.toString(), - resultSetMetaData.getScale(columnIndex), - resultSetMetaData.getInternalColumnType(columnIndex), - resultVersion, - sessionTimeZone, - session); + int columnType = resultSetMetaData.getColumnType(columnIndex); + return converters.getNumberConverter().getBigDecimal(obj, columnType, scale); } @Override public Time getTime(int columnIndex) throws SFException { logger.debug("public Time getTime(int columnIndex)", false); - + Object obj = getObjectInternal(columnIndex); int columnType = resultSetMetaData.getColumnType(columnIndex); - if (Types.TIME == columnType) { - SFTime sfTime = getSFTime(columnIndex); - if (sfTime == null) { - return null; - } - Time ts = - new Time( - sfTime.getFractionalSeconds(ResultUtil.DEFAULT_SCALE_OF_SFTIME_FRACTION_SECONDS)); - if (resultSetSerializable.getUseSessionTimezone()) { - ts = - SnowflakeUtil.getTimeInSessionTimezone( - SnowflakeUtil.getSecondsFromMillis(ts.getTime()), - sfTime.getNanosecondsWithinSecond()); - } - return ts; - } else if (Types.TIMESTAMP == columnType || Types.TIMESTAMP_WITH_TIMEZONE == columnType) { - Timestamp ts = getTimestamp(columnIndex); - if (ts == null) { - return null; - } - if (resultSetSerializable.getUseSessionTimezone()) { - ts = getTimestamp(columnIndex, sessionTimeZone); - TimeZone sessionTimeZone = adjustTimezoneForTimestampTZ(columnIndex); - return new SnowflakeTimeWithTimezone( - ts, sessionTimeZone, resultSetSerializable.getUseSessionTimezone()); - } - return new Time(ts.getTime()); - } else { - throw new SFException( - ErrorCode.INVALID_VALUE_CONVERT, - columnType, - SnowflakeUtil.TIME_STR, - getObjectInternal(columnIndex)); - } + int columnSubType = resultSetMetaData.getInternalColumnType(columnIndex); + int scale = resultSetMetaData.getScale(columnIndex); + return converters + .getDateTimeConverter() + .getTime(obj, columnType, columnSubType, TimeZone.getDefault(), scale); } @Override public Timestamp getTimestamp(int columnIndex, TimeZone tz) throws SFException { + logger.debug("public Timestamp getTimestamp(int columnIndex)", false); + Object obj = getObjectInternal(columnIndex); int columnType = resultSetMetaData.getColumnType(columnIndex); - if (Types.TIMESTAMP == columnType || Types.TIMESTAMP_WITH_TIMEZONE == columnType) { - if (tz == null) { - tz = TimeZone.getDefault(); - } - SFTimestamp sfTS = getSFTimestamp(columnIndex); - - if (sfTS == null) { - return null; - } - Timestamp res = sfTS.getTimestamp(); - if (res == null) { - return null; - } - int subType = resultSetMetaData.getInternalColumnType(columnIndex); - // If we want to display format with no session offset, we have to use session timezone for - // ltz and tz types but UTC timezone for ntz type. - if (resultSetSerializable.getUseSessionTimezone()) { - if (subType == SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_LTZ - || subType == SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_TZ) { - TimeZone specificSessionTimezone = adjustTimezoneForTimestampTZ(columnIndex); - res = new SnowflakeTimestampWithTimezone(res, specificSessionTimezone); - } else { - res = new SnowflakeTimestampWithTimezone(res); - } - } - // If timestamp type is NTZ and JDBC_TREAT_TIMESTAMP_NTZ_AS_UTC=true, keep - // timezone in UTC to avoid daylight savings errors - else if (resultSetSerializable.getTreatNTZAsUTC() - && resultSetMetaData.getInternalColumnType(columnIndex) == Types.TIMESTAMP) { - res = new SnowflakeTimestampWithTimezone(res); - } - // If JDBC_TREAT_TIMESTAMP_NTZ_AS_UTC=false, default behavior is to honor - // client timezone for NTZ time. Move NTZ timestamp offset to correspond to - // client's timezone. JDBC_USE_SESSION_TIMEZONE overrides other params. - if (resultSetMetaData.getInternalColumnType(columnIndex) == Types.TIMESTAMP - && ((!resultSetSerializable.getTreatNTZAsUTC() && honorClientTZForTimestampNTZ) - || resultSetSerializable.getUseSessionTimezone())) { - res = sfTS.moveToTimeZone(tz).getTimestamp(); - } - // Adjust time if date happens before year 1582 for difference between - // Julian and Gregorian calendars - return ResultUtil.adjustTimestamp(res); - } else if (Types.DATE == columnType) { - Date d = getDate(columnIndex, tz); - if (d == null) { - return null; - } - return new Timestamp(d.getTime()); - } else if (Types.TIME == columnType) { - Time t = getTime(columnIndex); - if (t == null) { - return null; - } - if (resultSetSerializable.getUseSessionTimezone()) { - SFTime sfTime = getSFTime(columnIndex); - return new SnowflakeTimestampWithTimezone( - sfTime.getFractionalSeconds(ResultUtil.DEFAULT_SCALE_OF_SFTIME_FRACTION_SECONDS), - sfTime.getNanosecondsWithinSecond(), - TimeZone.getTimeZone("UTC")); - } - return new Timestamp(t.getTime()); - } else { - throw new SFException( - ErrorCode.INVALID_VALUE_CONVERT, - columnType, - SnowflakeUtil.TIMESTAMP_STR, - getObjectInternal(columnIndex)); - } + int columnSubType = resultSetMetaData.getInternalColumnType(columnIndex); + int scale = resultSetMetaData.getScale(columnIndex); + return converters + .getDateTimeConverter() + .getTimestamp(obj, columnType, columnSubType, tz, scale); } @Override public float getFloat(int columnIndex) throws SFException { logger.debug("public float getFloat(int columnIndex)", false); - - // Column index starts from 1, not 0. Object obj = getObjectInternal(columnIndex); - - if (obj == null) { - return 0; - } - int columnType = resultSetMetaData.getColumnType(columnIndex); - try { - if (obj instanceof String) { - if (columnType != Types.TIME - && columnType != Types.TIMESTAMP - && columnType != Types.TIMESTAMP_WITH_TIMEZONE) { - if ("inf".equals(obj)) { - return Float.POSITIVE_INFINITY; - } else if ("-inf".equals(obj)) { - return Float.NEGATIVE_INFINITY; - } else { - return Float.parseFloat((String) obj); - } - } - throw new SFException( - ErrorCode.INVALID_VALUE_CONVERT, - columnType, - SnowflakeUtil.FLOAT_STR, - getObjectInternal(columnIndex)); - } else { - return ((Number) obj).floatValue(); - } - } catch (NumberFormatException ex) { - throw new SFException( - ErrorCode.INVALID_VALUE_CONVERT, - columnType, - SnowflakeUtil.FLOAT_STR, - getObjectInternal(columnIndex)); - } + return converters.getNumberConverter().getFloat(obj, columnType); } @Override public double getDouble(int columnIndex) throws SFException { logger.debug("public double getDouble(int columnIndex)", false); - - // Column index starts from 1, not 0. Object obj = getObjectInternal(columnIndex); - - // snow-11974: null for getDouble should return 0 - if (obj == null) { - return 0; - } int columnType = resultSetMetaData.getColumnType(columnIndex); - try { - if (obj instanceof String) { - if (columnType != Types.TIME - && columnType != Types.TIMESTAMP - && columnType != Types.TIMESTAMP_WITH_TIMEZONE) { - if ("inf".equals(obj)) { - return Double.POSITIVE_INFINITY; - } else if ("-inf".equals(obj)) { - return Double.NEGATIVE_INFINITY; - } else { - return Double.parseDouble((String) obj); - } - } - throw new SFException( - ErrorCode.INVALID_VALUE_CONVERT, - columnType, - SnowflakeUtil.DOUBLE_STR, - getObjectInternal(columnIndex)); - } else { - return ((Number) obj).doubleValue(); - } - } catch (NumberFormatException ex) { - throw new SFException( - ErrorCode.INVALID_VALUE_CONVERT, - columnType, - SnowflakeUtil.DOUBLE_STR, - getObjectInternal(columnIndex)); - } + return converters.getNumberConverter().getDouble(obj, columnType); } @Override public byte[] getBytes(int columnIndex) throws SFException { logger.debug("public byte[] getBytes(int columnIndex)", false); - - // Column index starts from 1, not 0. Object obj = getObjectInternal(columnIndex); int columnType = resultSetMetaData.getColumnType(columnIndex); - - if (obj == null) { - return null; - } - - try { - // For all types except time/date/timestamp data, convert data into byte array. Different - // methods are needed - // for different types. - switch (columnType) { - case Types.FLOAT: - case Types.DOUBLE: - return ByteBuffer.allocate(Float8Vector.TYPE_WIDTH) - .putDouble(0, getDouble(columnIndex)) - .array(); - case Types.NUMERIC: - case Types.INTEGER: - case Types.SMALLINT: - case Types.TINYINT: - case Types.BIGINT: - return getBigDecimal(columnIndex).toBigInteger().toByteArray(); - case Types.VARCHAR: - case Types.CHAR: - return getString(columnIndex).getBytes(); - case Types.BOOLEAN: - return getBoolean(columnIndex) ? new byte[] {1} : new byte[] {0}; - case Types.TIMESTAMP: - case Types.TIME: - case Types.DATE: - case Types.DECIMAL: - throw new SFException( - ErrorCode.INVALID_VALUE_CONVERT, - columnType, - SnowflakeUtil.BYTES_STR, - getObjectInternal(columnIndex)); - default: - return SFBinary.fromHex(obj.toString()).getBytes(); - } - } catch (IllegalArgumentException ex) { - throw new SFException( - ErrorCode.INVALID_VALUE_CONVERT, - columnType, - SnowflakeUtil.BYTES_STR, - getObjectInternal(columnIndex)); - } + int columnSubType = resultSetMetaData.getInternalColumnType(columnIndex); + int scale = resultSetMetaData.getScale(columnIndex); + return converters.getBytesConverter().getBytes(obj, columnType, columnSubType, scale); } public Date getDate(int columnIndex) throws SFException { @@ -673,53 +216,11 @@ public Date getDate(int columnIndex) throws SFException { @Override public Date getDate(int columnIndex, TimeZone tz) throws SFException { logger.debug("public Date getDate(int columnIndex)", false); - - // Column index starts from 1, not 0. Object obj = getObjectInternal(columnIndex); - - if (obj == null) { - return null; - } - int columnType = resultSetMetaData.getColumnType(columnIndex); - - if (Types.TIMESTAMP == columnType || Types.TIMESTAMP_WITH_TIMEZONE == columnType) { - if (tz == null) { - tz = TimeZone.getDefault(); - } - int subType = resultSetMetaData.getInternalColumnType(columnIndex); - if (subType == SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_TZ - || subType == SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_LTZ) { - TimeZone specificSessionTimeZone = adjustTimezoneForTimestampTZ(columnIndex); - return new SnowflakeDateWithTimezone( - getTimestamp(columnIndex, tz).getTime(), - specificSessionTimeZone, - resultSetSerializable.getUseSessionTimezone()); - } - return new Date(getTimestamp(columnIndex, tz).getTime()); - - } else if (Types.DATE == columnType) { - if (tz == null || !resultSetSerializable.getFormatDateWithTimeZone()) { - return ArrowResultUtil.getDate(Integer.parseInt((String) obj)); - } - return ArrowResultUtil.getDate(Integer.parseInt((String) obj), tz, sessionTimeZone); - } - // for Types.TIME and all other type, throw user error - else { - throw new SFException( - ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.DATE_STR, obj); - } - } - - private SFTime getSFTime(int columnIndex) throws SFException { - Object obj = getObjectInternal(columnIndex); - - if (obj == null) { - return null; - } - + int columnSubType = resultSetMetaData.getInternalColumnType(columnIndex); int scale = resultSetMetaData.getScale(columnIndex); - return ResultUtil.getSFTime(obj.toString(), scale, session); + return converters.getDateTimeConverter().getDate(obj, columnType, columnSubType, tz, scale); } private Timestamp getTimestamp(int columnIndex) throws SFException { diff --git a/src/main/java/net/snowflake/client/core/SFResultSet.java b/src/main/java/net/snowflake/client/core/SFResultSet.java index bfcdb266c..3a32dde6b 100644 --- a/src/main/java/net/snowflake/client/core/SFResultSet.java +++ b/src/main/java/net/snowflake/client/core/SFResultSet.java @@ -12,6 +12,7 @@ import java.util.Arrays; import java.util.Comparator; import net.snowflake.client.core.BasicEvent.QueryState; +import net.snowflake.client.core.json.Converters; import net.snowflake.client.jdbc.*; import net.snowflake.client.jdbc.telemetry.Telemetry; import net.snowflake.client.jdbc.telemetry.TelemetryData; @@ -87,7 +88,11 @@ public SFResultSet( SFBaseStatement statement, boolean sortResult) throws SQLException { - this(resultSetSerializable, statement.getSFBaseSession().getTelemetryClient(), sortResult); + this( + resultSetSerializable, + statement.getSFBaseSession(), + statement.getSFBaseSession().getTelemetryClient(), + sortResult); this.statement = statement; SFBaseSession session = statement.getSFBaseSession(); @@ -118,6 +123,7 @@ public SFResultSet( * * @param resultSetSerializable data returned in query response * @param telemetryClient telemetryClient + * @param sortResult should sorting take place * @throws SQLException */ public SFResultSet( @@ -125,6 +131,41 @@ public SFResultSet( Telemetry telemetryClient, boolean sortResult) throws SQLException { + this(resultSetSerializable, new SFSession(), telemetryClient, sortResult); + } + + /** + * This is a minimum initialization for SFResultSet. Mainly used for testing purpose. However, + * real prod constructor will call this constructor as well + * + * @param resultSetSerializable data returned in query response + * @param session snowflake session + * @param telemetryClient telemetryClient + * @param sortResult should sorting take place + * @throws SQLException + */ + public SFResultSet( + SnowflakeResultSetSerializableV1 resultSetSerializable, + SFBaseSession session, + Telemetry telemetryClient, + boolean sortResult) + throws SQLException { + super( + resultSetSerializable.getTimeZone(), + new Converters( + resultSetSerializable.getTimeZone(), + session, + resultSetSerializable.getResultVersion(), + resultSetSerializable.isHonorClientTZForTimestampNTZ(), + resultSetSerializable.getTreatNTZAsUTC(), + resultSetSerializable.getUseSessionTimezone(), + resultSetSerializable.getFormatDateWithTimeZone(), + resultSetSerializable.getBinaryFormatter(), + resultSetSerializable.getDateFormatter(), + resultSetSerializable.getTimeFormatter(), + resultSetSerializable.getTimestampNTZFormatter(), + resultSetSerializable.getTimestampLTZFormatter(), + resultSetSerializable.getTimestampTZFormatter())); this.resultSetSerializable = resultSetSerializable; this.columnCount = 0; this.sortResult = sortResult; @@ -145,7 +186,6 @@ public SFResultSet( this.timestampTZFormatter = resultSetSerializable.getTimestampTZFormatter(); this.dateFormatter = resultSetSerializable.getDateFormatter(); this.timeFormatter = resultSetSerializable.getTimeFormatter(); - this.sessionTimeZone = resultSetSerializable.getTimeZone(); this.honorClientTZForTimestampNTZ = resultSetSerializable.isHonorClientTZForTimestampNTZ(); this.binaryFormatter = resultSetSerializable.getBinaryFormatter(); this.resultVersion = resultSetSerializable.getResultVersion(); diff --git a/src/main/java/net/snowflake/client/core/json/BooleanConverter.java b/src/main/java/net/snowflake/client/core/json/BooleanConverter.java new file mode 100644 index 000000000..f644c3461 --- /dev/null +++ b/src/main/java/net/snowflake/client/core/json/BooleanConverter.java @@ -0,0 +1,37 @@ +package net.snowflake.client.core.json; + +import java.sql.Types; +import net.snowflake.client.core.SFException; +import net.snowflake.client.jdbc.ErrorCode; +import net.snowflake.client.jdbc.SnowflakeUtil; + +public class BooleanConverter { + public Boolean getBoolean(Object obj, int columnType) throws SFException { + if (obj == null) { + return false; + } + if (obj instanceof Boolean) { + return (Boolean) obj; + } + // if type is an approved type that can be converted to Boolean, do this + if (columnType == Types.BOOLEAN + || columnType == Types.INTEGER + || columnType == Types.SMALLINT + || columnType == Types.TINYINT + || columnType == Types.BIGINT + || columnType == Types.BIT + || columnType == Types.VARCHAR + || columnType == Types.CHAR + || columnType == Types.DECIMAL) { + String type = obj.toString(); + if ("1".equals(type) || Boolean.TRUE.toString().equalsIgnoreCase(type)) { + return true; + } + if ("0".equals(type) || Boolean.FALSE.toString().equalsIgnoreCase(type)) { + return false; + } + } + throw new SFException( + ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.BOOLEAN_STR, obj); + } +} diff --git a/src/main/java/net/snowflake/client/core/json/BytesConverter.java b/src/main/java/net/snowflake/client/core/json/BytesConverter.java new file mode 100644 index 000000000..9e4b01ef5 --- /dev/null +++ b/src/main/java/net/snowflake/client/core/json/BytesConverter.java @@ -0,0 +1,68 @@ +package net.snowflake.client.core.json; + +import java.nio.ByteBuffer; +import java.sql.Types; +import net.snowflake.client.core.SFException; +import net.snowflake.client.jdbc.ErrorCode; +import net.snowflake.client.jdbc.SnowflakeUtil; +import net.snowflake.common.core.SFBinary; +import org.apache.arrow.vector.Float8Vector; + +public class BytesConverter { + private final Converters converters; + + BytesConverter(Converters converters) { + this.converters = converters; + } + + public byte[] getBytes(Object obj, int columnType, int columnSubType, Integer scale) + throws SFException { + if (obj == null) { + return null; + } + + try { + // For all types except time/date/timestamp data, convert data into byte array. Different + // methods are needed + // for different types. + switch (columnType) { + case Types.FLOAT: + case Types.DOUBLE: + return ByteBuffer.allocate(Float8Vector.TYPE_WIDTH) + .putDouble(0, converters.getNumberConverter().getDouble(obj, columnType)) + .array(); + case Types.NUMERIC: + case Types.INTEGER: + case Types.SMALLINT: + case Types.TINYINT: + case Types.BIGINT: + return converters + .getNumberConverter() + .getBigDecimal(obj, columnType, scale) + .toBigInteger() + .toByteArray(); + case Types.VARCHAR: + case Types.CHAR: + return converters + .getStringConverter() + .getString(obj, columnType, columnSubType, scale) + .getBytes(); + case Types.BOOLEAN: + return converters.getBooleanConverter().getBoolean(obj, columnType) + ? new byte[] {1} + : new byte[] {0}; + case Types.TIMESTAMP: + case Types.TIME: + case Types.DATE: + case Types.DECIMAL: + throw new SFException( + ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.BYTES_STR, obj); + default: + return SFBinary.fromHex(obj.toString()).getBytes(); + } + } catch (IllegalArgumentException ex) { + throw new SFException( + ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.BYTES_STR, obj); + } + } +} diff --git a/src/main/java/net/snowflake/client/core/json/Converters.java b/src/main/java/net/snowflake/client/core/json/Converters.java new file mode 100644 index 000000000..fa3baadb6 --- /dev/null +++ b/src/main/java/net/snowflake/client/core/json/Converters.java @@ -0,0 +1,74 @@ +package net.snowflake.client.core.json; + +import java.util.TimeZone; +import net.snowflake.client.core.SFBaseSession; +import net.snowflake.common.core.SFBinaryFormat; +import net.snowflake.common.core.SnowflakeDateTimeFormat; + +public class Converters { + private final BooleanConverter booleanConverter; + private final NumberConverter numberConverter; + private final DateTimeConverter dateTimeConverter; + private final BytesConverter bytesConverter; + private final StringConverter stringConverter; + + public Converters( + TimeZone sessionTimeZone, + SFBaseSession session, + long resultVersion, + boolean honorClientTZForTimestampNTZ, + boolean treatNTZAsUTC, + boolean useSessionTimezone, + boolean formatDateWithTimeZone, + SFBinaryFormat binaryFormatter, + SnowflakeDateTimeFormat dateFormatter, + SnowflakeDateTimeFormat timeFormatter, + SnowflakeDateTimeFormat timestampNTZFormatter, + SnowflakeDateTimeFormat timestampLTZFormatter, + SnowflakeDateTimeFormat timestampTZFormatter) { + booleanConverter = new BooleanConverter(); + numberConverter = new NumberConverter(); + dateTimeConverter = + new DateTimeConverter( + sessionTimeZone, + session, + resultVersion, + honorClientTZForTimestampNTZ, + treatNTZAsUTC, + useSessionTimezone, + formatDateWithTimeZone); + bytesConverter = new BytesConverter(this); + stringConverter = + new StringConverter( + sessionTimeZone, + binaryFormatter, + dateFormatter, + timeFormatter, + timestampNTZFormatter, + timestampLTZFormatter, + timestampTZFormatter, + resultVersion, + session, + this); + } + + public BooleanConverter getBooleanConverter() { + return booleanConverter; + } + + public NumberConverter getNumberConverter() { + return numberConverter; + } + + public DateTimeConverter getDateTimeConverter() { + return dateTimeConverter; + } + + public BytesConverter getBytesConverter() { + return bytesConverter; + } + + public StringConverter getStringConverter() { + return stringConverter; + } +} diff --git a/src/main/java/net/snowflake/client/core/json/DateTimeConverter.java b/src/main/java/net/snowflake/client/core/json/DateTimeConverter.java new file mode 100644 index 000000000..2622e259a --- /dev/null +++ b/src/main/java/net/snowflake/client/core/json/DateTimeConverter.java @@ -0,0 +1,195 @@ +package net.snowflake.client.core.json; + +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.TimeZone; +import net.snowflake.client.core.ResultUtil; +import net.snowflake.client.core.SFBaseSession; +import net.snowflake.client.core.SFException; +import net.snowflake.client.core.arrow.ArrowResultUtil; +import net.snowflake.client.jdbc.ErrorCode; +import net.snowflake.client.jdbc.SnowflakeDateWithTimezone; +import net.snowflake.client.jdbc.SnowflakeTimeWithTimezone; +import net.snowflake.client.jdbc.SnowflakeTimestampWithTimezone; +import net.snowflake.client.jdbc.SnowflakeUtil; +import net.snowflake.common.core.SFTime; +import net.snowflake.common.core.SFTimestamp; + +public class DateTimeConverter { + private final TimeZone sessionTimeZone; + private final long resultVersion; + private final boolean honorClientTZForTimestampNTZ; + private final boolean treatNTZAsUTC; + private final boolean useSessionTimezone; + private final boolean formatDateWithTimeZone; + private final SFBaseSession session; + + public DateTimeConverter( + TimeZone sessionTimeZone, + SFBaseSession session, + long resultVersion, + boolean honorClientTZForTimestampNTZ, + boolean treatNTZAsUTC, + boolean useSessionTimezone, + boolean formatDateWithTimeZone) { + this.sessionTimeZone = sessionTimeZone; + this.session = session; + this.resultVersion = resultVersion; + this.honorClientTZForTimestampNTZ = honorClientTZForTimestampNTZ; + this.treatNTZAsUTC = treatNTZAsUTC; + this.useSessionTimezone = useSessionTimezone; + this.formatDateWithTimeZone = formatDateWithTimeZone; + } + + public Timestamp getTimestamp( + Object obj, int columnType, int columnSubType, TimeZone tz, int scale) throws SFException { + if (obj == null) { + return null; + } + if (Types.TIMESTAMP == columnType || Types.TIMESTAMP_WITH_TIMEZONE == columnType) { + if (tz == null) { + tz = TimeZone.getDefault(); + } + SFTimestamp sfTS = + ResultUtil.getSFTimestamp( + obj.toString(), scale, columnSubType, resultVersion, sessionTimeZone, session); + + Timestamp res = sfTS.getTimestamp(); + if (res == null) { + return null; + } + // If we want to display format with no session offset, we have to use session timezone for + // ltz and tz types but UTC timezone for ntz type. + if (useSessionTimezone) { + if (columnSubType == SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_LTZ + || columnSubType == SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_TZ) { + TimeZone specificSessionTimezone = adjustTimezoneForTimestampTZ(obj, columnSubType); + res = new SnowflakeTimestampWithTimezone(res, specificSessionTimezone); + } else { + res = new SnowflakeTimestampWithTimezone(res); + } + } + // If timestamp type is NTZ and JDBC_TREAT_TIMESTAMP_NTZ_AS_UTC=true, keep + // timezone in UTC to avoid daylight savings errors + else if (treatNTZAsUTC && columnSubType == Types.TIMESTAMP) { + res = new SnowflakeTimestampWithTimezone(res); + } + // If JDBC_TREAT_TIMESTAMP_NTZ_AS_UTC=false, default behavior is to honor + // client timezone for NTZ time. Move NTZ timestamp offset to correspond to + // client's timezone. JDBC_USE_SESSION_TIMEZONE overrides other params. + if (columnSubType == Types.TIMESTAMP + && ((!treatNTZAsUTC && honorClientTZForTimestampNTZ) || useSessionTimezone)) { + res = sfTS.moveToTimeZone(tz).getTimestamp(); + } + // Adjust time if date happens before year 1582 for difference between + // Julian and Gregorian calendars + return ResultUtil.adjustTimestamp(res); + } else if (Types.DATE == columnType) { + Date d = getDate(obj, columnType, columnSubType, tz, scale); + if (d == null) { + return null; + } + return new Timestamp(d.getTime()); + } else if (Types.TIME == columnType) { + Time t = getTime(obj, columnType, columnSubType, tz, scale); + if (t == null) { + return null; + } + if (useSessionTimezone) { + SFTime sfTime = ResultUtil.getSFTime(obj.toString(), scale, session); + return new SnowflakeTimestampWithTimezone( + sfTime.getFractionalSeconds(ResultUtil.DEFAULT_SCALE_OF_SFTIME_FRACTION_SECONDS), + sfTime.getNanosecondsWithinSecond(), + TimeZone.getTimeZone("UTC")); + } + return new Timestamp(t.getTime()); + } else { + throw new SFException( + ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.TIMESTAMP_STR, obj); + } + } + + public Time getTime(Object obj, int columnType, int columnSubType, TimeZone tz, int scale) + throws SFException { + if (obj == null) { + return null; + } + if (Types.TIME == columnType) { + SFTime sfTime = ResultUtil.getSFTime(obj.toString(), scale, session); + Time ts = + new Time( + sfTime.getFractionalSeconds(ResultUtil.DEFAULT_SCALE_OF_SFTIME_FRACTION_SECONDS)); + if (useSessionTimezone) { + ts = + SnowflakeUtil.getTimeInSessionTimezone( + SnowflakeUtil.getSecondsFromMillis(ts.getTime()), + sfTime.getNanosecondsWithinSecond()); + } + return ts; + } else if (Types.TIMESTAMP == columnType || Types.TIMESTAMP_WITH_TIMEZONE == columnType) { + Timestamp ts = getTimestamp(obj, columnType, columnSubType, tz, scale); + if (ts == null) { + return null; + } + if (useSessionTimezone) { + ts = getTimestamp(obj, columnType, columnSubType, sessionTimeZone, scale); + TimeZone sessionTimeZone = adjustTimezoneForTimestampTZ(obj, columnSubType); + return new SnowflakeTimeWithTimezone(ts, sessionTimeZone, useSessionTimezone); + } + return new Time(ts.getTime()); + } else { + throw new SFException( + ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.TIME_STR, obj); + } + } + + public Date getDate(Object obj, int columnType, int columnSubType, TimeZone tz, int scale) + throws SFException { + if (obj == null) { + return null; + } + + if (Types.TIMESTAMP == columnType || Types.TIMESTAMP_WITH_TIMEZONE == columnType) { + if (tz == null) { + tz = TimeZone.getDefault(); + } + if (columnSubType == SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_TZ + || columnSubType == SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_LTZ) { + TimeZone specificSessionTimeZone = adjustTimezoneForTimestampTZ(obj, columnSubType); + return new SnowflakeDateWithTimezone( + getTimestamp(obj, columnType, columnSubType, tz, scale).getTime(), + specificSessionTimeZone, + useSessionTimezone); + } + return new Date(getTimestamp(obj, columnType, columnSubType, tz, scale).getTime()); + + } else if (Types.DATE == columnType) { + if (tz == null || !formatDateWithTimeZone) { + return ArrowResultUtil.getDate(Integer.parseInt((String) obj)); + } + return ArrowResultUtil.getDate(Integer.parseInt((String) obj), tz, sessionTimeZone); + } + // for Types.TIME and all other type, throw user error + else { + throw new SFException( + ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.DATE_STR, obj); + } + } + + private TimeZone adjustTimezoneForTimestampTZ(Object obj, int columnSubType) { + // If the timestamp is of type timestamp_tz, use the associated offset timezone instead of the + // session timezone for formatting + if (obj != null + && columnSubType == SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_TZ + && resultVersion > 0) { + String timestampStr = obj.toString(); + int indexForSeparator = timestampStr.indexOf(' '); + String timezoneIndexStr = timestampStr.substring(indexForSeparator + 1); + return SFTimestamp.convertTimezoneIndexToTimeZone(Integer.parseInt(timezoneIndexStr)); + } + // By default, return session timezone + return sessionTimeZone; + } +} diff --git a/src/main/java/net/snowflake/client/core/json/NumberConverter.java b/src/main/java/net/snowflake/client/core/json/NumberConverter.java new file mode 100644 index 000000000..132003359 --- /dev/null +++ b/src/main/java/net/snowflake/client/core/json/NumberConverter.java @@ -0,0 +1,192 @@ +package net.snowflake.client.core.json; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.Types; +import net.snowflake.client.core.SFException; +import net.snowflake.client.jdbc.ErrorCode; +import net.snowflake.client.jdbc.SnowflakeUtil; + +public class NumberConverter { + // Precision of maximum long value in Java (2^63-1). Precision is 19 + private static final int LONG_PRECISION = 19; + + private static final BigDecimal MAX_LONG_VAL = new BigDecimal(Long.MAX_VALUE); + private static final BigDecimal MIN_LONG_VAL = new BigDecimal(Long.MIN_VALUE); + + public byte getByte(Object obj) { + if (obj == null) { + return 0; + } + + if (obj instanceof String) { + return Byte.parseByte((String) obj); + } else { + return ((Number) obj).byteValue(); + } + } + + public short getShort(Object obj, int columnType) throws SFException { + if (obj == null) { + return 0; + } + try { + if (obj instanceof String) { + String objString = (String) obj; + if (objString.contains(".") && (columnType == Types.FLOAT || columnType == Types.DOUBLE)) { + objString = objString.substring(0, objString.indexOf(".")); + } + return Short.parseShort(objString); + } else { + return ((Number) obj).shortValue(); + } + } catch (NumberFormatException ex) { + throw new SFException( + ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.SHORT_STR, obj); + } + } + + public int getInt(Object obj, int columnType) throws SFException { + if (obj == null) { + return 0; + } + try { + if (obj instanceof String) { + String objString = (String) obj; + if (objString.contains(".") && (columnType == Types.FLOAT || columnType == Types.DOUBLE)) { + objString = objString.substring(0, objString.indexOf(".")); + } + return Integer.parseInt(objString); + } else { + return ((Number) obj).intValue(); + } + } catch (NumberFormatException ex) { + throw new SFException( + ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.INT_STR, obj); + } + } + + public long getLong(Object obj, int columnType) throws SFException { + if (obj == null) { + return 0; + } + try { + if (obj instanceof String) { + String objString = (String) obj; + if (objString.contains(".") && (columnType == Types.FLOAT || columnType == Types.DOUBLE)) { + objString = objString.substring(0, objString.indexOf(".")); + } + return Long.parseLong(objString); + } else { + return ((Number) obj).longValue(); + } + } catch (NumberFormatException nfe) { + + if (Types.INTEGER == columnType || Types.SMALLINT == columnType) { + throw new SFException( + ErrorCode.INTERNAL_ERROR, SnowflakeUtil.LONG_STR + ": " + obj.toString()); + } else { + throw new SFException( + ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.LONG_STR, obj); + } + } + } + + public BigDecimal getBigDecimal(Object obj, int columnType) throws SFException { + if (obj == null) { + return null; + } + try { + if (columnType != Types.TIME + && columnType != Types.TIMESTAMP + && columnType != Types.TIMESTAMP_WITH_TIMEZONE) { + return new BigDecimal(obj.toString()); + } + throw new SFException( + ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.BIG_DECIMAL_STR, obj); + + } catch (NumberFormatException ex) { + throw new SFException( + ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.BIG_DECIMAL_STR, obj); + } + } + + public BigDecimal getBigDecimal(Object obj, int columnType, Integer scale) throws SFException { + if (obj == null) { + return null; + } + BigDecimal value = new BigDecimal(obj.toString()); + value = value.setScale(scale, RoundingMode.HALF_UP); + return value; + } + + public float getFloat(Object obj, int columnType) throws SFException { + if (obj == null) { + return 0; + } + + try { + if (obj instanceof String) { + if (columnType != Types.TIME + && columnType != Types.TIMESTAMP + && columnType != Types.TIMESTAMP_WITH_TIMEZONE) { + if ("inf".equals(obj)) { + return Float.POSITIVE_INFINITY; + } else if ("-inf".equals(obj)) { + return Float.NEGATIVE_INFINITY; + } else { + return Float.parseFloat((String) obj); + } + } + throw new SFException( + ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.FLOAT_STR, obj); + } else { + return ((Number) obj).floatValue(); + } + } catch (NumberFormatException ex) { + throw new SFException( + ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.FLOAT_STR, obj); + } + } + + public double getDouble(Object obj, int columnType) throws SFException { + if (obj == null) { + return 0; + } + try { + if (obj instanceof String) { + if (columnType != Types.TIME + && columnType != Types.TIMESTAMP + && columnType != Types.TIMESTAMP_WITH_TIMEZONE) { + if ("inf".equals(obj)) { + return Double.POSITIVE_INFINITY; + } else if ("-inf".equals(obj)) { + return Double.NEGATIVE_INFINITY; + } else { + return Double.parseDouble((String) obj); + } + } + throw new SFException( + ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.DOUBLE_STR, obj); + } else { + return ((Number) obj).doubleValue(); + } + } catch (NumberFormatException ex) { + throw new SFException( + ErrorCode.INVALID_VALUE_CONVERT, columnType, SnowflakeUtil.DOUBLE_STR, obj); + } + } + + public Object getBigInt(Object obj, int columnType) throws SFException { + // If precision is < precision of max long precision, we can automatically convert to long. + // Otherwise, do a check to ensure it doesn't overflow max long value. + String numberAsString = obj.toString(); + if (numberAsString.length() >= LONG_PRECISION) { + BigDecimal bigNum = getBigDecimal(obj, columnType); + if (bigNum.compareTo(MAX_LONG_VAL) == 1 || bigNum.compareTo(MIN_LONG_VAL) == -1) { + return bigNum; + } + } + return getLong(obj, columnType); + } +} diff --git a/src/main/java/net/snowflake/client/core/json/StringConverter.java b/src/main/java/net/snowflake/client/core/json/StringConverter.java new file mode 100644 index 000000000..4b5684910 --- /dev/null +++ b/src/main/java/net/snowflake/client/core/json/StringConverter.java @@ -0,0 +1,155 @@ +package net.snowflake.client.core.json; + +import java.sql.Date; +import java.sql.Types; +import java.util.TimeZone; +import net.snowflake.client.core.ResultUtil; +import net.snowflake.client.core.SFBaseSession; +import net.snowflake.client.core.SFException; +import net.snowflake.client.jdbc.ErrorCode; +import net.snowflake.client.jdbc.SnowflakeUtil; +import net.snowflake.client.log.ArgSupplier; +import net.snowflake.client.log.SFLogger; +import net.snowflake.client.log.SFLoggerFactory; +import net.snowflake.common.core.SFBinary; +import net.snowflake.common.core.SFBinaryFormat; +import net.snowflake.common.core.SFTime; +import net.snowflake.common.core.SFTimestamp; +import net.snowflake.common.core.SnowflakeDateTimeFormat; + +public class StringConverter { + private static final SFLogger logger = SFLoggerFactory.getLogger(StringConverter.class); + private final TimeZone sessionTimeZone; + private final SFBinaryFormat binaryFormatter; + private final SnowflakeDateTimeFormat dateFormatter; + private final SnowflakeDateTimeFormat timeFormatter; + private final SnowflakeDateTimeFormat timestampNTZFormatter; + private final SnowflakeDateTimeFormat timestampLTZFormatter; + private final SnowflakeDateTimeFormat timestampTZFormatter; + private final long resultVersion; + private final SFBaseSession session; + private final Converters converters; + + public StringConverter( + TimeZone sessionTimeZone, + SFBinaryFormat binaryFormatter, + SnowflakeDateTimeFormat dateFormatter, + SnowflakeDateTimeFormat timeFormatter, + SnowflakeDateTimeFormat timestampNTZFormatter, + SnowflakeDateTimeFormat timestampLTZFormatter, + SnowflakeDateTimeFormat timestampTZFormatter, + long resultVersion, + SFBaseSession session, + Converters converters) { + this.sessionTimeZone = sessionTimeZone; + this.binaryFormatter = binaryFormatter; + this.dateFormatter = dateFormatter; + this.timeFormatter = timeFormatter; + this.timestampNTZFormatter = timestampNTZFormatter; + this.timestampLTZFormatter = timestampLTZFormatter; + this.timestampTZFormatter = timestampTZFormatter; + this.resultVersion = resultVersion; + this.session = session; + this.converters = converters; + } + + public String getString(Object obj, int columnType, int columnSubType, int scale) + throws SFException { + if (obj == null) { + return null; + } + + switch (columnType) { + case Types.BOOLEAN: + return ResultUtil.getBooleanAsString(ResultUtil.getBoolean(obj.toString())); + + case Types.TIMESTAMP: + case SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_LTZ: + case SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_TZ: + return timestampToString(obj, columnType, columnSubType, scale); + case Types.DATE: + return dateToString(obj, columnType, columnSubType, scale); + case Types.TIME: + return timeToString(obj, scale); + case Types.BINARY: + return binaryToString(obj, columnType, columnSubType, scale); + default: + break; + } + return obj.toString(); + } + + private String timestampToString(Object obj, int columnType, int columnSubType, int scale) + throws SFException { + SFTimestamp sfTS = + ResultUtil.getSFTimestamp( + obj.toString(), scale, columnSubType, resultVersion, sessionTimeZone, session); + + String timestampStr = + ResultUtil.getSFTimestampAsString( + sfTS, + columnType, + scale, + timestampNTZFormatter, + timestampLTZFormatter, + timestampTZFormatter, + session); + + logger.debug( + "Converting timestamp to string from: {} to: {}", + (ArgSupplier) obj::toString, + timestampStr); + + return timestampStr; + } + + private String dateToString(Object obj, int columnType, int columnSubType, int scale) + throws SFException { + Date date = + converters + .getDateTimeConverter() + .getDate(obj, columnType, columnSubType, TimeZone.getDefault(), scale); + + if (dateFormatter == null) { + throw new SFException(ErrorCode.INTERNAL_ERROR, "missing date formatter"); + } + + String dateStr = ResultUtil.getDateAsString(date, dateFormatter); + + logger.debug("Converting date to string from: {} to: {}", (ArgSupplier) obj::toString, dateStr); + + return dateStr; + } + + private String timeToString(Object obj, int scale) throws SFException { + SFTime sfTime = ResultUtil.getSFTime(obj.toString(), scale, session); + + if (timeFormatter == null) { + throw new SFException(ErrorCode.INTERNAL_ERROR, "missing time formatter"); + } + + String timeStr = ResultUtil.getSFTimeAsString(sfTime, scale, timeFormatter); + + logger.debug("Converting time to string from: {} to: {}", (ArgSupplier) obj::toString, timeStr); + + return timeStr; + } + + private String binaryToString(Object obj, int columnType, int columnSubType, int scale) + throws SFException { + if (binaryFormatter == null) { + throw new SFException(ErrorCode.INTERNAL_ERROR, "missing binary formatter"); + } + + if (binaryFormatter == SFBinaryFormat.HEX) { + // Shortcut: the values are already passed with hex encoding, so just + // return the string unchanged rather than constructing an SFBinary. + return obj.toString(); + } + + SFBinary sfb = + new SFBinary( + converters.getBytesConverter().getBytes(obj, columnType, columnSubType, scale)); + return binaryFormatter.format(sfb); + } +} diff --git a/src/main/java/net/snowflake/client/jdbc/RestRequest.java b/src/main/java/net/snowflake/client/jdbc/RestRequest.java index b31227223..b6f1f7780 100644 --- a/src/main/java/net/snowflake/client/jdbc/RestRequest.java +++ b/src/main/java/net/snowflake/client/jdbc/RestRequest.java @@ -7,12 +7,11 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.util.concurrent.atomic.AtomicBoolean; -import javax.net.ssl.*; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLKeyException; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLProtocolException; import net.snowflake.client.core.*; -import net.snowflake.client.core.Event; -import net.snowflake.client.core.EventUtil; -import net.snowflake.client.core.HttpUtil; -import net.snowflake.client.core.SFOCSPException; import net.snowflake.client.jdbc.telemetryOOB.TelemetryService; import net.snowflake.client.log.ArgSupplier; import net.snowflake.client.log.SFLogger; diff --git a/src/main/java/net/snowflake/client/jdbc/SnowflakeResultSetSerializableV1.java b/src/main/java/net/snowflake/client/jdbc/SnowflakeResultSetSerializableV1.java index 764acdb9b..d5cf0c1c2 100644 --- a/src/main/java/net/snowflake/client/jdbc/SnowflakeResultSetSerializableV1.java +++ b/src/main/java/net/snowflake/client/jdbc/SnowflakeResultSetSerializableV1.java @@ -1087,7 +1087,9 @@ private ResultSet getResultSetInternal(Properties info) throws SQLException { } case JSON: { - sfBaseResultSet = new SFResultSet(this, telemetryClient, sortResult); + sfBaseResultSet = + new SFResultSet( + this, getSession().orElse(new SFSession()), telemetryClient, sortResult); break; } default: diff --git a/src/test/java/net/snowflake/client/core/json/BooleanConverterTest.java b/src/test/java/net/snowflake/client/core/json/BooleanConverterTest.java new file mode 100644 index 000000000..2162d651a --- /dev/null +++ b/src/test/java/net/snowflake/client/core/json/BooleanConverterTest.java @@ -0,0 +1,51 @@ +package net.snowflake.client.core.json; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import java.sql.Types; +import net.snowflake.client.core.SFException; +import org.junit.Test; + +public class BooleanConverterTest { + private final BooleanConverter booleanConverter = new BooleanConverter(); + + @Test + public void testConvertBoolean() throws SFException { + assertThat(booleanConverter.getBoolean(true, Types.BOOLEAN), equalTo(true)); + assertThat(booleanConverter.getBoolean(false, Types.BOOLEAN), equalTo(false)); + } + + @Test + public void testConvertNumeric() throws SFException { + assertThat(booleanConverter.getBoolean(1, Types.INTEGER), equalTo(true)); + assertThat(booleanConverter.getBoolean(1, Types.SMALLINT), equalTo(true)); + assertThat(booleanConverter.getBoolean(1, Types.TINYINT), equalTo(true)); + assertThat(booleanConverter.getBoolean(1, Types.BIGINT), equalTo(true)); + assertThat(booleanConverter.getBoolean(1, Types.BIT), equalTo(true)); + assertThat(booleanConverter.getBoolean(1, Types.DECIMAL), equalTo(true)); + assertThat(booleanConverter.getBoolean(0, Types.INTEGER), equalTo(false)); + assertThat(booleanConverter.getBoolean(0, Types.SMALLINT), equalTo(false)); + assertThat(booleanConverter.getBoolean(0, Types.TINYINT), equalTo(false)); + assertThat(booleanConverter.getBoolean(0, Types.BIGINT), equalTo(false)); + assertThat(booleanConverter.getBoolean(0, Types.BIT), equalTo(false)); + assertThat(booleanConverter.getBoolean(0, Types.DECIMAL), equalTo(false)); + } + + @Test + public void testConvertString() throws SFException { + assertThat(booleanConverter.getBoolean("1", Types.VARCHAR), equalTo(true)); + assertThat(booleanConverter.getBoolean("1", Types.CHAR), equalTo(true)); + assertThat(booleanConverter.getBoolean("true", Types.VARCHAR), equalTo(true)); + assertThat(booleanConverter.getBoolean("TRUE", Types.CHAR), equalTo(true)); + assertThat(booleanConverter.getBoolean("0", Types.VARCHAR), equalTo(false)); + assertThat(booleanConverter.getBoolean("0", Types.CHAR), equalTo(false)); + assertThat(booleanConverter.getBoolean("false", Types.VARCHAR), equalTo(false)); + assertThat(booleanConverter.getBoolean("FALSE", Types.CHAR), equalTo(false)); + } + + @Test(expected = SFException.class) + public void testConvertOtherType() throws SFException { + booleanConverter.getBoolean("1", Types.BINARY); + } +} diff --git a/src/test/java/net/snowflake/client/core/json/BytesConverterTest.java b/src/test/java/net/snowflake/client/core/json/BytesConverterTest.java new file mode 100644 index 000000000..47e898486 --- /dev/null +++ b/src/test/java/net/snowflake/client/core/json/BytesConverterTest.java @@ -0,0 +1,51 @@ +package net.snowflake.client.core.json; + +import static org.junit.Assert.assertArrayEquals; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.sql.Types; +import net.snowflake.client.core.SFException; +import net.snowflake.client.core.SFSession; +import org.apache.arrow.vector.Float8Vector; +import org.junit.Test; + +public class BytesConverterTest { + private final Converters converters = + new Converters( + null, new SFSession(), 0, false, false, false, false, null, null, null, null, null, null); + private final BytesConverter bytesConverter = new BytesConverter(converters); + + @Test + public void testConvertFloatingPointToBytes() throws SFException { + byte[] expected = ByteBuffer.allocate(Float8Vector.TYPE_WIDTH).putDouble(0, 12.5).array(); + assertArrayEquals(expected, bytesConverter.getBytes(12.5f, Types.FLOAT, Types.FLOAT, 0)); + assertArrayEquals(expected, bytesConverter.getBytes(12.5f, Types.DOUBLE, Types.DOUBLE, 0)); + assertArrayEquals(expected, bytesConverter.getBytes(12.5, Types.FLOAT, Types.DOUBLE, 0)); + assertArrayEquals(expected, bytesConverter.getBytes(12.5, Types.DOUBLE, Types.DOUBLE, 0)); + } + + @Test + public void testConvertIntegerNumberToBytes() throws SFException { + byte[] expected = new BigInteger("12").toByteArray(); + assertArrayEquals(expected, bytesConverter.getBytes(12, Types.NUMERIC, Types.NUMERIC, 0)); + assertArrayEquals(expected, bytesConverter.getBytes(12, Types.INTEGER, Types.INTEGER, 0)); + assertArrayEquals(expected, bytesConverter.getBytes(12, Types.SMALLINT, Types.SMALLINT, 0)); + assertArrayEquals(expected, bytesConverter.getBytes(12, Types.TINYINT, Types.TINYINT, 0)); + assertArrayEquals(expected, bytesConverter.getBytes(12, Types.BIGINT, Types.BIGINT, 0)); + } + + @Test + public void testString() throws SFException { + assertArrayEquals( + "abc".getBytes(), bytesConverter.getBytes("abc", Types.VARCHAR, Types.VARCHAR, 0)); + } + + @Test + public void testConvertBooleanToBytes() throws SFException { + assertArrayEquals( + new byte[] {1}, bytesConverter.getBytes(true, Types.BOOLEAN, Types.BOOLEAN, 0)); + assertArrayEquals( + new byte[] {0}, bytesConverter.getBytes(false, Types.BOOLEAN, Types.BOOLEAN, 0)); + } +} diff --git a/src/test/java/net/snowflake/client/core/json/DateTimeConverterTest.java b/src/test/java/net/snowflake/client/core/json/DateTimeConverterTest.java new file mode 100644 index 000000000..985264f3e --- /dev/null +++ b/src/test/java/net/snowflake/client/core/json/DateTimeConverterTest.java @@ -0,0 +1,147 @@ +package net.snowflake.client.core.json; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.util.TimeZone; +import net.snowflake.client.core.SFException; +import net.snowflake.client.core.SFSession; +import net.snowflake.client.jdbc.SnowflakeUtil; +import org.junit.Test; + +public class DateTimeConverterTest { + private final TimeZone honoluluTimeZone = + TimeZone.getTimeZone(ZoneId.of("Pacific/Honolulu")); // session time zone + private final TimeZone nuukTimeZone = TimeZone.getTimeZone(ZoneId.of("America/Nuuk")); + private final DateTimeConverter dateTimeConverter = + new DateTimeConverter(honoluluTimeZone, new SFSession(), 1, true, false, false, false); + private final DateTimeConverter dateTimeConverterWithUseSessionTimeZone = + new DateTimeConverter(honoluluTimeZone, new SFSession(), 1, true, false, true, false); + private final DateTimeConverter dateTimeConverterWithTreatNTZAsUTC = + new DateTimeConverter(honoluluTimeZone, new SFSession(), 1, true, true, false, false); + + @Test + public void testGetVariousTypesWhenNullObjectGiven() throws SFException { + assertNull(dateTimeConverter.getTimestamp(null, Types.TIMESTAMP, Types.TIMESTAMP, null, 0)); + assertNull(dateTimeConverter.getTime(null, Types.TIMESTAMP, Types.TIMESTAMP, null, 0)); + assertNull(dateTimeConverter.getDate(null, Types.TIMESTAMP, Types.TIMESTAMP, null, 0)); + } + + @Test + public void testGetTimestampWithDefaultTimeZone() throws SFException { + assertEquals( + Timestamp.valueOf(LocalDateTime.of(2023, 8, 9, 8, 2, 3)), + dateTimeConverter.getTimestamp("1691568123", Types.TIMESTAMP, Types.TIMESTAMP, null, 0)); + assertEquals( + Timestamp.valueOf(LocalDateTime.of(2023, 8, 9, 8, 2, 3, 456789000)), + dateTimeConverter.getTimestamp( + "1691568123.456789", Types.TIMESTAMP, Types.TIMESTAMP, null, 0)); + assertEquals( + Timestamp.valueOf(LocalDateTime.of(2023, 8, 9, 8, 2, 3, 456789123)), + dateTimeConverter.getTimestamp( + "1691568123.456789123", Types.TIMESTAMP, Types.TIMESTAMP, null, 0)); + } + + @Test + public void testGetTimestampWithSpecificTimeZone() throws SFException { + assertEquals( + Timestamp.valueOf(LocalDateTime.of(2023, 8, 9, 8, 2, 3)).toString(), + dateTimeConverterWithTreatNTZAsUTC + .getTimestamp("1691568123", Types.TIMESTAMP, Types.TIMESTAMP, nuukTimeZone, 0) + .toString()); + assertEquals( + Timestamp.valueOf(LocalDateTime.of(2023, 8, 9, 8, 2, 3, 456789000)).toString(), + dateTimeConverterWithTreatNTZAsUTC + .getTimestamp("1691568123.456789", Types.TIMESTAMP, Types.TIMESTAMP, nuukTimeZone, 0) + .toString()); + assertEquals( + Timestamp.valueOf(LocalDateTime.of(2023, 8, 9, 8, 2, 3, 456789123)).toString(), + dateTimeConverterWithTreatNTZAsUTC + .getTimestamp("1691568123.456789123", Types.TIMESTAMP, Types.TIMESTAMP, nuukTimeZone, 0) + .toString()); + } + + // TODO replace equality when SNOW-991418 is fixed + @Test + public void testGetTimeWithDefaultTimeZone() throws SFException { + assertEquals( + Time.valueOf(LocalTime.of(8, 2, 3)).toString(), + dateTimeConverter + .getTime("1691568123", Types.TIMESTAMP, Types.TIMESTAMP, null, 0) + .toString()); + assertEquals( + Time.valueOf(LocalTime.of(8, 2, 3)).toString(), + dateTimeConverter + .getTime("1691568123.456789", Types.TIMESTAMP, Types.TIMESTAMP, null, 0) + .toString()); + assertEquals( + Time.valueOf(LocalTime.of(8, 2, 3)).toString(), + dateTimeConverter + .getTime("1691568123.456789123", Types.TIMESTAMP, Types.TIMESTAMP, null, 0) + .toString()); + } + + @Test + public void testGetTimeWithSessionTimeZone() throws SFException { + Time expected = Time.valueOf(LocalTime.of(22, 2, 3)); + Time actual = + dateTimeConverterWithUseSessionTimeZone.getTime( + "1691568123", Types.TIMESTAMP, SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_LTZ, null, 0); + assertEquals(expected.toString(), actual.toString()); + } + + @Test + public void testGetDateWithDefaultTimeZone() throws SFException { + assertEquals( + Date.valueOf(LocalDate.of(2023, 8, 9)).toString(), + dateTimeConverter + .getDate("1691568123", Types.TIMESTAMP, Types.TIMESTAMP, null, 0) + .toString()); + assertEquals( + Date.valueOf(LocalDate.of(2023, 8, 9)).toString(), + dateTimeConverter + .getDate("1691568123.456789", Types.TIMESTAMP, Types.TIMESTAMP, null, 0) + .toString()); + assertEquals( + Date.valueOf(LocalDate.of(2023, 8, 9)).toString(), + dateTimeConverter + .getDate("1691568123.456789123", Types.TIMESTAMP, Types.TIMESTAMP, null, 0) + .toString()); + } + + @Test + public void testGetDateWithSpecificTimeZone() throws SFException { + assertEquals( + Date.valueOf(LocalDate.of(2023, 8, 9)).toString(), + dateTimeConverter + .getDate("1691568123", Types.TIMESTAMP, Types.TIMESTAMP, nuukTimeZone, 0) + .toString()); + assertEquals( + Date.valueOf(LocalDate.of(2023, 8, 9)).toString(), + dateTimeConverter + .getDate("1691568123.456789", Types.TIMESTAMP, Types.TIMESTAMP, nuukTimeZone, 0) + .toString()); + assertEquals( + Date.valueOf(LocalDate.of(2023, 8, 9)).toString(), + dateTimeConverter + .getDate("1691568123.456789123", Types.TIMESTAMP, Types.TIMESTAMP, nuukTimeZone, 0) + .toString()); + } + + @Test + public void testGetDateWithSessionTimeZone() throws SFException { + Date expected = Date.valueOf(LocalDate.of(2023, 8, 8)); + Date actual = + dateTimeConverterWithUseSessionTimeZone.getDate( + "1691568123", Types.TIMESTAMP, SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_LTZ, null, 0); + assertEquals(expected.toString(), actual.toString()); + } +} diff --git a/src/test/java/net/snowflake/client/core/json/NumberConverterTest.java b/src/test/java/net/snowflake/client/core/json/NumberConverterTest.java new file mode 100644 index 000000000..c37573b72 --- /dev/null +++ b/src/test/java/net/snowflake/client/core/json/NumberConverterTest.java @@ -0,0 +1,163 @@ +package net.snowflake.client.core.json; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import java.math.BigDecimal; +import java.sql.Types; +import net.snowflake.client.core.SFException; +import org.junit.Test; + +public class NumberConverterTest { + private final NumberConverter numberConverter = new NumberConverter(); + + @Test + public void testByteFromNumber() { + assertThat(numberConverter.getByte(1), equalTo((byte) 1)); + assertThat(numberConverter.getByte(258), equalTo((byte) 2)); + assertThat(numberConverter.getByte(-1), equalTo((byte) -1)); + } + + @Test + public void testByteFromString() { + assertThat(numberConverter.getByte("1"), equalTo((byte) 1)); + assertThat(numberConverter.getByte("-1"), equalTo((byte) -1)); + } + + @Test + public void testShortFromNumber() throws SFException { + assertThat(numberConverter.getShort(1, Types.INTEGER), equalTo((short) 1)); + assertThat(numberConverter.getShort(2.5, Types.DOUBLE), equalTo((short) 2)); + assertThat(numberConverter.getShort(3.4f, Types.FLOAT), equalTo((short) 3)); + } + + @Test + public void testShortFromString() throws SFException { + assertThat(numberConverter.getShort("1", Types.INTEGER), equalTo((short) 1)); + assertThat(numberConverter.getShort("2.5", Types.DOUBLE), equalTo((short) 2)); + assertThat(numberConverter.getShort("3.4", Types.FLOAT), equalTo((short) 3)); + assertThat(numberConverter.getShort("4.5.6", Types.FLOAT), equalTo((short) 4)); + } + + @Test + public void testIntFromNumber() throws SFException { + assertThat(numberConverter.getInt(1, Types.INTEGER), equalTo(1)); + assertThat(numberConverter.getInt(2.5, Types.DOUBLE), equalTo(2)); + assertThat(numberConverter.getInt(3.4f, Types.FLOAT), equalTo(3)); + } + + @Test + public void testIntFromString() throws SFException { + assertThat(numberConverter.getInt("1", Types.INTEGER), equalTo(1)); + assertThat(numberConverter.getInt("2.5", Types.DOUBLE), equalTo(2)); + assertThat(numberConverter.getInt("3.4", Types.FLOAT), equalTo(3)); + assertThat(numberConverter.getInt("4.5.6", Types.FLOAT), equalTo(4)); + } + + @Test + public void testLongFromNumber() throws SFException { + assertThat(numberConverter.getLong(1, Types.INTEGER), equalTo(1L)); + assertThat(numberConverter.getLong(2.5, Types.DOUBLE), equalTo(2L)); + assertThat(numberConverter.getLong(3.4f, Types.FLOAT), equalTo(3L)); + } + + @Test + public void testLongFromString() throws SFException { + assertThat(numberConverter.getLong("1", Types.INTEGER), equalTo(1L)); + assertThat(numberConverter.getLong("2.5", Types.DOUBLE), equalTo(2L)); + assertThat(numberConverter.getLong("3.4", Types.FLOAT), equalTo(3L)); + assertThat(numberConverter.getLong("4.5.6", Types.FLOAT), equalTo(4L)); + } + + @Test + public void testBigDecimalFromNumber() throws SFException { + assertThat(numberConverter.getBigDecimal(1, Types.INTEGER), equalTo(BigDecimal.ONE)); + assertThat(numberConverter.getBigDecimal(1, Types.BIGINT), equalTo(BigDecimal.ONE)); + assertThat(numberConverter.getBigDecimal(1.5, Types.FLOAT), equalTo(new BigDecimal("1.5"))); + } + + @Test + public void testBigDecimalFromString() throws SFException { + assertThat(numberConverter.getBigDecimal("1", Types.INTEGER), equalTo(BigDecimal.ONE)); + assertThat(numberConverter.getBigDecimal("1", Types.BIGINT), equalTo(BigDecimal.ONE)); + assertThat(numberConverter.getBigDecimal("1.5", Types.FLOAT), equalTo(new BigDecimal("1.5"))); + } + + @Test + public void testBigDecimalWithScaleFromNumber() throws SFException { + assertThat(numberConverter.getBigDecimal(1, Types.INTEGER), equalTo(BigDecimal.ONE)); + assertThat(numberConverter.getBigDecimal(1, Types.BIGINT), equalTo(BigDecimal.ONE)); + assertThat(numberConverter.getBigDecimal(1.5, Types.FLOAT), equalTo(new BigDecimal("1.5"))); + assertThat( + numberConverter.getBigDecimal(1.50001, Types.FLOAT, 1), equalTo(new BigDecimal("1.5"))); + assertThat( + numberConverter.getBigDecimal(1.50001, Types.FLOAT, 5), equalTo(new BigDecimal("1.50001"))); + } + + @Test + public void testBigDecimalWithScaleFromString() throws SFException { + assertThat(numberConverter.getBigDecimal("1", Types.INTEGER), equalTo(BigDecimal.ONE)); + assertThat(numberConverter.getBigDecimal("1", Types.BIGINT), equalTo(BigDecimal.ONE)); + assertThat(numberConverter.getBigDecimal("1.5", Types.FLOAT), equalTo(new BigDecimal("1.5"))); + assertThat( + numberConverter.getBigDecimal("1.50001", Types.FLOAT, 1), equalTo(new BigDecimal("1.5"))); + assertThat( + numberConverter.getBigDecimal("1.50001", Types.FLOAT, 5), + equalTo(new BigDecimal("1.50001"))); + } + + @Test + public void testFloatFromNumber() throws SFException { + assertThat(numberConverter.getFloat(1, Types.INTEGER), equalTo(1.0f)); + assertThat(numberConverter.getFloat(1, Types.BIGINT), equalTo(1.0f)); + assertThat(numberConverter.getFloat(1.5, Types.FLOAT), equalTo(1.5f)); + assertThat(numberConverter.getFloat(1.5, Types.DOUBLE), equalTo(1.5f)); + } + + @Test + public void testFloatFromString() throws SFException { + assertThat(numberConverter.getFloat("1", Types.INTEGER), equalTo(1.0f)); + assertThat(numberConverter.getFloat("1", Types.BIGINT), equalTo(1.0f)); + assertThat(numberConverter.getFloat("1.5", Types.FLOAT), equalTo(1.5f)); + assertThat(numberConverter.getFloat("1.5", Types.DOUBLE), equalTo(1.5f)); + assertThat(numberConverter.getFloat("inf", Types.FLOAT), equalTo(Float.POSITIVE_INFINITY)); + assertThat(numberConverter.getFloat("-inf", Types.FLOAT), equalTo(Float.NEGATIVE_INFINITY)); + } + + @Test + public void testDoubleFromNumber() throws SFException { + assertThat(numberConverter.getDouble(1, Types.INTEGER), equalTo(1.0)); + assertThat(numberConverter.getDouble(1, Types.BIGINT), equalTo(1.0)); + assertThat(numberConverter.getDouble(1.5, Types.FLOAT), equalTo(1.5)); + assertThat(numberConverter.getDouble(1.5, Types.DOUBLE), equalTo(1.5)); + } + + @Test + public void testDoubleFromString() throws SFException { + assertThat(numberConverter.getDouble("1", Types.INTEGER), equalTo(1.0)); + assertThat(numberConverter.getDouble("1", Types.BIGINT), equalTo(1.0)); + assertThat(numberConverter.getDouble("1.5", Types.FLOAT), equalTo(1.5)); + assertThat(numberConverter.getDouble("1.5", Types.DOUBLE), equalTo(1.5)); + assertThat(numberConverter.getDouble("inf", Types.FLOAT), equalTo(Double.POSITIVE_INFINITY)); + assertThat(numberConverter.getDouble("-inf", Types.FLOAT), equalTo(Double.NEGATIVE_INFINITY)); + } + + @Test + public void testBigIntFromNumber() throws SFException { + assertThat(numberConverter.getBigInt(1, Types.BIGINT), equalTo(1L)); + assertThat( + numberConverter.getBigInt(9_223_372_036_854_775_807L, Types.BIGINT), + equalTo(9_223_372_036_854_775_807L)); + } + + @Test + public void testBigIntFromString() throws SFException { + assertThat(numberConverter.getBigInt("1", Types.BIGINT), equalTo(1L)); + assertThat( + numberConverter.getBigInt("9223372036854775807", Types.BIGINT), + equalTo(9_223_372_036_854_775_807L)); + assertThat( + numberConverter.getBigInt("9223372036854775808", Types.BIGINT), + equalTo(new BigDecimal("9223372036854775808"))); + } +} diff --git a/src/test/java/net/snowflake/client/core/json/StringConverterTest.java b/src/test/java/net/snowflake/client/core/json/StringConverterTest.java new file mode 100644 index 000000000..5fe3dd2cb --- /dev/null +++ b/src/test/java/net/snowflake/client/core/json/StringConverterTest.java @@ -0,0 +1,108 @@ +package net.snowflake.client.core.json; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +import java.sql.Types; +import java.time.ZoneId; +import java.util.TimeZone; +import net.snowflake.client.core.SFException; +import net.snowflake.client.core.SFSession; +import net.snowflake.client.jdbc.SnowflakeResultSetSerializableV1; +import net.snowflake.client.jdbc.SnowflakeUtil; +import net.snowflake.common.core.SFBinaryFormat; +import net.snowflake.common.core.SnowflakeDateTimeFormat; +import org.junit.Before; +import org.junit.Test; + +public class StringConverterTest { + private final TimeZone honoluluTimeZone = + TimeZone.getTimeZone(ZoneId.of("America/Nuuk")); // session time zone + private final SnowflakeResultSetSerializableV1 resultSetSerializableV1 = + mock(SnowflakeResultSetSerializableV1.class); + private Converters converters; + + private StringConverter stringConverter; + + @Before + public void init() { + SnowflakeDateTimeFormat timestampNTZFormatter = + SnowflakeDateTimeFormat.fromSqlFormat("YYYY-MM-DD HH24:MI:SS.FF3"); + SnowflakeDateTimeFormat timestampLTZFormatter = + SnowflakeDateTimeFormat.fromSqlFormat("YYYY/DD/MM HH24:MI:SS.FF3"); + SnowflakeDateTimeFormat timestampTZFormatter = + SnowflakeDateTimeFormat.fromSqlFormat("YYYY MM DD HH24:MI:SS.FF3"); + SnowflakeDateTimeFormat dateFormatter = SnowflakeDateTimeFormat.fromSqlFormat("YYYY-MM-DD"); + SnowflakeDateTimeFormat timeFormatter = SnowflakeDateTimeFormat.fromSqlFormat("HH24:MI:SS.FF3"); + converters = + new Converters( + honoluluTimeZone, + new SFSession(), + 1, + false, + false, + false, + false, + SFBinaryFormat.BASE64, + dateFormatter, + timeFormatter, + timestampNTZFormatter, + timestampLTZFormatter, + timestampTZFormatter); + stringConverter = converters.getStringConverter(); + } + + @Test + public void testConvertingString() throws SFException { + assertEquals("test", stringConverter.getString("test", Types.VARCHAR, Types.VARCHAR, 0)); + } + + @Test + public void testConvertingBoolean() throws SFException { + assertEquals("TRUE", stringConverter.getString(true, Types.BOOLEAN, Types.BOOLEAN, 0)); + assertEquals("TRUE", stringConverter.getString("true", Types.BOOLEAN, Types.BOOLEAN, 0)); + assertEquals("FALSE", stringConverter.getString(false, Types.BOOLEAN, Types.BOOLEAN, 0)); + assertEquals("FALSE", stringConverter.getString("false", Types.BOOLEAN, Types.BOOLEAN, 0)); + } + + @Test + public void testConvertingNumbers() throws SFException { + assertEquals("12", stringConverter.getString(12, Types.INTEGER, Types.INTEGER, 0)); + assertEquals("12", stringConverter.getString(12, Types.TINYINT, Types.TINYINT, 0)); + assertEquals("12", stringConverter.getString(12, Types.SMALLINT, Types.SMALLINT, 0)); + assertEquals("12", stringConverter.getString(12L, Types.BIGINT, Types.BIGINT, 0)); + assertEquals("12.5", stringConverter.getString(12.5, Types.DOUBLE, Types.DOUBLE, 0)); + assertEquals("12.5", stringConverter.getString(12.5F, Types.FLOAT, Types.FLOAT, 0)); + } + + @Test + public void testConvertingTimestamp() throws SFException { + assertEquals( + "1988-03-21 22:33:15.000", + stringConverter.getString("574986795", Types.TIMESTAMP, Types.TIMESTAMP, 0)); + assertEquals( + "1988-03-21 19:33:15.000", + stringConverter.getString( + "574986795", Types.TIMESTAMP, SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_LTZ, 0)); + assertEquals( + "1988-03-21 14:33:15.000", + stringConverter.getString( + "574986795 960", Types.TIMESTAMP, SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_TZ, 0)); + } + + @Test + public void testConvertingDate() throws SFException { + assertEquals("2023-12-18", stringConverter.getString("19709", Types.DATE, Types.DATE, 0)); + } + + @Test + public void testConvertingTime() throws SFException { + assertEquals( + "00:13:18.000", stringConverter.getString("798.838000000", Types.TIME, Types.TIME, 0)); + } + + @Test + public void testConvertingBinary() throws SFException { + assertEquals("AQID", stringConverter.getString("010203", Types.BINARY, Types.BINARY, 0)); + } +} diff --git a/src/test/java/net/snowflake/client/jdbc/MockConnectionTest.java b/src/test/java/net/snowflake/client/jdbc/MockConnectionTest.java index 3c10381be..18ad2eb8c 100644 --- a/src/test/java/net/snowflake/client/jdbc/MockConnectionTest.java +++ b/src/test/java/net/snowflake/client/jdbc/MockConnectionTest.java @@ -17,6 +17,7 @@ import java.util.stream.IntStream; import net.snowflake.client.category.TestCategoryConnection; import net.snowflake.client.core.*; +import net.snowflake.client.core.json.Converters; import net.snowflake.client.jdbc.telemetry.Telemetry; import net.snowflake.client.jdbc.telemetry.TelemetryData; import net.snowflake.common.core.SnowflakeDateTimeFormat; @@ -598,6 +599,7 @@ public String[] getChildQueryIds(String queryID) throws SQLException { } private static class MockJsonResultSet extends SFJsonResultSet { + private static final TimeZone defaultTimeZone = TimeZone.getTimeZone("America/Los_Angeles"); JsonNode resultJson; int currentRowIdx = -1; @@ -605,6 +607,22 @@ private static class MockJsonResultSet extends SFJsonResultSet { public MockJsonResultSet(JsonNode mockedJsonResponse, MockSnowflakeSFSession sfSession) throws SnowflakeSQLException { + super( + defaultTimeZone, + new Converters( + defaultTimeZone, + new SFSession(), + 1, + true, + false, + false, + false, + null, + null, + null, + null, + null, + null)); setSession(sfSession); this.resultJson = mockedJsonResponse.path("data").path("rowset"); this.resultSetMetaData = MockConnectionTest.getRSMDFromResponse(mockedJsonResponse, session);