@@ -979,4 +979,259 @@ private String addDomainsToTableRow(String csvLine, String newDomains) {
979979 }
980980 return String .join ("," , parts );
981981 }
982+
983+ @ Test
984+ void test_recursiveImportCustomPropertyExtension (TestNamespace ns )
985+ throws IOException , InterruptedException {
986+ String propName = ns .prefix ("potato" );
987+ String serverUrl = SdkClients .getServerUrl ();
988+ String token = SdkClients .getAdminToken ();
989+ com .fasterxml .jackson .databind .ObjectMapper mapper =
990+ new com .fasterxml .jackson .databind .ObjectMapper ();
991+ HttpClient client = HttpClient .newHttpClient ();
992+
993+ HttpRequest getStringTypeReq =
994+ HttpRequest .newBuilder ()
995+ .uri (URI .create (serverUrl + "/v1/metadata/types/name/string" ))
996+ .header ("Authorization" , "Bearer " + token )
997+ .GET ()
998+ .build ();
999+ HttpResponse <String > stringTypeResp =
1000+ client .send (getStringTypeReq , HttpResponse .BodyHandlers .ofString ());
1001+ assertEquals (200 , stringTypeResp .statusCode (), "Should fetch string type" );
1002+
1003+ HttpRequest getTableTypeReq =
1004+ HttpRequest .newBuilder ()
1005+ .uri (URI .create (serverUrl + "/v1/metadata/types/name/table" ))
1006+ .header ("Authorization" , "Bearer " + token )
1007+ .GET ()
1008+ .build ();
1009+ HttpResponse <String > tableTypeResp =
1010+ client .send (getTableTypeReq , HttpResponse .BodyHandlers .ofString ());
1011+ assertEquals (200 , tableTypeResp .statusCode (), "Should fetch table type" );
1012+
1013+ com .fasterxml .jackson .databind .JsonNode stringTypeNode = mapper .readTree (stringTypeResp .body ());
1014+ com .fasterxml .jackson .databind .JsonNode tableTypeNode = mapper .readTree (tableTypeResp .body ());
1015+ String tableTypeId = tableTypeNode .get ("id" ).asText ();
1016+
1017+ java .util .Map <String , Object > propertyTypeRef =
1018+ java .util .Map .of (
1019+ "id" , stringTypeNode .get ("id" ).asText (),
1020+ "type" , "type" ,
1021+ "name" , stringTypeNode .get ("name" ).asText (),
1022+ "fullyQualifiedName" , stringTypeNode .get ("fullyQualifiedName" ).asText ());
1023+ String customPropertyBody =
1024+ mapper .writeValueAsString (
1025+ java .util .Map .of (
1026+ "name" ,
1027+ propName ,
1028+ "description" ,
1029+ "Test extension property for recursive import" ,
1030+ "propertyType" ,
1031+ propertyTypeRef ));
1032+
1033+ HttpRequest registerPropReq =
1034+ HttpRequest .newBuilder ()
1035+ .uri (URI .create (serverUrl + "/v1/metadata/types/" + tableTypeId ))
1036+ .header ("Authorization" , "Bearer " + token )
1037+ .header ("Content-Type" , "application/json" )
1038+ .PUT (HttpRequest .BodyPublishers .ofString (customPropertyBody ))
1039+ .build ();
1040+ HttpResponse <String > registerResp =
1041+ client .send (registerPropReq , HttpResponse .BodyHandlers .ofString ());
1042+ assertEquals (200 , registerResp .statusCode (), "Should register custom property on table type" );
1043+
1044+ try {
1045+ DatabaseService service =
1046+ createEntity (createMinimalRequest (ns ).withName (ns .prefix ("ext_svc" )));
1047+ Database database =
1048+ SdkClients .adminClient ()
1049+ .databases ()
1050+ .create (
1051+ new CreateDatabase ()
1052+ .withName (ns .prefix ("ext_db" ))
1053+ .withService (service .getFullyQualifiedName ()));
1054+ DatabaseSchema schema =
1055+ SdkClients .adminClient ()
1056+ .databaseSchemas ()
1057+ .create (
1058+ new CreateDatabaseSchema ()
1059+ .withName (ns .prefix ("ext_schema" ))
1060+ .withDatabase (database .getFullyQualifiedName ()));
1061+
1062+ String tableName = ns .prefix ("ext_tbl" );
1063+ String tableFqn = schema .getFullyQualifiedName () + "." + tableName ;
1064+
1065+ String validCsv =
1066+ buildRecursiveCsv (
1067+ database , schema , tableName , tableFqn , "" , propName + ":s3://bucket/file.csv" );
1068+ CsvImportResult validResult =
1069+ importCsvRecursive (service .getFullyQualifiedName (), validCsv , true );
1070+ assertEquals (ApiStatus .SUCCESS , validResult .getStatus (), validResult .getImportResultsCsv ());
1071+ assertEquals (0 , validResult .getNumberOfRowsFailed ());
1072+ assertEquals (3 , validResult .getNumberOfRowsProcessed ());
1073+ assertEquals (3 , validResult .getNumberOfRowsPassed ());
1074+
1075+ String badExtCsv =
1076+ buildRecursiveCsv (
1077+ database , schema , tableName , tableFqn , "" , "unknown_prop_xyz_test:somevalue" );
1078+ CsvImportResult badResult =
1079+ importCsvRecursive (service .getFullyQualifiedName (), badExtCsv , true );
1080+ assertEquals (ApiStatus .PARTIAL_SUCCESS , badResult .getStatus ());
1081+ assertEquals (1 , badResult .getNumberOfRowsFailed ());
1082+ assertEquals (3 , badResult .getNumberOfRowsProcessed ());
1083+ assertEquals (2 , badResult .getNumberOfRowsPassed ());
1084+
1085+ String dedupCsv =
1086+ buildRecursiveCsv (
1087+ database ,
1088+ schema ,
1089+ tableName ,
1090+ tableFqn ,
1091+ "invalidownerformat" ,
1092+ "unknown_prop_xyz_test:somevalue" );
1093+ CsvImportResult dedupResult =
1094+ importCsvRecursive (service .getFullyQualifiedName (), dedupCsv , true );
1095+ assertEquals (
1096+ 1 ,
1097+ dedupResult .getNumberOfRowsFailed (),
1098+ "Multi-field failure on one row must count as 1 failed row" );
1099+
1100+ } finally {
1101+ removeCustomPropertyFromType (tableTypeId , propName , token );
1102+ }
1103+ }
1104+
1105+ private String buildRecursiveCsv (
1106+ Database database ,
1107+ DatabaseSchema schema ,
1108+ String tableName ,
1109+ String tableFqn ,
1110+ String tableOwner ,
1111+ String tableExtension ) {
1112+ String header =
1113+ "name*,displayName,description,owner,tags,glossaryTerms,tiers,certification,"
1114+ + "retentionPeriod,sourceUrl,domains,extension,entityType*,fullyQualifiedName,"
1115+ + "column.dataTypeDisplay,column.dataType,column.arrayDataType,column.dataLength,"
1116+ + "storedProcedure.code,storedProcedure.language" ;
1117+ String dbRow =
1118+ csvRow (
1119+ database .getName (),
1120+ "" ,
1121+ "" ,
1122+ "" ,
1123+ "" ,
1124+ "" ,
1125+ "" ,
1126+ "" ,
1127+ "" ,
1128+ "" ,
1129+ "" ,
1130+ "" ,
1131+ "database" ,
1132+ database .getFullyQualifiedName (),
1133+ "" ,
1134+ "" ,
1135+ "" ,
1136+ "" ,
1137+ "" ,
1138+ "" );
1139+ String schemaRow =
1140+ csvRow (
1141+ schema .getName (),
1142+ "" ,
1143+ "" ,
1144+ "" ,
1145+ "" ,
1146+ "" ,
1147+ "" ,
1148+ "" ,
1149+ "" ,
1150+ "" ,
1151+ "" ,
1152+ "" ,
1153+ "databaseSchema" ,
1154+ schema .getFullyQualifiedName (),
1155+ "" ,
1156+ "" ,
1157+ "" ,
1158+ "" ,
1159+ "" ,
1160+ "" );
1161+ String tableRow =
1162+ csvRow (
1163+ tableName ,
1164+ "" ,
1165+ "" ,
1166+ tableOwner ,
1167+ "" ,
1168+ "" ,
1169+ "" ,
1170+ "" ,
1171+ "" ,
1172+ "" ,
1173+ "" ,
1174+ tableExtension ,
1175+ "table" ,
1176+ tableFqn ,
1177+ "" ,
1178+ "" ,
1179+ "" ,
1180+ "" ,
1181+ "" ,
1182+ "" );
1183+ return header + "\n " + dbRow + "\n " + schemaRow + "\n " + tableRow + "\n " ;
1184+ }
1185+
1186+ private void removeCustomPropertyFromType (String typeId , String propName , String token )
1187+ throws IOException , InterruptedException {
1188+ com .fasterxml .jackson .databind .ObjectMapper localMapper =
1189+ new com .fasterxml .jackson .databind .ObjectMapper ();
1190+ HttpClient client = HttpClient .newHttpClient ();
1191+ String baseUrl = SdkClients .getServerUrl ();
1192+ String getUrl = baseUrl + "/v1/metadata/types/" + typeId + "?fields=customProperties" ;
1193+ HttpRequest getReq =
1194+ HttpRequest .newBuilder ()
1195+ .uri (URI .create (getUrl ))
1196+ .header ("Authorization" , "Bearer " + token )
1197+ .GET ()
1198+ .build ();
1199+ HttpResponse <String > getResp = client .send (getReq , HttpResponse .BodyHandlers .ofString ());
1200+ if (getResp .statusCode () != 200 ) {
1201+ return ;
1202+ }
1203+ com .fasterxml .jackson .databind .JsonNode typeNode = localMapper .readTree (getResp .body ());
1204+ com .fasterxml .jackson .databind .JsonNode customProps = typeNode .get ("customProperties" );
1205+ if (customProps == null || !customProps .isArray ()) {
1206+ return ;
1207+ }
1208+ for (int i = 0 ; i < customProps .size (); i ++) {
1209+ if (propName .equals (customProps .get (i ).path ("name" ).asText ())) {
1210+ String patchBody = "[{\" op\" :\" remove\" ,\" path\" :\" /customProperties/" + i + "\" }]" ;
1211+ HttpRequest patchReq =
1212+ HttpRequest .newBuilder ()
1213+ .uri (URI .create (baseUrl + "/v1/metadata/types/" + typeId ))
1214+ .header ("Authorization" , "Bearer " + token )
1215+ .header ("Content-Type" , "application/json-patch+json" )
1216+ .method ("PATCH" , HttpRequest .BodyPublishers .ofString (patchBody ))
1217+ .build ();
1218+ client .send (patchReq , HttpResponse .BodyHandlers .ofString ());
1219+ break ;
1220+ }
1221+ }
1222+ }
1223+
1224+ private String csvRow (String ... fields ) {
1225+ StringBuilder sb = new StringBuilder ();
1226+ for (int i = 0 ; i < fields .length ; i ++) {
1227+ if (i > 0 ) sb .append ("," );
1228+ String field = fields [i ];
1229+ if (field .contains ("," ) || field .contains ("\" " ) || field .contains ("\n " )) {
1230+ sb .append ('"' ).append (field .replace ("\" " , "\" \" " )).append ('"' );
1231+ } else {
1232+ sb .append (field );
1233+ }
1234+ }
1235+ return sb .toString ();
1236+ }
9821237}
0 commit comments