From 3a54f1253f957b6004d616560c52dedf3638c2c0 Mon Sep 17 00:00:00 2001 From: hailin0 Date: Mon, 25 Nov 2024 22:19:01 +0800 Subject: [PATCH] [Improve][Excel] Support read blank string & auto type-cast (#8111) --- .../file/source/reader/ExcelReadStrategy.java | 17 ++- .../file/writer/ExcelReadStrategyTest.java | 110 +++++++++++++++++- .../test/resources/excel/test_read_excel.xlsx | Bin 10390 -> 10578 bytes 3 files changed, 121 insertions(+), 6 deletions(-) diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/ExcelReadStrategy.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/ExcelReadStrategy.java index d7dfe206ab5..b3d789be57e 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/ExcelReadStrategy.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/source/reader/ExcelReadStrategy.java @@ -193,6 +193,8 @@ private Object getCellValue(CellType cellType, Cell cell) { return cell.getLocalDateTimeCellValue(); } return cell.getNumericCellValue(); + case BLANK: + return ""; case ERROR: break; default: @@ -206,14 +208,25 @@ private Object getCellValue(CellType cellType, Cell cell) { @SneakyThrows private Object convert(Object field, SeaTunnelDataType fieldType) { if (field == null) { - return ""; + return null; } + SqlType sqlType = fieldType.getSqlType(); + if (!(SqlType.STRING.equals(sqlType)) && "".equals(field)) { + return null; + } switch (sqlType) { case MAP: case ARRAY: return objectMapper.readValue((String) field, fieldType.getTypeClass()); case STRING: + if (field instanceof Double) { + String stringValue = field.toString(); + if (stringValue.endsWith(".0")) { + return stringValue.substring(0, stringValue.length() - 2); + } + return stringValue; + } return String.valueOf(field); case DOUBLE: return Double.parseDouble(field.toString()); @@ -250,7 +263,7 @@ private Object convert(Object field, SeaTunnelDataType fieldType) { return LocalDateTime.parse( (String) field, DateTimeFormatter.ofPattern(datetimeFormat.getValue())); case NULL: - return ""; + return null; case BYTES: return field.toString().getBytes(StandardCharsets.UTF_8); case ROW: diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/ExcelReadStrategyTest.java b/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/ExcelReadStrategyTest.java index 149ee7648d5..6edc55d56cd 100644 --- a/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/ExcelReadStrategyTest.java +++ b/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/file/writer/ExcelReadStrategyTest.java @@ -54,12 +54,113 @@ public class ExcelReadStrategyTest { @Test public void testExcelRead() throws IOException, URISyntaxException { - testExcelRead("/excel/test_read_excel.xlsx"); - testExcelRead("/excel/test_read_excel_date_string.xlsx"); + URL excelFile = ExcelReadStrategyTest.class.getResource("/excel/test_read_excel.xlsx"); + URL conf = ExcelReadStrategyTest.class.getResource("/excel/test_read_excel.conf"); + Assertions.assertNotNull(excelFile); + Assertions.assertNotNull(conf); + String excelFilePath = Paths.get(excelFile.toURI()).toString(); + String confPath = Paths.get(conf.toURI()).toString(); + Config pluginConfig = ConfigFactory.parseFile(new File(confPath)); + ExcelReadStrategy excelReadStrategy = new ExcelReadStrategy(); + LocalConf localConf = new LocalConf(FS_DEFAULT_NAME_DEFAULT); + excelReadStrategy.setPluginConfig(pluginConfig); + excelReadStrategy.init(localConf); + + List fileNamesByPath = excelReadStrategy.getFileNamesByPath(excelFilePath); + CatalogTable userDefinedCatalogTable = CatalogTableUtil.buildWithConfig(pluginConfig); + excelReadStrategy.setCatalogTable(userDefinedCatalogTable); + TestCollector testCollector = new TestCollector(); + excelReadStrategy.read(fileNamesByPath.get(0), "", testCollector); + + SeaTunnelRow seaTunnelRow = testCollector.getRows().get(0); + + Assertions.assertEquals(seaTunnelRow.getArity(), 14); + Assertions.assertEquals(seaTunnelRow.getField(0).getClass(), Byte.class); + Assertions.assertEquals(seaTunnelRow.getField(1).getClass(), Short.class); + Assertions.assertEquals(seaTunnelRow.getField(2).getClass(), Integer.class); + Assertions.assertEquals(seaTunnelRow.getField(3).getClass(), Long.class); + Assertions.assertEquals(seaTunnelRow.getField(4).getClass(), String.class); + Assertions.assertEquals(seaTunnelRow.getField(5).getClass(), Double.class); + Assertions.assertEquals(seaTunnelRow.getField(6).getClass(), Float.class); + Assertions.assertEquals(seaTunnelRow.getField(7).getClass(), BigDecimal.class); + Assertions.assertEquals(seaTunnelRow.getField(8).getClass(), Boolean.class); + Assertions.assertEquals(seaTunnelRow.getField(9).getClass(), LinkedHashMap.class); + Assertions.assertEquals(seaTunnelRow.getField(10).getClass(), String[].class); + Assertions.assertEquals(seaTunnelRow.getField(11).getClass(), LocalDate.class); + Assertions.assertEquals(seaTunnelRow.getField(12).getClass(), LocalDateTime.class); + Assertions.assertEquals(seaTunnelRow.getField(13).getClass(), LocalTime.class); + + Assertions.assertEquals(seaTunnelRow.getField(0), (byte) 1); + Assertions.assertEquals(seaTunnelRow.getField(1), (short) 22); + Assertions.assertEquals(seaTunnelRow.getField(2), 333); + Assertions.assertEquals(seaTunnelRow.getField(3), 4444L); + Assertions.assertEquals(seaTunnelRow.getField(4), "Cosmos"); + Assertions.assertEquals(seaTunnelRow.getField(5), 5.555); + Assertions.assertEquals(seaTunnelRow.getField(6), (float) 6.666); + Assertions.assertEquals(seaTunnelRow.getField(7), new BigDecimal("7.78")); + Assertions.assertEquals(seaTunnelRow.getField(8), Boolean.FALSE); + Assertions.assertEquals( + seaTunnelRow.getField(9), + new LinkedHashMap() { + { + put("name", "Ivan"); + put("age", "26"); + } + }); + Assertions.assertArrayEquals( + (String[]) seaTunnelRow.getField(10), new String[] {"Ivan", "Dusayi"}); + Assertions.assertEquals( + seaTunnelRow.getField(11), + DateUtils.parse("2024-01-31", DateUtils.Formatter.YYYY_MM_DD)); + Assertions.assertEquals( + seaTunnelRow.getField(12), + DateTimeUtils.parse( + "2024-01-31 16:00:48", DateTimeUtils.Formatter.YYYY_MM_DD_HH_MM_SS)); + Assertions.assertEquals( + seaTunnelRow.getField(13), + TimeUtils.parse("16:00:48", TimeUtils.Formatter.HH_MM_SS)); + + SeaTunnelRow row2 = testCollector.getRows().get(1); + Assertions.assertEquals(row2.getArity(), 14); + // check number blank + Assertions.assertEquals(row2.getField(0).getClass(), Byte.class); + Assertions.assertNull(row2.getField(1)); + Assertions.assertNull(row2.getField(2)); + Assertions.assertNull(row2.getField(3)); + Assertions.assertEquals(row2.getField(4), "1"); + Assertions.assertNull(row2.getField(5)); + Assertions.assertNull(row2.getField(6)); + Assertions.assertNull(row2.getField(7)); + Assertions.assertNull(row2.getField(8)); + Assertions.assertNull(row2.getField(9)); + Assertions.assertNull(row2.getField(10)); + Assertions.assertNull(row2.getField(11)); + Assertions.assertNull(row2.getField(12)); + Assertions.assertNull(row2.getField(13)); + + SeaTunnelRow row3 = testCollector.getRows().get(2); + Assertions.assertEquals(row3.getArity(), 14); + Assertions.assertEquals(row3.getField(0).getClass(), Byte.class); + Assertions.assertNull(row3.getField(1)); + Assertions.assertNull(row3.getField(2)); + Assertions.assertNull(row3.getField(3)); + // check string blank + Assertions.assertEquals(row3.getField(4), ""); + Assertions.assertNull(row3.getField(5)); + Assertions.assertNull(row3.getField(6)); + Assertions.assertNull(row3.getField(7)); + Assertions.assertNull(row3.getField(8)); + Assertions.assertNull(row3.getField(9)); + Assertions.assertNull(row3.getField(10)); + Assertions.assertNull(row3.getField(11)); + Assertions.assertNull(row3.getField(12)); + Assertions.assertNull(row3.getField(13)); } - private void testExcelRead(String filePath) throws IOException, URISyntaxException { - URL excelFile = ExcelReadStrategyTest.class.getResource(filePath); + @Test + public void testExcelReadDateString() throws IOException, URISyntaxException { + URL excelFile = + ExcelReadStrategyTest.class.getResource("/excel/test_read_excel_date_string.xlsx"); URL conf = ExcelReadStrategyTest.class.getResource("/excel/test_read_excel.conf"); Assertions.assertNotNull(excelFile); Assertions.assertNotNull(conf); @@ -76,6 +177,7 @@ private void testExcelRead(String filePath) throws IOException, URISyntaxExcepti excelReadStrategy.setCatalogTable(userDefinedCatalogTable); TestCollector testCollector = new TestCollector(); excelReadStrategy.read(fileNamesByPath.get(0), "", testCollector); + for (SeaTunnelRow seaTunnelRow : testCollector.getRows()) { Assertions.assertEquals(seaTunnelRow.getArity(), 14); Assertions.assertEquals(seaTunnelRow.getField(0).getClass(), Byte.class); diff --git a/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/resources/excel/test_read_excel.xlsx b/seatunnel-connectors-v2/connector-file/connector-file-base/src/test/resources/excel/test_read_excel.xlsx index c277b4d7fdc0425edde6a8961d47d7071a851c11..87f72cfb28d9753d98b4c7536155c09024cf7d76 100644 GIT binary patch delta 6140 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 APyhe` delta 6022 zcmeI0Wl$WfQY0J=9q zB3c>Gf}2wyy7VmQq{OW{!@+^1s&o1l>K_R^^gZ;5YhL1@`!$W? z!75i`B)gJ?6BpkxMWisrT1=gAF)2*{1pfgc$5`jO)X-^zXt4g>=o_s@_i_n3=d7~$ zfPs`X2+>h%4UW}(y|Ltx<(FIW*QT)CX5C2>Mn5P3QMdjYeALZV?FkQUk*S5+qXn_< zs>t=czn8jqVD>|N3*S52ZTj4v7{N$r6UQOLs%o|r=@Yjpq(977&=oaym^$Z&f8<*# z`qQ-5nj4vNfR#hPf36)6=%{0Y$nW*U1-#W-d$JesikamNXGa{w4Iyr_0*!A43jYXBQ1(u9g=Io=W+Tub+%aNeQB=%r>a$!s|i}#hI9~ zLv)_|h0blSI5dag;Z{05oryDvqRR@ys#v?`3gCZ5+Dq7l&5-X*L^PtQ9>ybs`+aV# zZ;-uJy?7(z!W)g1%yDGY+${Xx>(R^2aelIZ-k<6M(*K(Yd}VeYhqBB zwk+S`*yhToxbmhrcaQ_hFyVz>*SlhYFL=Eli@veJMctUvMuy2j<;kzl=N-3=l^f{Z z+wE)U>eaKwMEBs$^W455B_r+mY+PHy#$!QqeKb7QvzjY%VCi+~f1G+?dqh9@lDavY zUuJ_NjspVGkOPwrx?g7!O62aXC-z7tfKhHBS^TY($6gee0Kg6ostLr1cslP|$WIbv z?(hfrtCfToPm^5llPbEkV(w`3I}l}r^>t3m{&L^XSw7mK7)IQmBj~l|81VW+k8&i$ zUDi06X_=n+tpVqd{U^<_QO13g{swMT#RIBJ7Q9zbjBHrj>vYjrnHYNJ31Ea8#Gc*e z@lUD5BbBJ|AxAx6lAxI-0?)MVTwxe*!f2Y0BshS42HMQ-nARW)BgPI}FVro%R zL>LM~J<|NZ9wt$rdY~Uj(?_^5*E31rVUxg`%h(!IQ^aZaW^+Q*csG4)$xi&d#4yL^ zI&LS_GC57=c%O`^nz@k>RvP$i*BAKkSMQ_{a(=2yvqB`=EaY|;!Rpyam6XiQBVh8? z2Zy1tA)I82T>t$mv z#T{RE=Cm<-*0+ZU8(D5oWuhX0P7iLJo*{tyHan^O;afbu(->o+2rs%u6aZHbUOcU9 zw~!%=(W?P<{kqnV{9{GoZCGnxb(={0COxVWN-QaC3Q=DfgFUF*8pjUJXkLPHvyY)-k4Hje05iRg2XTGf_0-mD zT5R1V)DU09y6m?a7G|<+vCf<;Kzq8wXOp-{XaJGr9XY zXZaXtR)2C(89w>QD=0nGa2?jz7pTs?N)CDJ8Mi<)KM3#<4%f4HIu zac5Z7d!jXxb|B(@LUBx%cr#b#p?O92n}C>q0TBz+_rEzqrRHUL31ra3=i3#NgG+`%LGjc>yQ4a+l*m}gNPsL?(62=9raIz zo$DAL7-cXsd@L%JIU5WQznE=_p79q~s)@-Vi<)or+NfH}-4!gqpsp>hRARMt*yf$F z{2i~YpBr)~$iw>E!ky~jK|y2dM08Hlq9Vx{fgN3+vo~|aAvzibtg6Nuv&9Xw8P%*W3oIk(uy^x*-rCEX1^Tau|e5M>~p?vI_5uw%^jEBZy%KJb5`%z`1%-#(?myS zAFNcIWq$~9uwOTS={uaBt6h{fR=h`H7f>V?1KnR&lY5>l)x7bfQn3j7a%%~bFiw28 zW4S`+6ieah&eP{@AHAS{I|J&9s8X7G^>=B}>#SvW82`PA3@B-QivKtA|JF#@|H{Y& z1~mA=H*9?5)U_TXJ^&!3h0>&iAT0Y{diXsmM$xFN5SWxH%3&DTklOVoSrOOk4+kgU zFFvX*RVgJ1|E9*%%(aP0|3X#=Hh+}sF!7cB4r9A*_Ht=M!o#jNZc>GQ{OX}*sk1*{RLRfBk=u6>%Q&D6jv zmUNfo`j(ehM%%8t6B%1S$s+DnB-=ccb-C-f+TcO;aF(-nzMKS1v*CqjEz^yB1K}D< zQ?|%F6VVF6yE+27eNk5)--$FA#E4O>*~0Zg!_?^1F%`9b{`le&_=<@G9S{YtAN3yY3@Tm@S(R4Grh#X*n1{_H{JeOE0+SNEmiD zFx>iT=?L$I<<1X~K4PqQ(~39X+Yd}HvwRzj!PbF;#ujsls?x-w@ZNYmJkG#g6~DEyxp;9F2qZ z$Wk+Wp<&}FG%uiFSIp0v3^!+W@JdS;yq97popkHefuYw4%3Q^UYPIs6qMW(W5!(HRMbN$}d}9#} zzob+{FVjP47M^h4`gg$WyTQ^X2MSJGJ0uJS6A1aF>Vbg^!x!IKX=&$0(1{iQVo6hU9!FD~V(~&3q?sE7?`zV$4Yn=%dqYvjo4jQK+3%^|f+BT{_Db_I^%^w9DH zEy5b6k&J$rXPX#dN~zFfBvO#g@eau^%$tbBUSdVNGAqNQyYTj@pl?ux&@bZk53BDj z$7sOUOy{RHk*-a40ldkp!K|mF9b>lPkNq> zlbB;`&K&db_A!gQ2xfRxj^jP1JWFMg)eO{L=-wMt?dTYyL zY(ramEFwBQGhtt17boLD$uT-pBEDR2NmKj;@P$#v!}yIdQpv&H}zM zetA^-M}Ve9+s{y`=C$H?o!(r8$nTrq1IJo@qYSx-+tstwTrQpU1d>9;T_?CK(a&}L zkY#FWcSw01N81w5fI$F{BTnB12@Uql0xdDfIPF}m{(i>C5QjMZ`IC5Q`?&%ZXfoVS zYE%uZ4p+dGaw>{w2p*|_bT?}33C@^utotcZ!^`m}P9mJsX9SKjRTK9aRP@MWiq3U8k3K z(K7LfpA6pb2HqkxL5kO7xfyAAJmV>mk>qdK`AB8nYs&!b*)DSsBc)^x2B|+HC2;V9 zCD_j$03 z3TeCUg~V^32z>3VG+ZM-GVzjgm7mIUfvHiyK z_)d=3Hp7uw!bNbMHLaxSyav1@l^d;c$x52UDj$ag{Y8u6Naw0`CiE`ty9-^ai=K7l zlq$l7_;znT^r#={%5Gl&)b|#4yM*~Ad+9vRYbM^E%q5w=9l4av++80;*Gk`2!F%$I z220EI_vw<> z?9#A-(N>o~jObNTJs{$in(ot+du}9t&y9%hxsks!pP!qjlck%R)4$jp{2y#C;u!0X ze1EIcO!cF_xscM&v@|SLE$>-d(R_Srvp7_me>hzTtago=Q(ZovX=AsriWo}6lnuUsy2#hYKu&!KqJKrz z<#|j}zc=cbJ|uIa=RYzH&a+M`-VG{98}nx3SwK*iI4BTmmq>GsSpfHYu%H&v-6{s*e*Ki@)(SK zaY%_l(6BX`<6b<%J7pBVxAR90X&3KYsqDkB*h67bo?a;$ccv{QkV#?ukOI+e_8TJs zjDE>v2P9&;I-RkYkHnH=*)%HnOrl6Bl(8MRUcVRK6!uMFSuRAW?#kO459;h9mbUCn z^R?FfE4s}I`)xD!jHq$@ED836&em?*>-mP*`lxadzB^aZ;U#m}JWJyJuOM9_2L0H! zhbob>=5Jq`P(-{nH1jH;-iO}w2_G6i>KX{*IrnE%e~>9m(-tJAkL)MwD^^TO(GXGR z-$hzXK+`sQ`BT%*4)|=_@Bg1b5UK_OMzgZk0x?usP|U0ps67Z5sE`o=KvlC*qIjWT zj6Oz`8Z-&(p9~qb3YDP!ublH&Mg4pAQc{#7yEw2LRm3iWu}6TCc|gIV zqYlI%2mHlg|LZLMb%_WAik?Ff=!`PvV5a*EGy(wBf4d|NglYqUQMDW-bpOOE_jBoR d-!6X-TE{^xaB%$(0h4$r7EUhgL)L%P{x`?wI066w