diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 1f52101..539d701 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -20,6 +20,8 @@ jobs: uses: actions/setup-java@v1 with: java-version: 1.8 + - name: Start immudb container + run: docker run -d --health-cmd "immuadmin status" --health-interval 10s --health-timeout 5s --health-retries 5 -p 3322:3322 codenotary/immudb:1.4.0 - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle diff --git a/build.gradle b/build.gradle index b7d84c8..eae470a 100644 --- a/build.gradle +++ b/build.gradle @@ -120,32 +120,6 @@ dependencies { implementation 'javax.annotation:javax.annotation-api:1.2-b01' } -task immudbStart { - // INFO: You can disable this logic (comment these lines below within this task) - // if you want to use an existing immudb server, without starting and stopping - // the locally used immudb version (stored in `immudb` directory and controlled - // through the scripts). Just make sure immudb server has the proper version. - ProcessBuilder pb = new ProcessBuilder() - pb.command("/bin/bash", "immudb/clean.sh") - Process procClean = pb.start() - procClean.waitFor() - - pb = new ProcessBuilder() - pb.command("/bin/bash", "immudb/start.sh") - Process proc = pb.start() - proc.waitFor(15, TimeUnit.SECONDS) -} - -task immudbStop(type: Exec) { - // INFO: You can disable this logic (comment these lines below within this task) - // if you want to use an existing immudb server, without starting and stopping - // the locally used immudb version (stored in `immudb` directory and controlled - // through the scripts). Just make sure immudb server has the proper version. - workingDir 'immudb' - commandLine './clean.sh' -} - - idea { module { sourceDirs += file("${projectDir}/src/generated/main/java") @@ -166,9 +140,7 @@ test { } } -test.dependsOn immudbStart - -test.finalizedBy([jacocoTestReport, immudbStop]) +test.finalizedBy([jacocoTestReport]) task integrationTest(type: Test) { testClassesDirs = sourceSets.integrationTest.output.classesDirs @@ -259,4 +231,4 @@ publishing { signing { sign publishing.publications.gpr } -} +} \ No newline at end of file diff --git a/src/main/java/io/codenotary/immudb4j/ImmuClient.java b/src/main/java/io/codenotary/immudb4j/ImmuClient.java index ca00ac3..e435556 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 ========== // @@ -1103,12 +1226,12 @@ public synchronized List txScanAll(long initialTxId) { return buildList(txList); } - public synchronized List txScanAll(long initialTxId, int limit, boolean desc) { + public synchronized List txScanAll(long initialTxId, boolean desc, int limit) { final ImmudbProto.TxScanRequest req = ImmudbProto.TxScanRequest .newBuilder() .setInitialTx(initialTxId) - .setDesc(desc) .setLimit(limit) + .setDesc(desc) .build(); final ImmudbProto.TxList txList = blockingStub.txScan(req); 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(); + } + +} diff --git a/src/test/java/io/codenotary/immudb4j/TxTest.java b/src/test/java/io/codenotary/immudb4j/TxTest.java index 11a26a5..8951510 100644 --- a/src/test/java/io/codenotary/immudb4j/TxTest.java +++ b/src/test/java/io/codenotary/immudb4j/TxTest.java @@ -87,11 +87,11 @@ public void t2() { Assert.fail("Failed at set.", e); } - List txs = immuClient.txScanAll(initialTxId, 1, false); + List txs = immuClient.txScanAll(initialTxId, false, 1); Assert.assertNotNull(txs); Assert.assertEquals(txs.size(), 1); - txs = immuClient.txScanAll(initialTxId, 2, false); + txs = immuClient.txScanAll(initialTxId, false, 2); Assert.assertNotNull(txs); Assert.assertEquals(txs.size(), 2);