From 16e4bef293903f67288e187c444236f3c18e0523 Mon Sep 17 00:00:00 2001 From: Jens Kristian Villadsen Date: Fri, 3 Jan 2025 15:26:14 +0100 Subject: [PATCH 01/11] fast prototyping import of parquet files --- .../csiro/pathling/update/ImportExecutor.java | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/fhir-server/src/main/java/au/csiro/pathling/update/ImportExecutor.java b/fhir-server/src/main/java/au/csiro/pathling/update/ImportExecutor.java index 7b9ffbbc44..f9c7511ae2 100644 --- a/fhir-server/src/main/java/au/csiro/pathling/update/ImportExecutor.java +++ b/fhir-server/src/main/java/au/csiro/pathling/update/ImportExecutor.java @@ -39,6 +39,7 @@ import org.apache.spark.api.java.function.FilterFunction; import org.apache.spark.api.java.function.MapFunction; import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; import org.apache.spark.sql.SparkSession; import org.apache.spark.sql.catalyst.encoders.ExpressionEncoder; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -50,6 +51,7 @@ import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; +import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.UrlType; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; @@ -137,6 +139,15 @@ public OperationOutcome execute(@Nonnull @ResourceParam final Parameters inParam .map(param -> ImportMode.fromCode( ((CodeType) param.getValue()).asStringValue())) .orElse(ImportMode.OVERWRITE); + + // Get the serialized resource type from the source parameter. + final String serializationMode = sourceParam.getPart().stream() + .filter(param -> "serializationType".equals(param.getName())) + .findFirst() + .map(param -> ((StringType) param.getValue()).getValueAsString()).orElse(null); + + + final String resourceCode = ((CodeType) resourceTypeParam.getValue()).getCode(); final ResourceType resourceType = ResourceType.fromCode(resourceCode); @@ -148,18 +159,16 @@ public OperationOutcome execute(@Nonnull @ResourceParam final Parameters inParam throw new InvalidUserInputError("Unsupported resource type: " + resourceCode); } + // Read the resources from the source URL into a dataset of strings. - final Dataset jsonStrings = readStringsFromUrl(urlParam); + final Dataset rows = readRowsFromUrl(urlParam, serializationMode, fhirEncoder); - // Parse each line into a HAPI FHIR object, then encode to a Spark dataset. - final Dataset resources = jsonStrings.map(jsonToResourceConverter(), - fhirEncoder); log.info("Importing {} resources (mode: {})", resourceType.toCode(), importMode.getCode()); if (importMode == ImportMode.OVERWRITE) { - database.overwrite(resourceType, resources.toDF()); + database.overwrite(resourceType, rows); } else { - database.merge(resourceType, resources.toDF()); + database.merge(resourceType, rows); } } @@ -177,22 +186,28 @@ public OperationOutcome execute(@Nonnull @ResourceParam final Parameters inParam } @Nonnull - private Dataset readStringsFromUrl(@Nonnull final ParametersParameterComponent urlParam) { + private Dataset readRowsFromUrl(@Nonnull final ParametersParameterComponent urlParam, + final String serializationMode, final ExpressionEncoder fhirEncoder) { final String url = ((UrlType) urlParam.getValue()).getValueAsString(); final String decodedUrl = URLDecoder.decode(url, StandardCharsets.UTF_8); final String convertedUrl = FileSystemPersistence.convertS3ToS3aUrl(decodedUrl); - final Dataset jsonStrings; + final Dataset rowDataset; try { // Check that the user is authorized to execute the operation. accessRules.ifPresent(ar -> ar.checkCanImportFrom(convertedUrl)); final FilterFunction nonBlanks = s -> !s.isBlank(); - jsonStrings = spark.read().textFile(convertedUrl).filter(nonBlanks); + if("parquet".equals(serializationMode)) + rowDataset = spark.read().parquet(convertedUrl); + else + // Parse each line into a HAPI FHIR object, then encode to a Spark dataset. + rowDataset = spark.read().textFile(convertedUrl).filter(nonBlanks).map(jsonToResourceConverter(), + fhirEncoder).toDF(); } catch (final SecurityError e) { throw new InvalidUserInputError("Not allowed to import from URL: " + convertedUrl, e); } catch (final Exception e) { throw new InvalidUserInputError("Error reading from URL: " + convertedUrl, e); } - return jsonStrings; + return rowDataset; } @Nonnull From 0d4e4a4418d9892c14577953b3374d7b15ee425c Mon Sep 17 00:00:00 2001 From: John Grimes Date: Wed, 15 Jan 2025 10:48:40 +1000 Subject: [PATCH 02/11] Add format parameter to import tests --- .../integration/modification/ImportTest.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java b/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java index 90d39f3918..6ccfc66349 100644 --- a/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java +++ b/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java @@ -72,20 +72,21 @@ class ImportTest extends ModificationTest { @SuppressWarnings("SameParameterValue") @Nonnull - Parameters buildImportParameters(@Nonnull final URL jsonURL, - @Nonnull final ResourceType resourceType) { + Parameters buildImportParameters(@Nonnull final URL url, + @Nonnull final ResourceType resourceType, @Nonnull final String format) { final Parameters parameters = new Parameters(); final ParametersParameterComponent sourceParam = parameters.addParameter().setName("source"); sourceParam.addPart().setName("resourceType").setValue(new CodeType(resourceType.toCode())); - sourceParam.addPart().setName("url").setValue(new UrlType(jsonURL.toExternalForm())); + sourceParam.addPart().setName("url").setValue(new UrlType(url.toExternalForm())); + sourceParam.addPart().setName("format").setValue(new CodeType(format)); return parameters; } @SuppressWarnings("SameParameterValue") @Nonnull - Parameters buildImportParameters(@Nonnull final URL jsonURL, - @Nonnull final ResourceType resourceType, @Nonnull final ImportMode mode) { - final Parameters parameters = buildImportParameters(jsonURL, resourceType); + Parameters buildImportParameters(@Nonnull final URL url, + @Nonnull final ResourceType resourceType, @Nonnull final String format, @Nonnull final ImportMode mode) { + final Parameters parameters = buildImportParameters(url, resourceType, format); final ParametersParameterComponent sourceParam = parameters.getParameter().stream() .filter(p -> p.getName().equals("source")).findFirst() .orElseThrow(); @@ -96,7 +97,7 @@ Parameters buildImportParameters(@Nonnull final URL jsonURL, @Test void importJsonFile() { final URL jsonURL = getResourceAsUrl("import/Patient.ndjson"); - importExecutor.execute(buildImportParameters(jsonURL, ResourceType.PATIENT)); + importExecutor.execute(buildImportParameters(jsonURL, ResourceType.PATIENT, "ndjson")); final Dataset result = database.read(ResourceType.PATIENT); final Dataset expected = new DatasetBuilder(spark) @@ -119,7 +120,7 @@ void importJsonFile() { void mergeJsonFile() { final URL jsonURL = getResourceAsUrl("import/Patient_updates.ndjson"); importExecutor.execute( - buildImportParameters(jsonURL, ResourceType.PATIENT, ImportMode.MERGE)); + buildImportParameters(jsonURL, ResourceType.PATIENT, "ndjson", ImportMode.MERGE)); final Dataset result = database.read(ResourceType.PATIENT); final Dataset expected = new DatasetBuilder(spark) @@ -143,14 +144,14 @@ void mergeJsonFile() { @Test void importJsonFileWithBlankLines() { final URL jsonURL = getResourceAsUrl("import/Patient_with_eol.ndjson"); - importExecutor.execute(buildImportParameters(jsonURL, ResourceType.PATIENT)); + importExecutor.execute(buildImportParameters(jsonURL, ResourceType.PATIENT, "ndjson")); assertEquals(9, database.read(ResourceType.PATIENT).count()); } @Test void importJsonFileWithRecursiveDatatype() { final URL jsonURL = getResourceAsUrl("import/Questionnaire.ndjson"); - importExecutor.execute(buildImportParameters(jsonURL, ResourceType.QUESTIONNAIRE)); + importExecutor.execute(buildImportParameters(jsonURL, ResourceType.QUESTIONNAIRE, "ndjson")); final Dataset questionnaireDataset = database.read(ResourceType.QUESTIONNAIRE); assertEquals(1, questionnaireDataset.count()); @@ -191,7 +192,7 @@ void throwsOnUnsupportedResourceType() { final InvalidUserInputError error = assertThrows(InvalidUserInputError.class, () -> importExecutor.execute( buildImportParameters(new URL("file://some/url"), - resourceType)), "Unsupported resource type: " + resourceType.toCode()); + resourceType, "ndjson")), "Unsupported resource type: " + resourceType.toCode()); assertEquals("Unsupported resource type: " + resourceType.toCode(), error.getMessage()); } } @@ -200,7 +201,7 @@ void throwsOnUnsupportedResourceType() { void throwsOnMissingId() { final URL jsonURL = getResourceAsUrl("import/Patient_missing_id.ndjson"); final Exception error = assertThrows(Exception.class, - () -> importExecutor.execute(buildImportParameters(jsonURL, ResourceType.PATIENT))); + () -> importExecutor.execute(buildImportParameters(jsonURL, ResourceType.PATIENT, "ndjson"))); final BaseServerResponseException convertedError = ErrorHandlingInterceptor.convertError(error); assertInstanceOf(InvalidRequestException.class, convertedError); From a17871118b2ac6f0d819b249c59a9acdd7c4796d Mon Sep 17 00:00:00 2001 From: John Grimes Date: Wed, 15 Jan 2025 10:58:10 +1000 Subject: [PATCH 03/11] Add Parquet and Delta test data for ImportTest --- ...b-868c-89803ac2dc1b-c000.snappy.parquet.crc | Bin 0 -> 1032 bytes ...c-8d65-eb2ef157cd95-c000.snappy.parquet.crc | Bin 0 -> 1048 bytes .../_delta_log/.00000000000000000000.json.crc | Bin 0 -> 348 bytes .../_delta_log/.00000000000000000001.json.crc | Bin 0 -> 28 bytes .../_delta_log/00000000000000000000.json | 4 ++++ .../_delta_log/00000000000000000001.json | 3 +++ ...-442b-868c-89803ac2dc1b-c000.snappy.parquet | Bin 0 -> 131068 bytes ...-4e9c-8d65-eb2ef157cd95-c000.snappy.parquet | Bin 0 -> 132841 bytes .../import/Patient.parquet/._SUCCESS.crc | Bin 0 -> 8 bytes ...b-9de3-3b890bbc4f3d-c000.snappy.parquet.crc | Bin 0 -> 1036 bytes .../resources/import/Patient.parquet/_SUCCESS | 0 ...-46bb-9de3-3b890bbc4f3d-c000.snappy.parquet | Bin 0 -> 131087 bytes 12 files changed, 7 insertions(+) create mode 100644 fhir-server/src/test/resources/import/Patient.delta/.part-00000-672fe062-8b0c-442b-868c-89803ac2dc1b-c000.snappy.parquet.crc create mode 100644 fhir-server/src/test/resources/import/Patient.delta/.part-00000-b3c5e8be-3b6c-4e9c-8d65-eb2ef157cd95-c000.snappy.parquet.crc create mode 100644 fhir-server/src/test/resources/import/Patient.delta/_delta_log/.00000000000000000000.json.crc create mode 100644 fhir-server/src/test/resources/import/Patient.delta/_delta_log/.00000000000000000001.json.crc create mode 100644 fhir-server/src/test/resources/import/Patient.delta/_delta_log/00000000000000000000.json create mode 100644 fhir-server/src/test/resources/import/Patient.delta/_delta_log/00000000000000000001.json create mode 100644 fhir-server/src/test/resources/import/Patient.delta/part-00000-672fe062-8b0c-442b-868c-89803ac2dc1b-c000.snappy.parquet create mode 100644 fhir-server/src/test/resources/import/Patient.delta/part-00000-b3c5e8be-3b6c-4e9c-8d65-eb2ef157cd95-c000.snappy.parquet create mode 100644 fhir-server/src/test/resources/import/Patient.parquet/._SUCCESS.crc create mode 100644 fhir-server/src/test/resources/import/Patient.parquet/.part-00000-79c9ad61-dc37-46bb-9de3-3b890bbc4f3d-c000.snappy.parquet.crc create mode 100644 fhir-server/src/test/resources/import/Patient.parquet/_SUCCESS create mode 100644 fhir-server/src/test/resources/import/Patient.parquet/part-00000-79c9ad61-dc37-46bb-9de3-3b890bbc4f3d-c000.snappy.parquet diff --git a/fhir-server/src/test/resources/import/Patient.delta/.part-00000-672fe062-8b0c-442b-868c-89803ac2dc1b-c000.snappy.parquet.crc b/fhir-server/src/test/resources/import/Patient.delta/.part-00000-672fe062-8b0c-442b-868c-89803ac2dc1b-c000.snappy.parquet.crc new file mode 100644 index 0000000000000000000000000000000000000000..108162920a94e0bd519b9c1970bdf8af92303565 GIT binary patch literal 1032 zcmV+j1o!)6a$^7h00ID#{sDRS1dM=P%X7y95%Q)g43-axbc0@Q0+VPU2G? zs|&im_!hgSr%rCw;`;?<9Yx|XGdwuj2SX%(T|H_8(e^E4gq(UDcjF*K(Qy#xsX-0< z5$Cnmr==^mom4`%MOR4f`d4Liqnqmwmzd9*NZV(0gVsQ=SATfhfw<5;E`^`RnaS6^ z15BesgLRhdCh@Oy7y{ac2ohi9U*dVG2%Vl;#N$lxAlDr&WU$X03dT>J=&6LOCbkF} z7bRiy`t$jjs%UucpOu{ZSRQiszL~vE)}5|TUR%y?6rkZmOFPq4VQ^NV8c+kf!SF2H z@u*M4=b7tA_M|=um-LP1QV1nOFNfV0nOU+}K&qDu47?8s4yL{b06R;`lduBsPNlB{ zCVGVQPPNJQ6e`u}1cK_GATn|4bl^bNlwTA=8o2z`;!63L%4FpSUVvovxITA;M+YCb z&nC9|D$z0fdx2A(<)Z`59oFeNMtWKbi(UuX_xr zm$n%WeQE@U+UAx5;gUaOW<8R$WXGPmGBHS89|=D@yOVzlDhy#X0 zzU675O*%XFdiR54%>QYC*vlVyf@;*YO9 zf$6U>cICpSt96x)W3R&{+$cY?rwrij*Ir)w#D*?CDmb$4C`c&6`3;I6lF$~X{#UiU z6QBQURqW~4`OG5mEIvo|b$|3b1CNukg9c@v>75G;L6F#zRKQgq){yt*Vzc?OINcf; zd0fiudAXr;P-g8^Nx1v36UCROEP%}%D!*xAa_0tfBYNGonE|?}`f!<6 z2gcvS(?9a{7s>K_h?AhVo6ZLA|AWPSp%$gdV`3)S5&91>x*Xa;L-N#bB_!U%xD;rc z^!|~JaISoAErVQY89|pAnymu7CnZrHHXHrG8(FS#tPHE|tBF7hI!!FJyTFqirEdV9 z3!e7AIk}e_D9aIc3v@~KN8Pkq9`rGD*>GWwFEOuu&GGQ8sSTRM0*bnio|J?diSbQ0 z00}+9ZUyS1x(hwG9-yM+GIV+vZEawDTwFxB1ZyISt=NkH%%+e=`Rsnp;iQX*F;iS3 CUjj7% literal 0 HcmV?d00001 diff --git a/fhir-server/src/test/resources/import/Patient.delta/.part-00000-b3c5e8be-3b6c-4e9c-8d65-eb2ef157cd95-c000.snappy.parquet.crc b/fhir-server/src/test/resources/import/Patient.delta/.part-00000-b3c5e8be-3b6c-4e9c-8d65-eb2ef157cd95-c000.snappy.parquet.crc new file mode 100644 index 0000000000000000000000000000000000000000..a95845523b4f5b0e44cbb9adc4f0d8e6d6c2a185 GIT binary patch literal 1048 zcmV+z1n2u>a$^7h00ID#{sDOcK=6Jy{Zq6^oi+>2Hs-4dh;_e`?CcTOrPzSovT;8Q zrb)+tYv5jwSk_V=7872y?l(i8f>MZmV2Ty*1L`W*${Q}yiyY5klc2()p4af@QOHet zb8#l6Vq=syzGpd2#N33;P)w?o886KL7HSjBP)w?3WrLvM9AW~u<5qrdth3kX!3RVo zyp9Fp?LrebE2XIe=x~y@xK0>0{Xxs57fpJd~dW`6T8~?@YIE< zJhMBAf$J*XVL?#5miBqi4g4%Qsaro1p!AQiqYz~=`wQ-%M|x~F2*y29bisjA zS$3q{@u*J{?|Ea?yS>e0<+DSm^F;q+|+RvAIzT?9@v17!wt|~TXF{-B0R4I zj_Ond?y7M+oRNJFD|6QiYD`;j?ikBd%xTuj@b8mJSA|Iw%lksRICz4VUSQ^2SIRA& zkKcm85W`=52T{UJDhpg43984(MjBW}>o|$G_VSt8t+wbW_L!Bp*4R$9)RWO_dY&d+ zu(#!LJ~S0|hWKu3#xsr3jR~v7n>&5oNkIUHsVmz+B{(Au|^DJ$x`*TJpNnh$DvV^#UU5t+B@x@e%?590)I?oEHJDsynZ}`2`Lf#Z6qw`F|hQ>NmVB7lNuY9wRnHF&K4Ksf>q$=05w#96*oLp6!q5w4V z2$N-N>rX`)K5EYfR_XC-2umgKgxS|5e$nJ@q(Ww*Vc;W(4bp*9rh0gF*e2)=8$Y`ElQ7#Z*75*qaGNU8#(j4{OOs;)^VQN`gmCqa zj&7)!_k7 zF3vYc2>VRG2pMB+yEeaW<*Q&Vm_Ah`A}hDbQIE@3h{_Irx>}KD>2xa_h0H@#{o~b* ShGvtalX%;cc0Dw;2rCbDKn)-O literal 0 HcmV?d00001 diff --git a/fhir-server/src/test/resources/import/Patient.delta/_delta_log/.00000000000000000000.json.crc b/fhir-server/src/test/resources/import/Patient.delta/_delta_log/.00000000000000000000.json.crc new file mode 100644 index 0000000000000000000000000000000000000000..caac7a091bd3f360ad9bf1823c564cdb0b5dd9d1 GIT binary patch literal 348 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%0DAul=u>#@^wkn|3sq3Iq)QVa(q^_*;9MzeBSO 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 XjsT7Tj=*C!0tL7FMB&JRqvihrMmthO literal 0 HcmV?d00001 diff --git a/fhir-server/src/test/resources/import/Patient.parquet/._SUCCESS.crc b/fhir-server/src/test/resources/import/Patient.parquet/._SUCCESS.crc new file mode 100644 index 0000000000000000000000000000000000000000..3b7b044936a890cd8d651d349a752d819d71d22c GIT binary patch literal 8 PcmYc;N@ieSU}69O2$TUk literal 0 HcmV?d00001 diff --git a/fhir-server/src/test/resources/import/Patient.parquet/.part-00000-79c9ad61-dc37-46bb-9de3-3b890bbc4f3d-c000.snappy.parquet.crc b/fhir-server/src/test/resources/import/Patient.parquet/.part-00000-79c9ad61-dc37-46bb-9de3-3b890bbc4f3d-c000.snappy.parquet.crc new file mode 100644 index 0000000000000000000000000000000000000000..de431b0bbb3d134e5510aa3832c4c566d49e933a GIT binary patch literal 1036 zcmV+n1oQi2a$^7h00ID#{sDRS1dM=P%X7y95%Q)g43-axbc0@Q0+VPU2G? zs|&im_!hgSr%rCw;`;?<9Yx|XGdwuj2SX%(T|H_8(e^E4gq(UDcjF*K(Qy#xsX-0< z5$Cnmr==^mom4`%MOR4f`d4Liqnqmwmzd9*NZV(0gVsQ=SATf;V3fCtH;_y|UIC)M z8iWs{Tt|V%oo@I+L1o{KH;rF9ny@2&d+~d@w%q?5aZd$n6t=taPx!9yy_3s}M;_5B zrU=xyrWmYBKD_lupJI<-hyB|Snjx->!SVsMRu1+4G4%X{&8sP)+sKr;m3g`=dmDN9 z79u3Jq5%Hf@u*LWWSooc;LXYunaxO;dsePeM$*ud#&xj}m4coN7|Z|M?6&*W)T;+a z0h%k$Ui+eUA~?G!SaDcktL|)U0pZRebf|kmP)7YRGY6#8(?i8_- zPDqOCPG1nX_ieuHH-`1oxt)%`z2g`4${!pp)R*s<%;@!CR?sTn(~3P>5H9Dp!3rlV z9D5Pk@ucah@VxmZDO6EcFkxsxzwEg@R7Z``zo>GpF69G}1njNU!0y9nqMjrJN=@g{ zzdF79^?#ur-}Nf{0OU_+|qy)f?)aC5tF|QzihoWLUn{k zCXmkz#1E5Q?r!UGmc&EDr>hFvCMP#s>I{(go%V-SxjbDhjbIBg#5p(cLJm7iNT%;U=2VcPrru^1Nu3u}DY$pM#f2}b905YP3y&?|ugO>n-?QOiH>Ncs2) zzgLn_z%L&CrlLlG;hukMk5Q($m)b}WqJ&sN5{KWH`^~l=x#wSVT7W`ySwo?e3;^NPMabUN=et3a&the1CG~J`+U|#Mpt{`tMVCz&*x!qeZ2}!b(XQzzW=JRi|``of3O=-GysE>c0DxO7K~7| G7^qR2unc7Y literal 0 HcmV?d00001 diff --git a/fhir-server/src/test/resources/import/Patient.parquet/_SUCCESS b/fhir-server/src/test/resources/import/Patient.parquet/_SUCCESS new file mode 100644 index 0000000000..e69de29bb2 diff --git a/fhir-server/src/test/resources/import/Patient.parquet/part-00000-79c9ad61-dc37-46bb-9de3-3b890bbc4f3d-c000.snappy.parquet b/fhir-server/src/test/resources/import/Patient.parquet/part-00000-79c9ad61-dc37-46bb-9de3-3b890bbc4f3d-c000.snappy.parquet new file mode 100644 index 0000000000000000000000000000000000000000..b29a12d18fa998d203fac4ae6fe23f1f8ceb3dfd GIT binary patch literal 131087 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%0D}8r8MCc3<@5VZ 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 cCX4)u2lemtI{<82V(LX9RL6T literal 0 HcmV?d00001 From b413a37e03f923f9940f67cab4105ba6d82bd971 Mon Sep 17 00:00:00 2001 From: John Grimes Date: Wed, 15 Jan 2025 11:03:02 +1000 Subject: [PATCH 04/11] Add test for importing Parquet, and rename parameter to format --- .../csiro/pathling/update/ImportExecutor.java | 2 +- .../integration/modification/ImportTest.java | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/fhir-server/src/main/java/au/csiro/pathling/update/ImportExecutor.java b/fhir-server/src/main/java/au/csiro/pathling/update/ImportExecutor.java index f9c7511ae2..047245c1be 100644 --- a/fhir-server/src/main/java/au/csiro/pathling/update/ImportExecutor.java +++ b/fhir-server/src/main/java/au/csiro/pathling/update/ImportExecutor.java @@ -142,7 +142,7 @@ public OperationOutcome execute(@Nonnull @ResourceParam final Parameters inParam // Get the serialized resource type from the source parameter. final String serializationMode = sourceParam.getPart().stream() - .filter(param -> "serializationType".equals(param.getName())) + .filter(param -> "format".equals(param.getName())) .findFirst() .map(param -> ((StringType) param.getValue()).getValueAsString()).orElse(null); diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java b/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java index 6ccfc66349..3ff39a3654 100644 --- a/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java +++ b/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java @@ -182,6 +182,28 @@ void importJsonFileWithRecursiveDatatype() { DatasetAssert.of(expandedItemsDataset).hasRows(expectedDataset); } + + @Test + void importParquetFile() { + final URL parquetURL = getResourceAsUrl("import/Patient.parquet"); + importExecutor.execute(buildImportParameters(parquetURL, ResourceType.PATIENT, "parquet")); + + final Dataset result = database.read(ResourceType.PATIENT); + final Dataset expected = new DatasetBuilder(spark) + .withIdColumn() + .withRow("121503c8-9564-4b48-9086-a22df717948e") + .withRow("2b36c1e2-bbe1-45ae-8124-4adad2677702") + .withRow("7001ad9c-34d2-4eb5-8165-5fdc2147f469") + .withRow("8ee183e2-b3c0-4151-be94-b945d6aa8c6d") + .withRow("9360820c-8602-4335-8b50-c88d627a0c20") + .withRow("a7eb2ce7-1075-426c-addd-957b861b0e55") + .withRow("bbd33563-70d9-4f6d-a79a-dd1fc55f5ad9") + .withRow("beff242e-580b-47c0-9844-c1a68c36c5bf") + .withRow("e62e52ae-2d75-4070-a0ae-3cc78d35ed08") + .build(); + + DatasetAssert.of(result.select("id")).hasRows(expected); + } @Test void throwsOnUnsupportedResourceType() { From 46c23e05f6e657207e01d5f34b4fa7e63fdfab6c Mon Sep 17 00:00:00 2001 From: John Grimes Date: Wed, 15 Jan 2025 11:05:24 +1000 Subject: [PATCH 05/11] Add test for Delta import --- .../integration/modification/ImportTest.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java b/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java index 3ff39a3654..c43d3b0e7b 100644 --- a/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java +++ b/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java @@ -204,6 +204,28 @@ void importParquetFile() { DatasetAssert.of(result.select("id")).hasRows(expected); } + + @Test + void importDeltaFile() { + final URL jsonURL = getResourceAsUrl("import/Patient.delta"); + importExecutor.execute(buildImportParameters(jsonURL, ResourceType.PATIENT, "delta")); + + final Dataset result = database.read(ResourceType.PATIENT); + final Dataset expected = new DatasetBuilder(spark) + .withIdColumn() + .withRow("121503c8-9564-4b48-9086-a22df717948e") + .withRow("2b36c1e2-bbe1-45ae-8124-4adad2677702") + .withRow("7001ad9c-34d2-4eb5-8165-5fdc2147f469") + .withRow("8ee183e2-b3c0-4151-be94-b945d6aa8c6d") + .withRow("9360820c-8602-4335-8b50-c88d627a0c20") + .withRow("a7eb2ce7-1075-426c-addd-957b861b0e55") + .withRow("bbd33563-70d9-4f6d-a79a-dd1fc55f5ad9") + .withRow("beff242e-580b-47c0-9844-c1a68c36c5bf") + .withRow("e62e52ae-2d75-4070-a0ae-3cc78d35ed08") + .build(); + + DatasetAssert.of(result.select("id")).hasRows(expected); + } @Test void throwsOnUnsupportedResourceType() { From 18434244063c1ee8ce1d85e4fa9544c5c55a371f Mon Sep 17 00:00:00 2001 From: John Grimes Date: Wed, 15 Jan 2025 11:08:25 +1000 Subject: [PATCH 06/11] Add test for unsupported format --- .../test/integration/modification/ImportTest.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java b/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java index c43d3b0e7b..0ed749e40b 100644 --- a/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java +++ b/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java @@ -251,5 +251,17 @@ void throwsOnMissingId() { assertInstanceOf(InvalidRequestException.class, convertedError); assertEquals("Encountered a resource with no ID", convertedError.getMessage()); } - + + @Test + void throwsOnUnsupportedFormat() { + final List formats = Arrays.asList("ndjson", "parquet", "delta"); + for (final String format : formats) { + final InvalidUserInputError error = assertThrows(InvalidUserInputError.class, + () -> importExecutor.execute( + buildImportParameters(new URL("file://some/url"), + ResourceType.PATIENT, format)), "Unsupported format: " + format); + assertEquals("Unsupported format: " + format, error.getMessage()); + } + } + } From 5747b7393909e99d980324b2b9d426a4be0449b3 Mon Sep 17 00:00:00 2001 From: John Grimes Date: Wed, 15 Jan 2025 11:25:17 +1000 Subject: [PATCH 07/11] Add test for omission of format, and refactor --- .../integration/modification/ImportTest.java | 94 +++++++++---------- 1 file changed, 46 insertions(+), 48 deletions(-) diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java b/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java index 0ed749e40b..13b5cf4c33 100644 --- a/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java +++ b/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java @@ -70,6 +70,17 @@ class ImportTest extends ModificationTest { @Autowired ImportExecutor importExecutor; + @SuppressWarnings("SameParameterValue") + @Nonnull + Parameters buildImportParameters(@Nonnull final URL url, + @Nonnull final ResourceType resourceType) { + final Parameters parameters = new Parameters(); + final ParametersParameterComponent sourceParam = parameters.addParameter().setName("source"); + sourceParam.addPart().setName("resourceType").setValue(new CodeType(resourceType.toCode())); + sourceParam.addPart().setName("url").setValue(new UrlType(url.toExternalForm())); + return parameters; + } + @SuppressWarnings("SameParameterValue") @Nonnull Parameters buildImportParameters(@Nonnull final URL url, @@ -81,7 +92,7 @@ Parameters buildImportParameters(@Nonnull final URL url, sourceParam.addPart().setName("format").setValue(new CodeType(format)); return parameters; } - + @SuppressWarnings("SameParameterValue") @Nonnull Parameters buildImportParameters(@Nonnull final URL url, @@ -100,22 +111,18 @@ void importJsonFile() { importExecutor.execute(buildImportParameters(jsonURL, ResourceType.PATIENT, "ndjson")); final Dataset result = database.read(ResourceType.PATIENT); - final Dataset expected = new DatasetBuilder(spark) - .withIdColumn() - .withRow("121503c8-9564-4b48-9086-a22df717948e") - .withRow("2b36c1e2-bbe1-45ae-8124-4adad2677702") - .withRow("7001ad9c-34d2-4eb5-8165-5fdc2147f469") - .withRow("8ee183e2-b3c0-4151-be94-b945d6aa8c6d") - .withRow("9360820c-8602-4335-8b50-c88d627a0c20") - .withRow("a7eb2ce7-1075-426c-addd-957b861b0e55") - .withRow("bbd33563-70d9-4f6d-a79a-dd1fc55f5ad9") - .withRow("beff242e-580b-47c0-9844-c1a68c36c5bf") - .withRow("e62e52ae-2d75-4070-a0ae-3cc78d35ed08") - .build(); - - DatasetAssert.of(result.select("id")).hasRows(expected); + assertPatientDatasetMatches(result); } + @Test + void importJsonFileUsingDefault() { + final URL jsonURL = getResourceAsUrl("import/Patient.ndjson"); + importExecutor.execute(buildImportParameters(jsonURL, ResourceType.PATIENT)); + + final Dataset result = database.read(ResourceType.PATIENT); + assertPatientDatasetMatches(result); + } + @Test void mergeJsonFile() { final URL jsonURL = getResourceAsUrl("import/Patient_updates.ndjson"); @@ -182,51 +189,25 @@ void importJsonFileWithRecursiveDatatype() { DatasetAssert.of(expandedItemsDataset).hasRows(expectedDataset); } - + @Test void importParquetFile() { final URL parquetURL = getResourceAsUrl("import/Patient.parquet"); importExecutor.execute(buildImportParameters(parquetURL, ResourceType.PATIENT, "parquet")); final Dataset result = database.read(ResourceType.PATIENT); - final Dataset expected = new DatasetBuilder(spark) - .withIdColumn() - .withRow("121503c8-9564-4b48-9086-a22df717948e") - .withRow("2b36c1e2-bbe1-45ae-8124-4adad2677702") - .withRow("7001ad9c-34d2-4eb5-8165-5fdc2147f469") - .withRow("8ee183e2-b3c0-4151-be94-b945d6aa8c6d") - .withRow("9360820c-8602-4335-8b50-c88d627a0c20") - .withRow("a7eb2ce7-1075-426c-addd-957b861b0e55") - .withRow("bbd33563-70d9-4f6d-a79a-dd1fc55f5ad9") - .withRow("beff242e-580b-47c0-9844-c1a68c36c5bf") - .withRow("e62e52ae-2d75-4070-a0ae-3cc78d35ed08") - .build(); - - DatasetAssert.of(result.select("id")).hasRows(expected); + assertPatientDatasetMatches(result); } - + @Test void importDeltaFile() { - final URL jsonURL = getResourceAsUrl("import/Patient.delta"); - importExecutor.execute(buildImportParameters(jsonURL, ResourceType.PATIENT, "delta")); + final URL deltaURL = getResourceAsUrl("import/Patient.delta"); + importExecutor.execute(buildImportParameters(deltaURL, ResourceType.PATIENT, "delta")); final Dataset result = database.read(ResourceType.PATIENT); - final Dataset expected = new DatasetBuilder(spark) - .withIdColumn() - .withRow("121503c8-9564-4b48-9086-a22df717948e") - .withRow("2b36c1e2-bbe1-45ae-8124-4adad2677702") - .withRow("7001ad9c-34d2-4eb5-8165-5fdc2147f469") - .withRow("8ee183e2-b3c0-4151-be94-b945d6aa8c6d") - .withRow("9360820c-8602-4335-8b50-c88d627a0c20") - .withRow("a7eb2ce7-1075-426c-addd-957b861b0e55") - .withRow("bbd33563-70d9-4f6d-a79a-dd1fc55f5ad9") - .withRow("beff242e-580b-47c0-9844-c1a68c36c5bf") - .withRow("e62e52ae-2d75-4070-a0ae-3cc78d35ed08") - .build(); - - DatasetAssert.of(result.select("id")).hasRows(expected); + assertPatientDatasetMatches(result); } - + @Test void throwsOnUnsupportedResourceType() { final List resourceTypes = Arrays.asList(ResourceType.PARAMETERS, @@ -263,5 +244,22 @@ void throwsOnUnsupportedFormat() { assertEquals("Unsupported format: " + format, error.getMessage()); } } + + private void assertPatientDatasetMatches(@Nonnull final Dataset result) { + final Dataset expected = new DatasetBuilder(spark) + .withIdColumn() + .withRow("121503c8-9564-4b48-9086-a22df717948e") + .withRow("2b36c1e2-bbe1-45ae-8124-4adad2677702") + .withRow("7001ad9c-34d2-4eb5-8165-5fdc2147f469") + .withRow("8ee183e2-b3c0-4151-be94-b945d6aa8c6d") + .withRow("9360820c-8602-4335-8b50-c88d627a0c20") + .withRow("a7eb2ce7-1075-426c-addd-957b861b0e55") + .withRow("bbd33563-70d9-4f6d-a79a-dd1fc55f5ad9") + .withRow("beff242e-580b-47c0-9844-c1a68c36c5bf") + .withRow("e62e52ae-2d75-4070-a0ae-3cc78d35ed08") + .build(); + + DatasetAssert.of(result.select("id")).hasRows(expected); + } } From 6f877d3aeaea16124d0a2e62d2425670f62deef2 Mon Sep 17 00:00:00 2001 From: John Grimes Date: Wed, 15 Jan 2025 12:14:54 +1000 Subject: [PATCH 08/11] Get tests passing --- .../csiro/pathling/update/ImportExecutor.java | 29 ++++++++++------- .../integration/modification/ImportTest.java | 32 +++++++++---------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/fhir-server/src/main/java/au/csiro/pathling/update/ImportExecutor.java b/fhir-server/src/main/java/au/csiro/pathling/update/ImportExecutor.java index 047245c1be..f221084f48 100644 --- a/fhir-server/src/main/java/au/csiro/pathling/update/ImportExecutor.java +++ b/fhir-server/src/main/java/au/csiro/pathling/update/ImportExecutor.java @@ -29,6 +29,7 @@ import au.csiro.pathling.io.FileSystemPersistence; import au.csiro.pathling.io.ImportMode; import ca.uhn.fhir.rest.annotation.ResourceParam; +import io.delta.tables.DeltaTable; import jakarta.annotation.Nonnull; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; @@ -141,13 +142,11 @@ public OperationOutcome execute(@Nonnull @ResourceParam final Parameters inParam .orElse(ImportMode.OVERWRITE); // Get the serialized resource type from the source parameter. - final String serializationMode = sourceParam.getPart().stream() + final String format = sourceParam.getPart().stream() .filter(param -> "format".equals(param.getName())) .findFirst() .map(param -> ((StringType) param.getValue()).getValueAsString()).orElse(null); - - - + final String resourceCode = ((CodeType) resourceTypeParam.getValue()).getCode(); final ResourceType resourceType = ResourceType.fromCode(resourceCode); @@ -159,10 +158,8 @@ public OperationOutcome execute(@Nonnull @ResourceParam final Parameters inParam throw new InvalidUserInputError("Unsupported resource type: " + resourceCode); } - // Read the resources from the source URL into a dataset of strings. - final Dataset rows = readRowsFromUrl(urlParam, serializationMode, fhirEncoder); - + final Dataset rows = readRowsFromUrl(urlParam, format, fhirEncoder); log.info("Importing {} resources (mode: {})", resourceType.toCode(), importMode.getCode()); if (importMode == ImportMode.OVERWRITE) { @@ -196,12 +193,20 @@ private Dataset readRowsFromUrl(@Nonnull final ParametersParameterComponent // Check that the user is authorized to execute the operation. accessRules.ifPresent(ar -> ar.checkCanImportFrom(convertedUrl)); final FilterFunction nonBlanks = s -> !s.isBlank(); - if("parquet".equals(serializationMode)) - rowDataset = spark.read().parquet(convertedUrl); - else + if (serializationMode == null || "ndjson".equals(serializationMode)) { // Parse each line into a HAPI FHIR object, then encode to a Spark dataset. - rowDataset = spark.read().textFile(convertedUrl).filter(nonBlanks).map(jsonToResourceConverter(), - fhirEncoder).toDF(); + rowDataset = spark.read().textFile(convertedUrl).filter(nonBlanks) + .map(jsonToResourceConverter(), + fhirEncoder).toDF(); + } else if ("parquet".equals(serializationMode)) { + // Use the Spark Parquet reader. + rowDataset = spark.read().parquet(convertedUrl); + } else if ("delta".equals(serializationMode)) { + // Use the Delta Lake reader. + rowDataset = DeltaTable.forPath(spark, convertedUrl).toDF(); + } else { + throw new InvalidUserInputError("Unsupported format: " + serializationMode); + } } catch (final SecurityError e) { throw new InvalidUserInputError("Not allowed to import from URL: " + convertedUrl, e); } catch (final Exception e) { diff --git a/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java b/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java index 13b5cf4c33..ae1cc10b09 100644 --- a/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java +++ b/fhir-server/src/test/java/au/csiro/pathling/test/integration/modification/ImportTest.java @@ -80,7 +80,7 @@ Parameters buildImportParameters(@Nonnull final URL url, sourceParam.addPart().setName("url").setValue(new UrlType(url.toExternalForm())); return parameters; } - + @SuppressWarnings("SameParameterValue") @Nonnull Parameters buildImportParameters(@Nonnull final URL url, @@ -92,11 +92,12 @@ Parameters buildImportParameters(@Nonnull final URL url, sourceParam.addPart().setName("format").setValue(new CodeType(format)); return parameters; } - + @SuppressWarnings("SameParameterValue") @Nonnull Parameters buildImportParameters(@Nonnull final URL url, - @Nonnull final ResourceType resourceType, @Nonnull final String format, @Nonnull final ImportMode mode) { + @Nonnull final ResourceType resourceType, @Nonnull final String format, + @Nonnull final ImportMode mode) { final Parameters parameters = buildImportParameters(url, resourceType, format); final ParametersParameterComponent sourceParam = parameters.getParameter().stream() .filter(p -> p.getName().equals("source")).findFirst() @@ -122,7 +123,7 @@ void importJsonFileUsingDefault() { final Dataset result = database.read(ResourceType.PATIENT); assertPatientDatasetMatches(result); } - + @Test void mergeJsonFile() { final URL jsonURL = getResourceAsUrl("import/Patient_updates.ndjson"); @@ -207,7 +208,7 @@ void importDeltaFile() { final Dataset result = database.read(ResourceType.PATIENT); assertPatientDatasetMatches(result); } - + @Test void throwsOnUnsupportedResourceType() { final List resourceTypes = Arrays.asList(ResourceType.PARAMETERS, @@ -216,7 +217,7 @@ void throwsOnUnsupportedResourceType() { for (final ResourceType resourceType : resourceTypes) { final InvalidUserInputError error = assertThrows(InvalidUserInputError.class, () -> importExecutor.execute( - buildImportParameters(new URL("file://some/url"), + buildImportParameters(getResourceAsUrl("import/Patient.ndjson"), resourceType, "ndjson")), "Unsupported resource type: " + resourceType.toCode()); assertEquals("Unsupported resource type: " + resourceType.toCode(), error.getMessage()); } @@ -226,23 +227,20 @@ void throwsOnUnsupportedResourceType() { void throwsOnMissingId() { final URL jsonURL = getResourceAsUrl("import/Patient_missing_id.ndjson"); final Exception error = assertThrows(Exception.class, - () -> importExecutor.execute(buildImportParameters(jsonURL, ResourceType.PATIENT, "ndjson"))); + () -> importExecutor.execute( + buildImportParameters(jsonURL, ResourceType.PATIENT, "ndjson"))); final BaseServerResponseException convertedError = ErrorHandlingInterceptor.convertError(error); assertInstanceOf(InvalidRequestException.class, convertedError); assertEquals("Encountered a resource with no ID", convertedError.getMessage()); } - + @Test void throwsOnUnsupportedFormat() { - final List formats = Arrays.asList("ndjson", "parquet", "delta"); - for (final String format : formats) { - final InvalidUserInputError error = assertThrows(InvalidUserInputError.class, - () -> importExecutor.execute( - buildImportParameters(new URL("file://some/url"), - ResourceType.PATIENT, format)), "Unsupported format: " + format); - assertEquals("Unsupported format: " + format, error.getMessage()); - } + assertThrows(InvalidUserInputError.class, + () -> importExecutor.execute( + buildImportParameters(getResourceAsUrl("import/Patient.ndjson"), + ResourceType.PATIENT, "foo")), "Unsupported format: foo"); } private void assertPatientDatasetMatches(@Nonnull final Dataset result) { @@ -261,5 +259,5 @@ private void assertPatientDatasetMatches(@Nonnull final Dataset result) { DatasetAssert.of(result.select("id")).hasRows(expected); } - + } From 25e2c37245237a8d197aaf7763048e601ae16210 Mon Sep 17 00:00:00 2001 From: John Grimes Date: Wed, 15 Jan 2025 12:17:24 +1000 Subject: [PATCH 09/11] Update import OperationDefinition --- .../main/resources/fhir/import.OperationDefinition.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fhir-server/src/main/resources/fhir/import.OperationDefinition.json b/fhir-server/src/main/resources/fhir/import.OperationDefinition.json index 9c5709bcda..258e3801a7 100644 --- a/fhir-server/src/main/resources/fhir/import.OperationDefinition.json +++ b/fhir-server/src/main/resources/fhir/import.OperationDefinition.json @@ -47,6 +47,14 @@ "max": "1", "documentation": "A value of 'overwrite' will cause all existing resources of the specified type to be deleted and replaced with the contents of the source file. A value of 'merge' will match existing resources with updated resources in the source file based on their ID, and either update the existing resources or add new resources as appropriate. The default value is 'overwrite'.", "type": "code" + }, + { + "name": "format", + "use": "in", + "min": 0, + "max": "1", + "documentation": "Indicates the format of the source file. Possible values are 'ndjson', 'parquet' and 'delta'. The default value is 'ndjson'.", + "type": "code" } ] } From dbcbbafc0735ffddd46d80db44c63a141c790976 Mon Sep 17 00:00:00 2001 From: John Grimes Date: Wed, 15 Jan 2025 12:22:55 +1000 Subject: [PATCH 10/11] Update documentation --- site/docs/server/operations/import.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/site/docs/server/operations/import.md b/site/docs/server/operations/import.md index 6a7f3f8021..3ae80bfe10 100644 --- a/site/docs/server/operations/import.md +++ b/site/docs/server/operations/import.md @@ -9,12 +9,19 @@ description: The import operation allows FHIR data to be imported into the serve This operation allows FHIR R4 data to be imported into the server, making it available for query via other operations such -as [search](./search), [aggregate](./aggregate) and [extract](./extract). This -operation accepts the [NDJSON](https://hl7.org/fhir/R4/nd-json.html) format, and -links to retrieve that data are provided rather that sending the data inline -within the request itself. This is to allow for large data sets to be imported +as [search](./search), [aggregate](./aggregate) and [extract](./extract). Links +to retrieve that data are provided rather that sending the data inline within +the request itself. This is to allow for large data sets to be imported efficiently. +Source formats currently supported are: + +* [NDJSON](https://hl7.org/fhir/R4/nd-json.html) format +* [Parquet](https://parquet.apache.org/) conforming to + the [Pathling schema](../../libraries/encoders/schema) +* [Delta Lake](https://delta.io/) conforming to + the [Pathling schema](../../libraries/encoders/schema) + Currently Pathling supports retrieval of NDJSON files from [Amazon S3](https://aws.amazon.com/s3/) (`s3://`), [HDFS](https://hadoop.apache.org/docs/r1.2.1/hdfs_design.html) (`hdfs://`) and @@ -61,6 +68,9 @@ following parameters: resources with updated resources in the source file based on their ID, and either update the existing resources or add new resources as appropriate. The default value is `overwrite`. + - `format [0..1] (code)` - Indicates the format of the source file. + Possible values are `ndjson`, `parquet` and `delta`. The default value + is `ndjson`. ## Response From 6a0b06b4fafe2e1d91598dd9625fd7e1cf3d1f46 Mon Sep 17 00:00:00 2001 From: John Grimes Date: Fri, 31 Jan 2025 14:06:24 +1000 Subject: [PATCH 11/11] Refactor ImportExecutor to use ImportFormat enum --- .../csiro/pathling/update/ImportExecutor.java | 46 ++++++++------ .../csiro/pathling/update/ImportFormat.java | 62 +++++++++++++++++++ 2 files changed, 90 insertions(+), 18 deletions(-) create mode 100644 fhir-server/src/main/java/au/csiro/pathling/update/ImportFormat.java diff --git a/fhir-server/src/main/java/au/csiro/pathling/update/ImportExecutor.java b/fhir-server/src/main/java/au/csiro/pathling/update/ImportExecutor.java index f221084f48..37baf4142b 100644 --- a/fhir-server/src/main/java/au/csiro/pathling/update/ImportExecutor.java +++ b/fhir-server/src/main/java/au/csiro/pathling/update/ImportExecutor.java @@ -113,7 +113,7 @@ public ImportExecutor(@Nonnull final SparkSession spark, public OperationOutcome execute(@Nonnull @ResourceParam final Parameters inParams) { // Parse and validate the JSON request. final List sourceParams = inParams.getParameter().stream() - .filter(param -> "source".equals(param.getName())).collect(Collectors.toList()); + .filter(param -> "source".equals(param.getName())).toList(); if (sourceParams.isEmpty()) { throw new InvalidUserInputError("Must provide at least one source parameter"); } @@ -142,10 +142,18 @@ public OperationOutcome execute(@Nonnull @ResourceParam final Parameters inParam .orElse(ImportMode.OVERWRITE); // Get the serialized resource type from the source parameter. - final String format = sourceParam.getPart().stream() + final ImportFormat format = sourceParam.getPart().stream() .filter(param -> "format".equals(param.getName())) .findFirst() - .map(param -> ((StringType) param.getValue()).getValueAsString()).orElse(null); + .map(param -> { + final String formatCode = ((StringType) param.getValue()).getValue(); + try { + return ImportFormat.fromCode(formatCode); + } catch (final IllegalArgumentException e) { + throw new InvalidUserInputError("Unsupported format: " + formatCode); + } + }) + .orElse(ImportFormat.NDJSON); final String resourceCode = ((CodeType) resourceTypeParam.getValue()).getCode(); final ResourceType resourceType = ResourceType.fromCode(resourceCode); @@ -184,7 +192,7 @@ public OperationOutcome execute(@Nonnull @ResourceParam final Parameters inParam @Nonnull private Dataset readRowsFromUrl(@Nonnull final ParametersParameterComponent urlParam, - final String serializationMode, final ExpressionEncoder fhirEncoder) { + final ImportFormat format, final ExpressionEncoder fhirEncoder) { final String url = ((UrlType) urlParam.getValue()).getValueAsString(); final String decodedUrl = URLDecoder.decode(url, StandardCharsets.UTF_8); final String convertedUrl = FileSystemPersistence.convertS3ToS3aUrl(decodedUrl); @@ -193,20 +201,22 @@ private Dataset readRowsFromUrl(@Nonnull final ParametersParameterComponent // Check that the user is authorized to execute the operation. accessRules.ifPresent(ar -> ar.checkCanImportFrom(convertedUrl)); final FilterFunction nonBlanks = s -> !s.isBlank(); - if (serializationMode == null || "ndjson".equals(serializationMode)) { - // Parse each line into a HAPI FHIR object, then encode to a Spark dataset. - rowDataset = spark.read().textFile(convertedUrl).filter(nonBlanks) - .map(jsonToResourceConverter(), - fhirEncoder).toDF(); - } else if ("parquet".equals(serializationMode)) { - // Use the Spark Parquet reader. - rowDataset = spark.read().parquet(convertedUrl); - } else if ("delta".equals(serializationMode)) { - // Use the Delta Lake reader. - rowDataset = DeltaTable.forPath(spark, convertedUrl).toDF(); - } else { - throw new InvalidUserInputError("Unsupported format: " + serializationMode); - } + + rowDataset = switch (format) { + case NDJSON -> + // Parse each line into a HAPI FHIR object, then encode to a Spark dataset. + spark.read() + .textFile(convertedUrl).filter(nonBlanks) + .map(jsonToResourceConverter(), fhirEncoder) + .toDF(); + case PARQUET -> + // Use the Spark Parquet reader. + spark.read() + .parquet(convertedUrl); + case DELTA -> + // Use the Delta Lake reader. + DeltaTable.forPath(spark, convertedUrl).toDF(); + }; } catch (final SecurityError e) { throw new InvalidUserInputError("Not allowed to import from URL: " + convertedUrl, e); } catch (final Exception e) { diff --git a/fhir-server/src/main/java/au/csiro/pathling/update/ImportFormat.java b/fhir-server/src/main/java/au/csiro/pathling/update/ImportFormat.java new file mode 100644 index 0000000000..4f49f1b32b --- /dev/null +++ b/fhir-server/src/main/java/au/csiro/pathling/update/ImportFormat.java @@ -0,0 +1,62 @@ +/* + * Copyright 2025 Commonwealth Scientific and Industrial Research + * Organisation (CSIRO) ABN 41 687 119 230. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package au.csiro.pathling.update; + +import lombok.Getter; + +/** + * Represents the supported formats for resource import. + */ +@Getter +public enum ImportFormat { + /** + * Newline-delimited JSON (NDJSON) format. + */ + NDJSON("ndjson"), + /** + * Parquet format. + */ + PARQUET("parquet"), + /** + * Delta Lake format. + */ + DELTA("delta"); + + private final String code; + + ImportFormat(final String code) { + this.code = code; + } + + /** + * Resolve an ImportFormat enum from its string code. + * + * @param code The string code to resolve. + * @return An ImportFormat if a match is found. + * @throws IllegalArgumentException if no match can be found. + */ + public static ImportFormat fromCode(final String code) { + for (final ImportFormat format : ImportFormat.values()) { + if (format.getCode().equalsIgnoreCase(code)) { + return format; + } + } + throw new IllegalArgumentException("Unsupported format: " + code); + } + +}