From 9d89c089b3e3ed72e9f7b89ca85e1bc38f118890 Mon Sep 17 00:00:00 2001 From: Jeronimo Irazabal Date: Mon, 14 Nov 2022 15:04:59 -0300 Subject: [PATCH] sql: initial sql support Signed-off-by: Jeronimo Irazabal --- .../io/codenotary/immudb4j/ImmuClient.java | 125 +++++++++++++++- .../ImmudbAuthRequestInterceptor.java | 29 +++- src/main/java/io/codenotary/immudb4j/KV.java | 15 ++ .../java/io/codenotary/immudb4j/KVPair.java | 15 ++ .../java/io/codenotary/immudb4j/Session.java | 9 ++ .../immudb4j/crypto/InclusionProof.java | 1 - .../codenotary/immudb4j/sql/SQLException.java | 25 ++++ .../immudb4j/sql/SQLQueryResult.java | 136 ++++++++++++++++++ .../io/codenotary/immudb4j/sql/SQLValue.java | 105 ++++++++++++++ .../immudb4j/SQLTransactionsTest.java | 79 ++++++++++ 10 files changed, 535 insertions(+), 4 deletions(-) create mode 100644 src/main/java/io/codenotary/immudb4j/sql/SQLException.java create mode 100644 src/main/java/io/codenotary/immudb4j/sql/SQLQueryResult.java create mode 100644 src/main/java/io/codenotary/immudb4j/sql/SQLValue.java create mode 100644 src/test/java/io/codenotary/immudb4j/SQLTransactionsTest.java diff --git a/src/main/java/io/codenotary/immudb4j/ImmuClient.java b/src/main/java/io/codenotary/immudb4j/ImmuClient.java index ca00ac3..cede548 100644 --- a/src/main/java/io/codenotary/immudb4j/ImmuClient.java +++ b/src/main/java/io/codenotary/immudb4j/ImmuClient.java @@ -20,13 +20,19 @@ import io.codenotary.immudb.ImmuServiceGrpc; import io.codenotary.immudb.ImmudbProto; import io.codenotary.immudb.ImmudbProto.Chunk; +import io.codenotary.immudb.ImmudbProto.NamedParam; +import io.codenotary.immudb.ImmudbProto.NewTxResponse; import io.codenotary.immudb.ImmudbProto.ScanRequest; import io.codenotary.immudb.ImmudbProto.Score; +import io.codenotary.immudb.ImmudbProto.TxMode; import io.codenotary.immudb4j.basics.LatchHolder; import io.codenotary.immudb4j.crypto.CryptoUtils; import io.codenotary.immudb4j.crypto.DualProof; import io.codenotary.immudb4j.crypto.InclusionProof; import io.codenotary.immudb4j.exceptions.*; +import io.codenotary.immudb4j.sql.SQLException; +import io.codenotary.immudb4j.sql.SQLQueryResult; +import io.codenotary.immudb4j.sql.SQLValue; import io.codenotary.immudb4j.user.Permission; import io.codenotary.immudb4j.user.User; import io.grpc.ManagedChannel; @@ -42,9 +48,10 @@ import java.security.PublicKey; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.Iterator; import java.util.List; -import java.util.NoSuchElementException; +import java.util.Map; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.TimeUnit; @@ -205,6 +212,122 @@ public synchronized ImmuState currentState() throws VerificationException { return immuState; } + // + // ========== SQL ========== + // + + public synchronized void beginTransaction() throws SQLException { + if (session == null) { + throw new IllegalStateException("no open session"); + } + + if (session.getTransactionID() != null) { + throw new IllegalStateException("transaction already initiated"); + } + + final ImmudbProto.NewTxRequest req = ImmudbProto.NewTxRequest.newBuilder() + .setMode(TxMode.ReadWrite) + .build(); + + final NewTxResponse res = blockingStub.newTx(req); + + session.setTransactionID(res.getTransactionID()); + } + + public synchronized void commitTransaction() throws SQLException { + if (session == null) { + throw new IllegalStateException("no open session"); + } + + if (session.getTransactionID() == null) { + throw new IllegalStateException("no transaction initiated"); + } + + blockingStub.commit(Empty.getDefaultInstance()); + + session.setTransactionID( null); + } + + public synchronized void rollbackTransaction() throws SQLException { + if (session == null) { + throw new IllegalStateException("no open session"); + } + + if (session.getTransactionID() == null) { + throw new IllegalStateException("no transaction initiated"); + } + + blockingStub.rollback(Empty.getDefaultInstance()); + + session.setTransactionID(null); + } + + public void sqlExec(String stmt, SQLValue... params) throws SQLException { + sqlExec(stmt, sqlNameParams(params)); + } + + public synchronized void sqlExec(String stmt, Map params) throws SQLException { + if (session == null) { + throw new IllegalStateException("no open session"); + } + + if (session.getTransactionID() == null) { + throw new IllegalStateException("no transaction initiated"); + } + + final ImmudbProto.SQLExecRequest req = ImmudbProto.SQLExecRequest.newBuilder() + .setSql(stmt) + .addAllParams(sqlEncodeParams(params)) + .build(); + + blockingStub.txSQLExec(req); + } + + public SQLQueryResult sqlQuery(String stmt, SQLValue... params) throws SQLException { + return sqlQuery(stmt, sqlNameParams(params)); + } + + public synchronized SQLQueryResult sqlQuery(String stmt, Map params) throws SQLException { + if (session == null) { + throw new IllegalStateException("no open session"); + } + + if (session.getTransactionID() == null) { + throw new IllegalStateException("no transaction initiated"); + } + + final ImmudbProto.SQLQueryRequest req = ImmudbProto.SQLQueryRequest.newBuilder() + .setSql(stmt) + .addAllParams(sqlEncodeParams(params)) + .build(); + + return new SQLQueryResult(blockingStub.txSQLQuery(req)); + } + + private Map sqlNameParams(SQLValue... params) { + final Map nparams = new HashMap<>(params.length); + + for (int i = 1; i <= params.length; i++) { + nparams.put("param" + i, params[i-1]); + } + + return nparams; + } + + private Iterable sqlEncodeParams(Map params) { + List nparams = new ArrayList<>(); + + for (Map.Entry p : params.entrySet()) { + nparams.add(NamedParam.newBuilder() + .setName(p.getKey()) + .setValue(p.getValue().asProtoSQLValue()) + .build() + ); + } + + return nparams; + } + // // ========== DATABASE ========== // diff --git a/src/main/java/io/codenotary/immudb4j/ImmudbAuthRequestInterceptor.java b/src/main/java/io/codenotary/immudb4j/ImmudbAuthRequestInterceptor.java index 8b4b430..cfb777b 100644 --- a/src/main/java/io/codenotary/immudb4j/ImmudbAuthRequestInterceptor.java +++ b/src/main/java/io/codenotary/immudb4j/ImmudbAuthRequestInterceptor.java @@ -1,11 +1,32 @@ -package io.codenotary.immudb4j; +/* +Copyright 2022 CodeNotary, Inc. All rights reserved. + +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 -import io.grpc.*; + 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 io.codenotary.immudb4j; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; public class ImmudbAuthRequestInterceptor implements ClientInterceptor { private static final String SESSION_ID = "sessionid"; + private static final String TRANSACTION_ID = "transactionid"; private final ImmuClient client; @@ -26,6 +47,10 @@ public void start(Listener responseListener, Metadata headers) { if (session != null) { headers.put(Metadata.Key.of(SESSION_ID, Metadata.ASCII_STRING_MARSHALLER), session.getSessionID()); + + if (session.getTransactionID() != null) { + headers.put(Metadata.Key.of(TRANSACTION_ID, Metadata.ASCII_STRING_MARSHALLER), session.getTransactionID()); + } } super.start(responseListener, headers); diff --git a/src/main/java/io/codenotary/immudb4j/KV.java b/src/main/java/io/codenotary/immudb4j/KV.java index ae0591b..5a51c89 100644 --- a/src/main/java/io/codenotary/immudb4j/KV.java +++ b/src/main/java/io/codenotary/immudb4j/KV.java @@ -1,3 +1,18 @@ +/* +Copyright 2022 CodeNotary, Inc. All rights reserved. + +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 io.codenotary.immudb4j; import io.codenotary.immudb4j.crypto.CryptoUtils; diff --git a/src/main/java/io/codenotary/immudb4j/KVPair.java b/src/main/java/io/codenotary/immudb4j/KVPair.java index a0c0ada..e200cca 100644 --- a/src/main/java/io/codenotary/immudb4j/KVPair.java +++ b/src/main/java/io/codenotary/immudb4j/KVPair.java @@ -1,3 +1,18 @@ +/* +Copyright 2022 CodeNotary, Inc. All rights reserved. + +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 io.codenotary.immudb4j; /** diff --git a/src/main/java/io/codenotary/immudb4j/Session.java b/src/main/java/io/codenotary/immudb4j/Session.java index 5b4d130..b6826a5 100644 --- a/src/main/java/io/codenotary/immudb4j/Session.java +++ b/src/main/java/io/codenotary/immudb4j/Session.java @@ -20,6 +20,8 @@ public class Session { private String sessionID; private String database; + private String transactionID; + public Session(String sessionID, String database) { this.sessionID = sessionID; this.database = database; @@ -33,4 +35,11 @@ public String getDatabase() { return database; } + public String getTransactionID() { + return transactionID; + } + + public void setTransactionID(String transactionID) { + this.transactionID = transactionID; + } } diff --git a/src/main/java/io/codenotary/immudb4j/crypto/InclusionProof.java b/src/main/java/io/codenotary/immudb4j/crypto/InclusionProof.java index ecbff81..657fe54 100644 --- a/src/main/java/io/codenotary/immudb4j/crypto/InclusionProof.java +++ b/src/main/java/io/codenotary/immudb4j/crypto/InclusionProof.java @@ -16,7 +16,6 @@ package io.codenotary.immudb4j.crypto; import io.codenotary.immudb.ImmudbProto; -import io.codenotary.immudb4j.Utils; public class InclusionProof { diff --git a/src/main/java/io/codenotary/immudb4j/sql/SQLException.java b/src/main/java/io/codenotary/immudb4j/sql/SQLException.java new file mode 100644 index 0000000..987d924 --- /dev/null +++ b/src/main/java/io/codenotary/immudb4j/sql/SQLException.java @@ -0,0 +1,25 @@ + +/* +Copyright 2022 CodeNotary, Inc. All rights reserved. + +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 io.codenotary.immudb4j.sql; + +public class SQLException extends Exception { + + public SQLException(String message) { + super(message); + } + +} diff --git a/src/main/java/io/codenotary/immudb4j/sql/SQLQueryResult.java b/src/main/java/io/codenotary/immudb4j/sql/SQLQueryResult.java new file mode 100644 index 0000000..3348d11 --- /dev/null +++ b/src/main/java/io/codenotary/immudb4j/sql/SQLQueryResult.java @@ -0,0 +1,136 @@ + +/* +Copyright 2022 CodeNotary, Inc. All rights reserved. + +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 io.codenotary.immudb4j.sql; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import io.codenotary.immudb.ImmudbProto; + +public class SQLQueryResult { + + private ImmudbProto.SQLQueryResult res; + private int currRow = -1; + + private boolean closed; + + public SQLQueryResult(ImmudbProto.SQLQueryResult res) { + if (res == null) { + throw new RuntimeException("illegal arguments"); + } + + this.res = res; + } + + public synchronized void close() throws SQLException { + closed = true; + } + + public synchronized boolean next() throws SQLException { + if (closed) { + throw new SQLException("already closed"); + } + + if (currRow + 1 >= res.getRowsCount()) { + return false; + } + + currRow++; + + return true; + } + + private void validateReadingAt(int col) throws SQLException { + if (closed) { + throw new SQLException("already closed"); + } + + if (currRow < 0) { + throw new SQLException("no row was read"); + } + + if (res.getRowsCount() == currRow) { + throw new SQLException("no more rows"); + } + + if (res.getColumnsCount() < col) { + throw new SQLException("invalid column"); + } + } + + public synchronized int getColumnsCount() throws SQLException { + if (closed) { + throw new SQLException("already closed"); + } + + return res.getColumnsCount(); + } + + public synchronized String getColumnName(int i) throws SQLException { + if (closed) { + throw new SQLException("already closed"); + } + + final String fullColName = res.getColumns(i).getName(); + + return fullColName.substring(fullColName.lastIndexOf(".")+1, fullColName.length()-1); + } + + public synchronized String getColumnType(int i) throws SQLException { + if (closed) { + throw new SQLException("already closed"); + } + + return res.getColumns(i).getType(); + } + + public synchronized boolean getBoolean(int i) throws SQLException { + validateReadingAt(i); + + return res.getRows(currRow).getValues(i).getB(); + } + + public synchronized int getInt(int i) throws SQLException { + validateReadingAt(i); + + return (int)res.getRows(currRow).getValues(i).getN(); + } + + public synchronized long getLong(int i) throws SQLException { + validateReadingAt(i); + + return res.getRows(currRow).getValues(i).getN(); + } + + public synchronized String getString(int i) throws SQLException { + validateReadingAt(i); + + return res.getRows(currRow).getValues(i).getS(); + } + + public synchronized byte[] getBytes(int i) throws SQLException { + validateReadingAt(i); + + return res.getRows(currRow).getValues(i).getBs().toByteArray(); + } + + public synchronized Date getDate(int i) throws SQLException { + validateReadingAt(i); + + return new Date(TimeUnit.MICROSECONDS.toMillis(res.getRows(currRow).getValues(i).getTs())); + } +} diff --git a/src/main/java/io/codenotary/immudb4j/sql/SQLValue.java b/src/main/java/io/codenotary/immudb4j/sql/SQLValue.java new file mode 100644 index 0000000..d331db7 --- /dev/null +++ b/src/main/java/io/codenotary/immudb4j/sql/SQLValue.java @@ -0,0 +1,105 @@ + +/* +Copyright 2022 CodeNotary, Inc. All rights reserved. + +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 io.codenotary.immudb4j.sql; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import io.codenotary.immudb.ImmudbProto; +import io.codenotary.immudb4j.Utils; + +public class SQLValue { + + private ImmudbProto.SQLValue value; + + public SQLValue(Boolean value) { + final ImmudbProto.SQLValue.Builder builder = ImmudbProto.SQLValue.newBuilder(); + + if (value == null) { + builder.setNull(null); + } else { + builder.setB(value); + } + + this.value = builder.build(); + } + + public SQLValue(Integer value) { + final ImmudbProto.SQLValue.Builder builder = ImmudbProto.SQLValue.newBuilder(); + + if (value == null) { + builder.setNull(null); + } else { + builder.setN(value); + } + + this.value = builder.build(); + } + + public SQLValue(Long value) { + final ImmudbProto.SQLValue.Builder builder = ImmudbProto.SQLValue.newBuilder(); + + if (value == null) { + builder.setNull(null); + } else { + builder.setN(value); + } + + this.value = builder.build(); + } + + public SQLValue(String value) { + final ImmudbProto.SQLValue.Builder builder = ImmudbProto.SQLValue.newBuilder(); + + if (value == null) { + builder.setNull(null); + } else { + builder.setS(value); + } + + this.value = builder.build(); + } + + public SQLValue(Date value) { + final ImmudbProto.SQLValue.Builder builder = ImmudbProto.SQLValue.newBuilder(); + + if (value == null) { + builder.setNull(null); + } else { + builder.setTs(TimeUnit.MICROSECONDS.toMicros(value.getTime())); + } + + this.value = builder.build(); + } + + public SQLValue(byte[] value) { + final ImmudbProto.SQLValue.Builder builder = ImmudbProto.SQLValue.newBuilder(); + + if (value == null) { + builder.setNull(null); + } else { + builder.setBs(Utils.toByteString(value)); + } + + this.value = builder.build(); + } + + public ImmudbProto.SQLValue asProtoSQLValue() { + return value; + } + +} diff --git a/src/test/java/io/codenotary/immudb4j/SQLTransactionsTest.java b/src/test/java/io/codenotary/immudb4j/SQLTransactionsTest.java new file mode 100644 index 0000000..c5b4701 --- /dev/null +++ b/src/test/java/io/codenotary/immudb4j/SQLTransactionsTest.java @@ -0,0 +1,79 @@ +/* +Copyright 2022 CodeNotary, Inc. All rights reserved. + +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 io.codenotary.immudb4j; + +import io.codenotary.immudb4j.exceptions.CorruptedDataException; +import io.codenotary.immudb4j.exceptions.VerificationException; +import io.codenotary.immudb4j.sql.SQLException; +import io.codenotary.immudb4j.sql.SQLQueryResult; +import io.codenotary.immudb4j.sql.SQLValue; + +import org.testng.Assert; +import org.testng.annotations.Test; + +public class SQLTransactionsTest extends ImmuClientIntegrationTest { + + @Test(testName = "simple sql transaction") + public void t1() throws VerificationException, CorruptedDataException, InterruptedException, SQLException { + immuClient.openSession("defaultdb", "immudb", "immudb"); + + immuClient.beginTransaction(); + + immuClient.sqlExec( + "CREATE TABLE IF NOT EXISTS mytable(id INTEGER, title VARCHAR[256], active BOOLEAN, PRIMARY KEY id)"); + + final int rows = 10; + + for (int i = 0; i < rows; i++) { + immuClient.sqlExec("UPSERT INTO mytable(id, title, active) VALUES (?, ?, ?)", + new SQLValue(i), + new SQLValue(String.format("title%d", i)), + new SQLValue(i % 2 == 0)); + } + + SQLQueryResult res = immuClient.sqlQuery("SELECT id, title, active FROM mytable"); + + Assert.assertEquals(res.getColumnsCount(), 3); + + Assert.assertEquals(res.getColumnName(0), "id"); + Assert.assertEquals(res.getColumnType(0), "INTEGER"); + + Assert.assertEquals(res.getColumnName(1), "title"); + Assert.assertEquals(res.getColumnType(1), "VARCHAR"); + + Assert.assertEquals(res.getColumnName(2), "active"); + Assert.assertEquals(res.getColumnType(2), "BOOLEAN"); + + int i = 0; + + while (res.next()) { + Assert.assertEquals(i, res.getInt(0)); + Assert.assertEquals(String.format("title%d", i), res.getString(1)); + Assert.assertEquals(i % 2 == 0, res.getBoolean(2)); + + i++; + } + + Assert.assertEquals(rows, i); + + res.close(); + + immuClient.commitTransaction(); + + immuClient.closeSession(); + } + +}