From 42cce1aa2aee0f38d3d79cc19c423e0b3d264f7f Mon Sep 17 00:00:00 2001 From: "helmi.nour" Date: Tue, 26 Apr 2022 18:56:40 +0100 Subject: [PATCH 1/5] adding v2 predefined results formats --- .../java/com/relationalai/ArrowRelation.java | 27 ++ .../main/java/com/relationalai/Client.java | 316 +++++++++--------- .../IntegrityConstraintViolation.java | 29 ++ .../com/relationalai/MultipartReader.java | 178 ++++++++++ .../main/java/com/relationalai/Source.java | 30 ++ .../TransactionAsyncCompactResponse.java | 17 + .../relationalai/TransactionAsyncFile.java | 47 +++ .../TransactionAsyncMetadataResponse.java | 33 ++ .../TransactionAsyncResponse.java | 48 +++ .../relationalai/TransactionAsyncResult.java | 18 + .../TransactionAsyncSingleResponse.java | 8 + .../TransactionsAsyncMultipleResponses.java | 10 + .../com/relationalai/ExecuteAsyncTest.java | 39 ++- .../test/java/com/relationalai/UserTest.java | 8 +- 14 files changed, 633 insertions(+), 175 deletions(-) create mode 100644 rai-sdk/src/main/java/com/relationalai/ArrowRelation.java create mode 100644 rai-sdk/src/main/java/com/relationalai/IntegrityConstraintViolation.java create mode 100644 rai-sdk/src/main/java/com/relationalai/MultipartReader.java create mode 100644 rai-sdk/src/main/java/com/relationalai/Source.java create mode 100644 rai-sdk/src/main/java/com/relationalai/TransactionAsyncCompactResponse.java create mode 100644 rai-sdk/src/main/java/com/relationalai/TransactionAsyncFile.java create mode 100644 rai-sdk/src/main/java/com/relationalai/TransactionAsyncMetadataResponse.java create mode 100644 rai-sdk/src/main/java/com/relationalai/TransactionAsyncResponse.java create mode 100644 rai-sdk/src/main/java/com/relationalai/TransactionAsyncResult.java create mode 100644 rai-sdk/src/main/java/com/relationalai/TransactionAsyncSingleResponse.java create mode 100644 rai-sdk/src/main/java/com/relationalai/TransactionsAsyncMultipleResponses.java diff --git a/rai-sdk/src/main/java/com/relationalai/ArrowRelation.java b/rai-sdk/src/main/java/com/relationalai/ArrowRelation.java new file mode 100644 index 00000000..37bc18e7 --- /dev/null +++ b/rai-sdk/src/main/java/com/relationalai/ArrowRelation.java @@ -0,0 +1,27 @@ +package com.relationalai; + +import java.util.List; + +public class ArrowRelation extends Entity { + String relationId; + List table; + + public ArrowRelation(String relationId, List table) { + this.relationId = relationId; + this.table = table; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof ArrowRelation)) { + return false; + } + var that = (ArrowRelation) o; + + return this.relationId.equals(that.relationId) && this.table.equals(that.table); + } +} diff --git a/rai-sdk/src/main/java/com/relationalai/Client.java b/rai-sdk/src/main/java/com/relationalai/Client.java index 23377f1c..9cd6c309 100644 --- a/rai-sdk/src/main/java/com/relationalai/Client.java +++ b/rai-sdk/src/main/java/com/relationalai/Client.java @@ -16,14 +16,12 @@ package com.relationalai; -import com.fasterxml.jackson.databind.ObjectMapper; import com.jsoniter.any.Any; +import com.jsoniter.spi.JsonException; import org.apache.arrow.memory.RootAllocator; import org.apache.arrow.vector.FieldVector; import org.apache.arrow.vector.VectorSchemaRoot; import org.apache.arrow.vector.ipc.ArrowStreamReader; -import org.apache.arrow.vector.types.pojo.Field; -import org.apache.commons.fileupload.MultipartStream; import java.io.*; import java.net.URI; @@ -36,6 +34,7 @@ import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.*; +import java.util.stream.Collectors; public class Client { public static final String DEFAULT_REGION = "us-east"; @@ -250,7 +249,7 @@ void authenticate(HttpRequest.Builder builder, ClientCredentials credentials) builder.header("Authorization", String.format("Bearer %s", accessToken.token)); } - String sendRequest(HttpRequest.Builder builder) + Object sendRequest(HttpRequest.Builder builder) throws HttpError, InterruptedException, IOException { addHeaders(builder, defaultHeaders); authenticate(builder, this.credentials); @@ -265,99 +264,65 @@ String sendRequest(HttpRequest.Builder builder) if (contentType.toLowerCase().contains("application/json")) return new String(response.body(), StandardCharsets.UTF_8); else if (contentType.toLowerCase().contains("multipart/form-data")) { - return parseMultipartResponse(response); + return MultipartReader.parseMultipartResponse(response); } else { throw new HttpError(statusCode, "invalid response type"); } } - - private String parseMultipartResponse(HttpResponse response) throws IOException, HttpError { - int statusCode = response.statusCode(); - String contentType = response.headers().firstValue("Content-Type").orElse(""); - - List result = new ArrayList<>(); - - String boundary = null; - for (String part: contentType.split(";")) - if (part.trim().startsWith("boundary")) - boundary = part.split("=")[1].trim(); - - MultipartStream multipartStream = new MultipartStream( - new ByteArrayInputStream(response.body()), - boundary.getBytes(StandardCharsets.UTF_8), - 1024, - null - ); - - boolean nextPart = multipartStream.skipPreamble(); - - while (nextPart) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - - String partHeaders = multipartStream.readHeaders(); - multipartStream.readBodyData(out); - - String partContentType = null; - for (String row : partHeaders.split("\n")) { - if (row.toLowerCase().startsWith("content-type")) - partContentType = row.split(":")[1].trim(); - } - - if (partContentType.toLowerCase().equals("application/json")) { - result.add(out.toString(StandardCharsets.UTF_8)); - } else if (partContentType.toLowerCase().equals("application/vnd.apache.arrow.stream")) { - result.add(parseArrowResponse(out)); - out.close(); - } else { - throw new HttpError(statusCode, String.format("unknown part content type: %s", partContentType)); + private List readArrowFiles(List files) throws IOException { + var output = new ArrayList(); + for (var file : files) { + if ("application/vnd.apache.arrow.stream".equals(file.contentType.toLowerCase())) { + ByteArrayInputStream in = new ByteArrayInputStream(file.data); + List fieldVectors = null; + RootAllocator allocator = new RootAllocator(Long.MAX_VALUE); + + try(ArrowStreamReader arrowStreamReader = new ArrowStreamReader(in, allocator)){ + VectorSchemaRoot root = arrowStreamReader.getVectorSchemaRoot(); + fieldVectors = root.getFieldVectors(); + + while(arrowStreamReader.loadNextBatch()) { + for(FieldVector fieldVector : fieldVectors) { + List values = new ArrayList<>(); + + for (int i = 0; i < fieldVector.getValueCount(); i ++) { + values.add(fieldVector.getObject(i)); + } + output.add(new ArrowRelation(fieldVector.getField().getName(), values)); + } + } + } finally { + if (in != null ) { + in.close(); + } + if (fieldVectors != null) { + for (FieldVector fieldVector : fieldVectors) { + if (fieldVector != null) { + fieldVector.close(); + } + } + } + allocator.close(); + } } - - nextPart = multipartStream.readBoundary(); } - - return result.toString(); + return output; } - private String parseArrowResponse(ByteArrayOutputStream out) throws IOException { - List output = new ArrayList<>(); - - ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); - List fieldVectors = null; - RootAllocator allocator = new RootAllocator(Long.MAX_VALUE); - try(ArrowStreamReader arrowStreamReader = new ArrowStreamReader(in, allocator)){ - - VectorSchemaRoot root = arrowStreamReader.getVectorSchemaRoot(); - fieldVectors = root.getFieldVectors(); + private List parseProblemsResult(String rsp) { + var output = new ArrayList(); + var problems = Json.deserialize(rsp).asList(); - while(arrowStreamReader.loadNextBatch()) { - for(FieldVector fieldVector : fieldVectors) { - Map> col = new HashMap<>(); - - col.put(fieldVector.getField().getName(), new ArrayList<>()); - for (int i = 0; i < fieldVector.getValueCount(); i ++) { - col.get(fieldVector.getField().getName()).add(fieldVector.getObject(i)); - } - output.add(col); - } - } - } finally { - if (in != null ) { - in.close(); - } - if (fieldVectors != null) { - for (FieldVector fieldVector : fieldVectors) { - if (fieldVector != null) { - fieldVector.close(); - } - } + for (var problem : problems) { + var data = Json.serialize(problem); + try { + output.add(Json.deserialize(data, IntegrityConstraintViolation.class)); + } catch (JsonException e) { + output.add(Json.deserialize(data, Problem.class)); } - allocator.close(); } - - // serialize map to json string - return new ObjectMapper().writeValueAsString(output); + return output; } - static void printRequest(HttpRequest request) { System.out.printf("%s %s\n", request.method(), request.uri()); for (Map.Entry> entry : request.headers().map().entrySet()) { @@ -368,47 +333,47 @@ static void printRequest(HttpRequest request) { // todo: figure out how to get the body from a request (non-trivial) } - public String request(String method, String path, QueryParams params) + public Object request(String method, String path, QueryParams params) throws HttpError, InterruptedException, IOException { return sendRequest(newRequestBuilder(method, path, params)); } - public String request(String method, String path, QueryParams params, String body) + public Object request(String method, String path, QueryParams params, String body) throws HttpError, InterruptedException, IOException { return sendRequest(newRequestBuilder(method, path, params, body)); } - public String delete(String path) + public Object delete(String path) throws HttpError, InterruptedException, IOException { return delete(path, null, null); } - public String delete(String path, QueryParams params, String body) + public Object delete(String path, QueryParams params, String body) throws HttpError, InterruptedException, IOException { return request("DELETE", path, params, body); } - public String get(String path) + public Object get(String path) throws HttpError, InterruptedException, IOException { return get(path, null); } - public String get(String path, QueryParams params) + public Object get(String path, QueryParams params) throws HttpError, InterruptedException, IOException { return request("GET", path, params); } - public String patch(String path, QueryParams params, String body) + public Object patch(String path, QueryParams params, String body) throws HttpError, InterruptedException, IOException { return request("PATCH", path, params, body); } - public String post(String path, QueryParams params, String body) + public Object post(String path, QueryParams params, String body) throws HttpError, InterruptedException, IOException { return request("POST", path, params, body); } - public String put(String path, QueryParams params, String body) + public Object put(String path, QueryParams params, String body) throws HttpError, InterruptedException, IOException { return request("PUT", path, params, body); } @@ -476,7 +441,7 @@ public DeleteDatabaseResponse deleteDatabase(String database) var req = new DeleteDatabaseRequest(database); var rsp = delete(PATH_DATABASE, null, Json.serialize(req)); // once this is complete, there is no longer a database resource to return - return Json.deserialize(rsp, DeleteDatabaseResponse.class); + return Json.deserialize((String) rsp, DeleteDatabaseResponse.class); } public Database getDatabase(String database) @@ -484,7 +449,7 @@ public Database getDatabase(String database) var params = new QueryParams(); params.put("name", database); var rsp = get(PATH_DATABASE, params); - var databases = Json.deserialize(rsp, GetDatabaseResponse.class).databases; + var databases = Json.deserialize((String) rsp, GetDatabaseResponse.class).databases; if (databases.length == 0) throw new HttpError(404); return databases[0]; @@ -502,8 +467,8 @@ public Database[] listDatabases(String state) params = new QueryParams(); params.put("state", state); } - String rsp = get(PATH_DATABASE, params); - return Json.deserialize(rsp, ListDatabasesResponse.class).databases; + var rsp = get(PATH_DATABASE, params); + return Json.deserialize((String) rsp, ListDatabasesResponse.class).databases; } // Engines @@ -519,7 +484,7 @@ public Engine createEngine(String engine, String size) size = "XS"; var req = new CreateEngineRequest(this.region, engine, size); var rsp = put(PATH_ENGINE, null, Json.serialize(req)); - return Json.deserialize(rsp, CreateEngineResponse.class).engine; + return Json.deserialize((String) rsp, CreateEngineResponse.class).engine; } public Engine createEngineWait(String engine) @@ -561,7 +526,7 @@ public Engine getEngine(String engine) params.put("name", engine); params.put("deleted_on", ""); var rsp = get(PATH_ENGINE, params); - var engines = Json.deserialize(rsp, GetEngineResponse.class).engines; + var engines = Json.deserialize((String) rsp, GetEngineResponse.class).engines; if (engines.length == 0) throw new HttpError(404); return engines[0]; @@ -580,7 +545,7 @@ public Engine[] listEngines(String state) params.put("state", state); } var rsp = get(PATH_ENGINE, params); - return Json.deserialize(rsp, ListEnginesResponse.class).engines; + return Json.deserialize((String) rsp, ListEnginesResponse.class).engines; } // OAuth clients @@ -594,13 +559,13 @@ public OAuthClientExtra createOAuthClient(String name, String[] permissions) throws HttpError, InterruptedException, IOException { var req = new CreateOAuthClientRequest(name, permissions); var rsp = post(PATH_OAUTH_CLIENTS, null, Json.serialize(req)); - return Json.deserialize(rsp, CreateOAuthClientResponse.class).client; + return Json.deserialize((String) rsp, CreateOAuthClientResponse.class).client; } public DeleteOAuthClientResponse deleteOAuthClient(String id) throws HttpError, InterruptedException, IOException { var rsp = delete(makePath(PATH_OAUTH_CLIENTS, id)); - return Json.deserialize(rsp, DeleteOAuthClientResponse.class); + return Json.deserialize((String) rsp, DeleteOAuthClientResponse.class); } public OAuthClient findOAuthClient(String name) @@ -616,13 +581,13 @@ public OAuthClient findOAuthClient(String name) public OAuthClientExtra getOAuthClient(String id) throws HttpError, InterruptedException, IOException { var rsp = get(makePath(PATH_OAUTH_CLIENTS, id)); - return Json.deserialize(rsp, GetOAuthClientResponse.class).client; + return Json.deserialize((String) rsp, GetOAuthClientResponse.class).client; } public OAuthClient[] listOAuthClients() throws HttpError, InterruptedException, IOException { var rsp = get(PATH_OAUTH_CLIENTS); - return Json.deserialize(rsp, ListOAuthClientsResponse.class).clients; + return Json.deserialize((String) rsp, ListOAuthClientsResponse.class).clients; } // Users @@ -636,13 +601,13 @@ public User createUser(String email, String[] roles) throws HttpError, InterruptedException, IOException { var req = new CreateUserRequest(email, roles); var rsp = post(PATH_USERS, null, Json.serialize(req)); - return Json.deserialize(rsp, CreateUserResponse.class).user; + return Json.deserialize((String) rsp, CreateUserResponse.class).user; } public DeleteUserResponse deleteUser(String id) throws HttpError, InterruptedException, IOException { var rsp = delete(makePath(PATH_USERS, id)); - return Json.deserialize(rsp, DeleteUserResponse.class); + return Json.deserialize((String) rsp, DeleteUserResponse.class); } public User disableUser(String id) @@ -669,13 +634,13 @@ public User findUser(String email) public User getUser(String id) throws HttpError, InterruptedException, IOException { var rsp = get(makePath(PATH_USERS, id)); - return Json.deserialize(rsp, GetUserResponse.class).user; + return Json.deserialize((String) rsp, GetUserResponse.class).user; } public User[] listUsers() throws HttpError, InterruptedException, IOException { var rsp = get(PATH_USERS); - return Json.deserialize(rsp, ListUsersResponse.class).users; + return Json.deserialize((String) rsp, ListUsersResponse.class).users; } public User updateUser(String id, String status) @@ -696,7 +661,7 @@ public User updateUser(String id, String status, String[] roles) public User updateUser(String id, UpdateUserRequest req) throws HttpError, InterruptedException, IOException { var rsp = patch(makePath(PATH_USERS, id), null, Json.serialize(req)); - return Json.deserialize(rsp, UpdateUserResponse.class).user; + return Json.deserialize((String) rsp, UpdateUserResponse.class).user; } // Transactions @@ -728,92 +693,121 @@ public TransactionResult execute( var action = DbAction.makeQueryAction(source, inputs); var body = tx.payload(action); var rsp = post(PATH_TRANSACTION, tx.queryParams(), body); - return Json.deserialize(rsp, TransactionResult.class); + return Json.deserialize((String) rsp, TransactionResult.class); } - public HashMap executeAsyncWait( + public TransactionAsyncResult executeAsyncWait( String database, String engine, String source, boolean readonly) throws HttpError, IOException, InterruptedException { return executeAsyncWait(database, engine, source, readonly, new HashMap<>()); } - public HashMap executeAsyncWait( + public TransactionAsyncResult executeAsyncWait( String database, String engine, String source, boolean readonly, Map inputs) throws HttpError, IOException, InterruptedException { - String transactionId = null; - var output = new HashMap(); - var rsp = executeAsync(database, engine, source, readonly, inputs); - - try { - transactionId = rsp.asMap().get("id").toString(); - } catch (ClassCastException e) { - transactionId = rsp.get(0).asMap().get("id").toString(); - } + var id = executeAsync(database, engine, source, readonly, inputs).transaction.id; - var state = getTransaction(transactionId) - .asMap() - .get("transaction") - .asMap() - .get("state") - .toString(); + var transaction = getTransaction(id).transaction; - while (!"COMPLETED".equals(state)){ + while ( !("COMPLETED".equals(transaction.state) || "ABORTED".equals(transaction.state)) ) { Thread.sleep(2000); - - state = getTransaction(transactionId) - .asMap() - .get("transaction") - .asMap() - .get("state") - .toString(); + transaction = getTransaction(id).transaction; } - output.put("results", getTransactionResults(transactionId)); - output.put("metadata", getTransactionMetadata(transactionId)); - output.put("problems", getTransactionProblems(transactionId)); + var results = getTransactionResults(id); + var metadata = getTransactionMetadata(id); + var problems = getTransactionProblems(id); - return output; + return new TransactionAsyncResult( + transaction, + results, + metadata, + problems + ); } - public Any executeAsync( + public TransactionAsyncResult executeAsync( String database, String engine, String source, boolean readonly) throws HttpError, IOException, InterruptedException { return executeAsync(database, engine, source, readonly, new HashMap<>()); } - public Any executeAsync( + public TransactionAsyncResult executeAsync( String database, String engine, String source, boolean readonly, Map inputs) throws HttpError, IOException, InterruptedException { var tx = new TransactionAsync(database, engine, source, readonly, inputs); var body = tx.payload(); var rsp = post(PATH_TRANSACTIONS, tx.queryParams(), body); - return Json.deserialize(rsp); + if (rsp instanceof String) { + var txn = Json.deserialize((String) rsp, TransactionAsyncCompactResponse.class); + return new TransactionAsyncResult(txn, new ArrayList(), new ArrayList(), new ArrayList()); + } + return readTransactionAsyncResults((List) rsp); + } + + private TransactionAsyncResult readTransactionAsyncResults(List files) throws HttpError, IOException { + var transaction = files + .stream(). + filter(f -> f.name.equals("transaction")) + .collect(Collectors.toList()); + var metadata = files + .stream() + .filter(f -> f.name.equals("metadata")) + .collect(Collectors.toList()); + var problems = files + .stream() + .filter(f -> f.name.equals("problems")) + .collect(Collectors.toList()); + + if (transaction.isEmpty()) { + throw new HttpError(404, "transaction part is missing"); + } + var transactionResponse = Json.deserialize(new String(transaction.get(0).data, StandardCharsets.UTF_8), TransactionAsyncCompactResponse.class); + + if (metadata.isEmpty()) { + throw new HttpError(404, "metadata part is missing"); + } + var metadataResponse = Json.deserialize(new String(metadata.get(0).data, StandardCharsets.UTF_8), TransactionAsyncMetadataResponse[].class); + + if (problems.isEmpty()) { + throw new HttpError(404, "problems part is missing"); + } + var problemsResult = parseProblemsResult(new String(problems.get(0).data, StandardCharsets.UTF_8)); + + var results = readArrowFiles(files); + return new TransactionAsyncResult( + transactionResponse, + results, + Arrays.asList(metadataResponse), + problemsResult + ); } - public Any getTransaction(String id) throws HttpError, IOException, InterruptedException { + public TransactionAsyncSingleResponse getTransaction(String id) throws HttpError, IOException, InterruptedException { var rsp = get(String.format("%s/%s", PATH_TRANSACTIONS, id)); - return Json.deserialize(rsp); + return Json.deserialize((String) rsp, TransactionAsyncSingleResponse.class); } - public Any getTransactions() throws HttpError, IOException, InterruptedException { + public TransactionsAsyncMultipleResponses getTransactions() throws HttpError, IOException, InterruptedException { var rsp = get(PATH_TRANSACTIONS); - return Json.deserialize(rsp); + return Json.deserialize((String) rsp,TransactionsAsyncMultipleResponses.class); } - public Any getTransactionResults(String id) throws HttpError, IOException, InterruptedException { - var rsp = get(String.format("%s/%s/results", PATH_TRANSACTIONS, id)); - return Json.deserialize(rsp); + public List getTransactionResults(String id) throws HttpError, IOException, InterruptedException { + var rsp = (List) get(String.format("%s/%s/results", PATH_TRANSACTIONS, id)); + return readArrowFiles(rsp); } - public Any getTransactionMetadata(String id) throws HttpError, IOException, InterruptedException { + public List getTransactionMetadata(String id) throws HttpError, IOException, InterruptedException { var rsp = get(String.format("%s/%s/metadata", PATH_TRANSACTIONS, id)); - return Json.deserialize(rsp); + var results = Json.deserialize((String) rsp, TransactionAsyncMetadataResponse[].class); + return Arrays.asList(results); } - public Any getTransactionProblems(String id) throws HttpError, IOException, InterruptedException { - var rsp = get(String.format("%s/%s/problems", PATH_TRANSACTIONS, id)); - return Json.deserialize(rsp); + public List getTransactionProblems(String id) throws HttpError, IOException, InterruptedException { + var rsp = (String) get(String.format("%s/%s/problems", PATH_TRANSACTIONS, id)); + return parseProblemsResult(rsp); } // EDBs @@ -824,7 +818,7 @@ public Edb[] listEdbs(String database, String engine) var action = DbAction.makeListEdbAction(); var body = tx.payload(action); var rsp = post(PATH_TRANSACTION, tx.queryParams(), body); - var actions = Json.deserialize(rsp, ListEdbsResponse.class).actions; + var actions = Json.deserialize((String) rsp, ListEdbsResponse.class).actions; if (actions.length == 0) return new Edb[] {}; return actions[0].result.rels; @@ -839,7 +833,7 @@ public TransactionResult deleteModel(String database, String engine, String name var action = DbAction.makeDeleteModelAction(name); var body = tx.payload(action); var rsp = post(PATH_TRANSACTION, tx.queryParams(), body); - return Json.deserialize(rsp, TransactionResult.class); + return Json.deserialize((String) rsp, TransactionResult.class); } // Delete the list of named models. @@ -849,7 +843,7 @@ public TransactionResult deleteModel(String database, String engine, String[] na var actions = DbAction.makeDeleteModelsAction(names); var body = tx.payload(actions); var rsp = post(PATH_TRANSACTION, tx.queryParams(), body); - return Json.deserialize(rsp, TransactionResult.class); + return Json.deserialize((String) rsp, TransactionResult.class); } // Return the named model. @@ -878,7 +872,7 @@ public TransactionResult loadModel( var action = DbAction.makeInstallAction(name, model); var data = tx.payload(action); var rsp = post(PATH_TRANSACTION, tx.queryParams(), data); - return Json.deserialize(rsp, TransactionResult.class); + return Json.deserialize((String) rsp, TransactionResult.class); } // Load multiple models into the given database. @@ -889,7 +883,7 @@ public TransactionResult loadModels( var actions = DbAction.makeInstallAction(models); var data = tx.payload(actions); var rsp = post(PATH_TRANSACTION, tx.queryParams(), data); - return Json.deserialize(rsp, TransactionResult.class); + return Json.deserialize((String) rsp, TransactionResult.class); } // Returns the list of names of models installed in the given database. @@ -909,7 +903,7 @@ public Model[] listModels(String database, String engine) var tx = new Transaction(this.region, database, engine, "OPEN", true); var body = tx.payload(DbAction.makeListModelsAction()); var rsp = post(PATH_TRANSACTION, tx.queryParams(), body); - var actions = Json.deserialize(rsp, ListModelsResponse.class).actions; + var actions = Json.deserialize((String) rsp, ListModelsResponse.class).actions; if (actions.length == 0) return new Model[] {}; return actions[0].result.models; diff --git a/rai-sdk/src/main/java/com/relationalai/IntegrityConstraintViolation.java b/rai-sdk/src/main/java/com/relationalai/IntegrityConstraintViolation.java new file mode 100644 index 00000000..e3fa7bbf --- /dev/null +++ b/rai-sdk/src/main/java/com/relationalai/IntegrityConstraintViolation.java @@ -0,0 +1,29 @@ +/* + * Copyright 2022 RelationalAI, Inc. + * + * 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 com.relationalai; + +import com.jsoniter.annotation.JsonProperty; + +import java.util.List; + +public class IntegrityConstraintViolation extends Entity { + @JsonProperty(value = "type", required = true) + public String type; + + @JsonProperty(value = "sources", required = true) + public List sources; +} diff --git a/rai-sdk/src/main/java/com/relationalai/MultipartReader.java b/rai-sdk/src/main/java/com/relationalai/MultipartReader.java new file mode 100644 index 00000000..7f79a0d8 --- /dev/null +++ b/rai-sdk/src/main/java/com/relationalai/MultipartReader.java @@ -0,0 +1,178 @@ +package com.relationalai; + +import org.apache.commons.fileupload.MultipartStream; +import org.apache.commons.fileupload.ParameterParser; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.apache.commons.fileupload.FileUploadBase.ATTACHMENT; +import static org.apache.commons.fileupload.FileUploadBase.FORM_DATA; + +public class MultipartReader { + + public static List parseMultipartResponse(HttpResponse response) throws HttpError, IOException { + var output = new ArrayList(); + String contentType = response.headers().firstValue("Content-type").orElse(""); + if ("".equals(contentType)) { + throw new HttpError(404, "missing Content-type header"); + } + + MultipartStream multipartStream = new MultipartStream( + new ByteArrayInputStream(response.body()), + getBoundary(contentType), + 1024, + null + ); + + boolean nextPart = multipartStream.skipPreamble(); + + while (nextPart) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + String headersString = multipartStream.readHeaders(); + var headers = getHeadersMap(headersString); + + String name = getFieldName(headers.get("Content-Disposition")); + String filename = getFileName(headers.get("Content-Disposition")); + String fileContentType = headers.get("Content-Type"); + multipartStream.readBodyData(out); + + output.add(new TransactionAsyncFile(name, out.toByteArray(), filename, fileContentType)); + + nextPart = multipartStream.readBoundary(); + } + return output; + } + + protected static byte[] getBoundary(String contentType) { + ParameterParser parser = new ParameterParser(); + parser.setLowerCaseNames(true); + // Parameter parser can handle null input + Map params = parser.parse(contentType, new char[] {';', ','}); + String boundaryStr = (String) params.get("boundary"); + + if (boundaryStr == null) { + return null; + } + + return boundaryStr.getBytes(StandardCharsets.UTF_8); + } + private static int parseEndOfLine(String headerPart, int end) { + int index = end; + for (;;) { + int offset = headerPart.indexOf('\r', index); + if (offset == -1 || offset + 1 >= headerPart.length()) { + throw new IllegalStateException("Expected headers to be terminated by an empty line."); + } + if (headerPart.charAt(offset + 1) == '\n') { + return offset; + } + index = offset + 1; + } + } + protected static Map getHeadersMap(String headerPart) { + final int len = headerPart.length(); + final Map headers = new HashMap(); + + int start = 0; + for (;;) { + int end = parseEndOfLine(headerPart, start); + if (start == end) { + break; + } + String header = headerPart.substring(start, end); + start = end + 2; + while (start < len) { + int nonWs = start; + while (nonWs < len) { + char c = headerPart.charAt(nonWs); + if (c != ' ' && c != '\t') { + break; + } + ++nonWs; + } + if (nonWs == start) { + break; + } + // continuation line found + end = parseEndOfLine(headerPart, nonWs); + header += " " + headerPart.substring(nonWs, end); + start = end + 2; + } + + // parse header line + final int colonOffset = header.indexOf(':'); + if (colonOffset == -1) { + // this header line is malformed, skip it. + continue; + } + String headerName = header.substring(0, colonOffset).trim(); + String headerValue = header.substring(header.indexOf(':') + 1).trim(); + + if (headers.containsKey(headerName)) { + headers.put( headerName, headers.get(headerName) + "," + headerValue ); + } else { + headers.put(headerName, headerValue); + } + } + + return headers; + } + private static String getFieldName(String contentDisposition) { + String fieldName = null; + + if (contentDisposition != null && contentDisposition.toLowerCase().startsWith(FORM_DATA)) { + + ParameterParser parser = new ParameterParser(); + parser.setLowerCaseNames(true); + + // parameter parser can handle null input + Map params = parser.parse(contentDisposition, ';'); + fieldName = (String) params.get("name"); + if (fieldName != null) { + fieldName = fieldName.trim(); + } + } + + return fieldName; + } + + private static String getFileName(String contentDisposition) { + String fileName = null; + + if (contentDisposition != null) { + String cdl = contentDisposition.toLowerCase(); + + if (cdl.startsWith(FORM_DATA) || cdl.startsWith(ATTACHMENT)) { + + ParameterParser parser = new ParameterParser(); + parser.setLowerCaseNames(true); + + // parameter parser can handle null input + Map params = parser.parse(contentDisposition, ';'); + if (params.containsKey("filename")) { + fileName = (String) params.get("filename"); + if (fileName != null) { + fileName = fileName.trim(); + } else { + // even if there is no value, the parameter is present, + // so we return an empty file name rather than no file + // name. + fileName = ""; + } + } + } + } + + return fileName; + } +} diff --git a/rai-sdk/src/main/java/com/relationalai/Source.java b/rai-sdk/src/main/java/com/relationalai/Source.java new file mode 100644 index 00000000..c88d0af3 --- /dev/null +++ b/rai-sdk/src/main/java/com/relationalai/Source.java @@ -0,0 +1,30 @@ +/* + * Copyright 2022 RelationalAI, Inc. + * + * 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 com.relationalai; + +import com.jsoniter.annotation.JsonProperty; + +public class Source extends Entity { + @JsonProperty(value = "rel_key", required = true) + public RelKey relKey; + + @JsonProperty(value = "source", required = true) + public String src; + + @JsonProperty(value = "type", required = true) + public String type; +} diff --git a/rai-sdk/src/main/java/com/relationalai/TransactionAsyncCompactResponse.java b/rai-sdk/src/main/java/com/relationalai/TransactionAsyncCompactResponse.java new file mode 100644 index 00000000..572de469 --- /dev/null +++ b/rai-sdk/src/main/java/com/relationalai/TransactionAsyncCompactResponse.java @@ -0,0 +1,17 @@ +package com.relationalai; + +import com.jsoniter.annotation.JsonProperty; + +public class TransactionAsyncCompactResponse extends Entity { + @JsonProperty(value = "id", required = true) + public String id; + + @JsonProperty(value = "state", required = true) + public String state; + + public TransactionAsyncCompactResponse() {} + public TransactionAsyncCompactResponse(String id, String state) { + this.id = id; + this.state = state; + } +} diff --git a/rai-sdk/src/main/java/com/relationalai/TransactionAsyncFile.java b/rai-sdk/src/main/java/com/relationalai/TransactionAsyncFile.java new file mode 100644 index 00000000..11411230 --- /dev/null +++ b/rai-sdk/src/main/java/com/relationalai/TransactionAsyncFile.java @@ -0,0 +1,47 @@ +package com.relationalai; + +public class TransactionAsyncFile extends Entity { + String name; + byte[] data; + String filename; + String contentType; + + public TransactionAsyncFile(String name, byte[] data, String filename, String contentType) { + this.name = name; + this.data = data; + this.filename = filename; + this.contentType = contentType; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public byte[] getData() { + return data; + } + + public void setData(byte[] data) { + this.data = data; + } + + public String getFilename() { + return filename; + } + + public void setFilename(String filename) { + this.filename = filename; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } +} diff --git a/rai-sdk/src/main/java/com/relationalai/TransactionAsyncMetadataResponse.java b/rai-sdk/src/main/java/com/relationalai/TransactionAsyncMetadataResponse.java new file mode 100644 index 00000000..42206db8 --- /dev/null +++ b/rai-sdk/src/main/java/com/relationalai/TransactionAsyncMetadataResponse.java @@ -0,0 +1,33 @@ +package com.relationalai; + +import com.jsoniter.annotation.JsonProperty; + +import java.util.List; + +public class TransactionAsyncMetadataResponse extends Entity { + @JsonProperty(value = "relationId", required = true) + public String relationId; + + @JsonProperty(value = "types", required = true) + public List types; + + public TransactionAsyncMetadataResponse() {} + public TransactionAsyncMetadataResponse(String relationId, List types) { + this.relationId = relationId; + this.types = types; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof TransactionAsyncMetadataResponse)) { + return false; + } + var that = (TransactionAsyncMetadataResponse) o; + + return this.relationId.equals(that.relationId) && this.types.equals(that.types); + } +} diff --git a/rai-sdk/src/main/java/com/relationalai/TransactionAsyncResponse.java b/rai-sdk/src/main/java/com/relationalai/TransactionAsyncResponse.java new file mode 100644 index 00000000..c3ca7c2a --- /dev/null +++ b/rai-sdk/src/main/java/com/relationalai/TransactionAsyncResponse.java @@ -0,0 +1,48 @@ +package com.relationalai; + +import com.jsoniter.annotation.JsonProperty; + +public class TransactionAsyncResponse extends TransactionAsyncCompactResponse { + @JsonProperty(value = "account_name", required = true) + public String accountName; + + @JsonProperty(value = "created_by", required = true) + public String createdBy; + + @JsonProperty(value = "created_on", required = true) + public String createdOn; + + @JsonProperty(value = "database_name", required = true) + public String databaseName; + + @JsonProperty(value = "read_only", required = true) + public boolean readOnly; + + @JsonProperty(value = "query", required = true) + public String query; + + @JsonProperty(value = "last_requested_interval", required = true) + public String lastRequestedInterval; + + public TransactionAsyncResponse() {} + public TransactionAsyncResponse( + String id, + String state, + String accountName, + String createdBy, + String createdOn, + String databaseName, + boolean readOnly, + String query, + String lastRequestedInterval + ) { + super(id, state); + this.accountName = accountName; + this.createdBy = createdBy; + this.createdOn = createdOn; + this.databaseName = databaseName; + this.readOnly = readOnly; + this.query = query; + this.lastRequestedInterval = lastRequestedInterval; + } +} diff --git a/rai-sdk/src/main/java/com/relationalai/TransactionAsyncResult.java b/rai-sdk/src/main/java/com/relationalai/TransactionAsyncResult.java new file mode 100644 index 00000000..b6197623 --- /dev/null +++ b/rai-sdk/src/main/java/com/relationalai/TransactionAsyncResult.java @@ -0,0 +1,18 @@ +package com.relationalai; + +import java.util.List; + +public class TransactionAsyncResult extends Entity { + + public TransactionAsyncCompactResponse transaction; + public List results; + public List metadata; + public List problems; + + public TransactionAsyncResult(TransactionAsyncCompactResponse transaction, List results, List metadata, List problems) { + this.transaction = transaction; + this.results = results; + this.metadata = metadata; + this.problems = problems; + } +} diff --git a/rai-sdk/src/main/java/com/relationalai/TransactionAsyncSingleResponse.java b/rai-sdk/src/main/java/com/relationalai/TransactionAsyncSingleResponse.java new file mode 100644 index 00000000..21c198ad --- /dev/null +++ b/rai-sdk/src/main/java/com/relationalai/TransactionAsyncSingleResponse.java @@ -0,0 +1,8 @@ +package com.relationalai; + +import com.jsoniter.annotation.JsonProperty; + +public class TransactionAsyncSingleResponse extends Entity { + @JsonProperty(value = "transaction", required = true) + public TransactionAsyncResponse transaction; +} diff --git a/rai-sdk/src/main/java/com/relationalai/TransactionsAsyncMultipleResponses.java b/rai-sdk/src/main/java/com/relationalai/TransactionsAsyncMultipleResponses.java new file mode 100644 index 00000000..6e2404ca --- /dev/null +++ b/rai-sdk/src/main/java/com/relationalai/TransactionsAsyncMultipleResponses.java @@ -0,0 +1,10 @@ +package com.relationalai; + +import com.jsoniter.annotation.JsonProperty; + +import java.util.List; + +public class TransactionsAsyncMultipleResponses extends Entity { + @JsonProperty(value = "transactions", required = true) + public List transactions; +} diff --git a/rai-sdk/src/test/java/com/relationalai/ExecuteAsyncTest.java b/rai-sdk/src/test/java/com/relationalai/ExecuteAsyncTest.java index 1d59e88b..563ddeef 100644 --- a/rai-sdk/src/test/java/com/relationalai/ExecuteAsyncTest.java +++ b/rai-sdk/src/test/java/com/relationalai/ExecuteAsyncTest.java @@ -16,12 +16,13 @@ package com.relationalai; -import com.jsoniter.JsonIterator; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import static org.junit.jupiter.api.Assertions.*; @@ -37,17 +38,31 @@ void testExecuteAsync() throws HttpError, InterruptedException, IOException { var rsp = client.executeAsyncWait(databaseName, engineName, query, true); - assertEquals( - rsp.get("results"), - JsonIterator.deserialize("[[{\"v1\":[1,2,3,4,5]},{\"v2\":[1,4,9,16,25]},{\"v3\":[1,8,27,64,125]},{\"v4\":[1,16,81,256,625]}]]")); - assertEquals( - rsp.get("metadata"), - JsonIterator.deserialize("[{\"relationId\":\"/:output/Int64/Int64/Int64/Int64\",\"types\":[\":output\",\"Int64\",\"Int64\",\"Int64\",\"Int64\"]}]") - ); - assertEquals( - rsp.get("problems"), - JsonIterator.deserialize("[]") - ); + var results = new ArrayList () { + { + add(new ArrowRelation("v1", Arrays.asList(new Object[] {1L, 2L, 3L, 4L, 5L}) )); + add(new ArrowRelation("v2", Arrays.asList(new Object[] {1L, 4L, 9L, 16L, 25L}) )); + add(new ArrowRelation("v3", Arrays.asList(new Object[] {1L, 8L, 27L, 64L, 125L}) )); + add(new ArrowRelation("v4", Arrays.asList(new Object[] {1L, 16L, 81L, 256L, 625L}) )); + } + }; + + var metadata = new ArrayList() { + { + add( + new TransactionAsyncMetadataResponse( + "/:output/Int64/Int64/Int64/Int64", + Arrays.asList(new String[] {":output", "Int64", "Int64", "Int64", "Int64"}) + ) + ); + } + }; + + var problems = new ArrayList(); + + assertEquals(rsp.results, results); + assertEquals(rsp.metadata, metadata); + assertEquals(rsp.problems, problems); } @AfterAll diff --git a/rai-sdk/src/test/java/com/relationalai/UserTest.java b/rai-sdk/src/test/java/com/relationalai/UserTest.java index 1c36a475..0c7c1fd3 100644 --- a/rai-sdk/src/test/java/com/relationalai/UserTest.java +++ b/rai-sdk/src/test/java/com/relationalai/UserTest.java @@ -31,9 +31,13 @@ @TestInstance(Lifecycle.PER_CLASS) public class UserTest extends UnitTest { static UUID uuid = UUID.randomUUID(); - static String userEmail = String.format("java-sdk-test-%s@relational.ai", uuid); + static String userEmail = String.format("java-sdk-%s@example.com", uuid); - @Test void testUser() throws HttpError, InterruptedException, IOException { + // Created users are deleted from the account + // but not deleted from the database. + // We should enable back this test when + // the behavior is addressed (delete also users from the database). + void testUser() throws HttpError, InterruptedException, IOException { var client = createClient(); var rsp = client.findUser(userEmail); From 5446003fc32d37826af9733a6deacda42bc755d4 Mon Sep 17 00:00:00 2001 From: "helmi.nour" Date: Wed, 27 Apr 2022 11:09:17 +0100 Subject: [PATCH 2/5] changelog update --- CHANGELOG.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b15dc1b..9729b654 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ ## main +* Added v2 predefined results formats: + + - `getTransactions` returns `TransactionsAsyncMultipleResponses`. + - `getTransaction` returns `TransactionAsyncSingleResponse`. + - `getTransactionResults` returns `List`. + - `getTransactionMetadata` returns `List`. + - `getTransactionProblems` returns `List`. + - `executeAsync` returns `TransactionAsyncResult`. +## v0.1.0-alpha * Added support to the asynchronous protocol including: - `executeAsync`: runs an asynchronous request. - `executeAsyncWait`: runs an asynchronous request and wait of its completion. @@ -9,6 +18,3 @@ - `getTransactionMetadata`: gets transaction metadata. - `getTransactionProblems`: gets transaction execution problems. * Added `ExecuteAsyncTest` test class - -## v0.0.1 -* initial release \ No newline at end of file From 5463ef2e2f7a7a0e4171bb56720816e4b21b3707 Mon Sep 17 00:00:00 2001 From: "helmi.nour" Date: Wed, 27 Apr 2022 11:10:49 +0100 Subject: [PATCH 3/5] version bumpup --- README.md | 2 +- pom.xml | 2 +- rai-sdk-examples/pom.xml | 2 +- rai-sdk/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4972fbbe..9130bdab 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ In order to use the `rai-sdk-java`, you need add this dependency to your project com.relationalai rai-sdk - 0.1.0-alpha + 0.2.0-alpha You need also to point maven to the SDK GitHub packages repository in the project's POM: diff --git a/pom.xml b/pom.xml index 36d51212..b55ca602 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ The RelationalAI Software Development Kit (SDK) for Java com.relationalai rai-sdk-pom - 0.1.0-alpha + 0.2.0-alpha pom diff --git a/rai-sdk-examples/pom.xml b/rai-sdk-examples/pom.xml index e085bee4..3107ec4e 100644 --- a/rai-sdk-examples/pom.xml +++ b/rai-sdk-examples/pom.xml @@ -20,7 +20,7 @@ com.relationalai rai-sdk-pom - 0.1.0-alpha + 0.2.0-alpha RelationalAI SDK for Java Examples diff --git a/rai-sdk/pom.xml b/rai-sdk/pom.xml index 9f2f6bd1..928a69dd 100644 --- a/rai-sdk/pom.xml +++ b/rai-sdk/pom.xml @@ -20,7 +20,7 @@ com.relationalai rai-sdk-pom - 0.1.0-alpha + 0.2.0-alpha RelationalAI SDK for Java Package From b35640762a388205595349d9e496a780cbb0c736 Mon Sep 17 00:00:00 2001 From: "helmi.nour" Date: Wed, 27 Apr 2022 11:20:11 +0100 Subject: [PATCH 4/5] update --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9729b654..2c73079b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,3 @@ - ## main * Added v2 predefined results formats: @@ -8,6 +7,7 @@ - `getTransactionMetadata` returns `List`. - `getTransactionProblems` returns `List`. - `executeAsync` returns `TransactionAsyncResult`. + ## v0.1.0-alpha * Added support to the asynchronous protocol including: - `executeAsync`: runs an asynchronous request. From 676bcd323e5286a741ce29eb7558a346fc1aa7bd Mon Sep 17 00:00:00 2001 From: "helmi.nour" Date: Wed, 27 Apr 2022 11:29:18 +0100 Subject: [PATCH 5/5] cleanup --- rai-sdk/src/main/java/com/relationalai/Client.java | 3 +-- .../com/relationalai/{Problem.java => ClientProblem.java} | 4 ++-- rai-sdk/src/main/java/com/relationalai/MultipartReader.java | 1 - rai-sdk/src/main/java/com/relationalai/TransactionResult.java | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) rename rai-sdk/src/main/java/com/relationalai/{Problem.java => ClientProblem.java} (94%) diff --git a/rai-sdk/src/main/java/com/relationalai/Client.java b/rai-sdk/src/main/java/com/relationalai/Client.java index 9cd6c309..ab1d1f34 100644 --- a/rai-sdk/src/main/java/com/relationalai/Client.java +++ b/rai-sdk/src/main/java/com/relationalai/Client.java @@ -16,7 +16,6 @@ package com.relationalai; -import com.jsoniter.any.Any; import com.jsoniter.spi.JsonException; import org.apache.arrow.memory.RootAllocator; import org.apache.arrow.vector.FieldVector; @@ -318,7 +317,7 @@ private List parseProblemsResult(String rsp) { try { output.add(Json.deserialize(data, IntegrityConstraintViolation.class)); } catch (JsonException e) { - output.add(Json.deserialize(data, Problem.class)); + output.add(Json.deserialize(data, ClientProblem.class)); } } return output; diff --git a/rai-sdk/src/main/java/com/relationalai/Problem.java b/rai-sdk/src/main/java/com/relationalai/ClientProblem.java similarity index 94% rename from rai-sdk/src/main/java/com/relationalai/Problem.java rename to rai-sdk/src/main/java/com/relationalai/ClientProblem.java index 2b34cd01..04eecedf 100644 --- a/rai-sdk/src/main/java/com/relationalai/Problem.java +++ b/rai-sdk/src/main/java/com/relationalai/ClientProblem.java @@ -18,7 +18,7 @@ import com.jsoniter.annotation.JsonProperty; -public class Problem extends Entity { +public class ClientProblem extends Entity { @JsonProperty(value = "type", required = true) public String type; @@ -37,5 +37,5 @@ public class Problem extends Entity { @JsonProperty(value = "report", required = true) public String report; - public Problem() {} + public ClientProblem() {} } diff --git a/rai-sdk/src/main/java/com/relationalai/MultipartReader.java b/rai-sdk/src/main/java/com/relationalai/MultipartReader.java index 7f79a0d8..12ecc6ec 100644 --- a/rai-sdk/src/main/java/com/relationalai/MultipartReader.java +++ b/rai-sdk/src/main/java/com/relationalai/MultipartReader.java @@ -6,7 +6,6 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.util.ArrayList; diff --git a/rai-sdk/src/main/java/com/relationalai/TransactionResult.java b/rai-sdk/src/main/java/com/relationalai/TransactionResult.java index f5e09f79..9ba10660 100644 --- a/rai-sdk/src/main/java/com/relationalai/TransactionResult.java +++ b/rai-sdk/src/main/java/com/relationalai/TransactionResult.java @@ -26,7 +26,7 @@ public class TransactionResult extends Entity { public Relation[] output; @JsonProperty(value = "problems", required = true) - public Problem[] problems; + public ClientProblem[] problems; public TransactionResult() {} }