diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 328857e4..7d1252a6 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerinax" name = "oracledb" -version = "1.5.0" +version = "1.5.1" authors = ["Ballerina"] keywords = ["database", "client", "network", "SQL", "RDBMS", "OracleDB", "Oracle"] repository = "https://github.com/ballerina-platform/module-ballerinax-oracledb" @@ -12,8 +12,8 @@ distribution = "2201.2.0" [[platform.java11.dependency]] groupId = "io.ballerina.stdlib" artifactId = "oracledb-native" -version = "1.5.0" -path = "../native/build/libs/oracledb-native-1.5.0.jar" +version = "1.5.1" +path = "../native/build/libs/oracledb-native-1.5.1-SNAPSHOT.jar" [[platform.java11.dependency]] groupId = "io.ballerina.stdlib" diff --git a/ballerina/CompilerPlugin.toml b/ballerina/CompilerPlugin.toml index 8756bf37..7f06a1dc 100644 --- a/ballerina/CompilerPlugin.toml +++ b/ballerina/CompilerPlugin.toml @@ -3,4 +3,4 @@ id = "oracledb-compiler-plugin" class = "io.ballerina.stdlib.oracledb.compiler.OracleDBCompilerPlugin" [[dependency]] -path = "../compiler-plugin/build/libs/oracledb-compiler-plugin-1.5.0.jar" +path = "../compiler-plugin/build/libs/oracledb-compiler-plugin-1.5.1-SNAPSHOT.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 8fc9d4d0..e0c8dac6 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -72,7 +72,7 @@ modules = [ [[package]] org = "ballerina" name = "http" -version = "2.4.0" +version = "2.4.3" scope = "testOnly" dependencies = [ {org = "ballerina", name = "auth"}, @@ -224,7 +224,7 @@ dependencies = [ [[package]] org = "ballerina" name = "log" -version = "2.4.0" +version = "2.4.1" scope = "testOnly" dependencies = [ {org = "ballerina", name = "io"}, @@ -247,7 +247,7 @@ dependencies = [ [[package]] org = "ballerina" name = "oauth2" -version = "2.4.0" +version = "2.4.1" scope = "testOnly" dependencies = [ {org = "ballerina", name = "cache"}, @@ -380,7 +380,7 @@ modules = [ [[package]] org = "ballerinax" name = "oracledb" -version = "1.5.0" +version = "1.5.1" dependencies = [ {org = "ballerina", name = "crypto"}, {org = "ballerina", name = "file"}, diff --git a/ballerina/build.gradle b/ballerina/build.gradle index 07bb6dfd..1978b547 100644 --- a/ballerina/build.gradle +++ b/ballerina/build.gradle @@ -222,23 +222,26 @@ task startTestDockerContainers() { } } -task stopTestDockerContainers() { - doLast { - if (!Os.isFamily(Os.FAMILY_WINDOWS)) { - def containers = "ballerina-oracledb" - try { - def stdOut = new ByteArrayOutputStream() - exec { - commandLine 'sh', '-c', "docker stop ${containers}" - standardOutput = stdOut - } - } catch (ignore) { - println("Gradle process can safely ignore stopTestDockerContainers task") +def stopTestDockerContainer(containerName) { + if (!Os.isFamily(Os.FAMILY_WINDOWS)) { + try { + def stdOut = new ByteArrayOutputStream() + exec { + commandLine 'sh', '-c', "docker stop ${containerName}" + standardOutput = stdOut } + } catch (ignore) { + println("Gradle process can safely ignore stopTestDockerContainers task") } } } +task stopTestDockerContainers() { + doLast { + stopTestDockerContainer("ballerina-oracledb") + } +} + updateTomlFiles.dependsOn copyStdlibs startTestDockerContainers.dependsOn createTestDockerImage diff --git a/ballerina/client.bal b/ballerina/client.bal index c08bb43a..c15dd187 100644 --- a/ballerina/client.bal +++ b/ballerina/client.bal @@ -131,12 +131,14 @@ public type SecureSocket record {| # + connectTimeout - Timeout (in seconds) to be used when connecting to the Oracle server # + socketTimeout - Socket timeout (in seconds) to be used during the read/write operations with the Oracle database server # (0 means no socket timeout) +# + useXADatasource - If true, uses XADatasource for transactions public type Options record {| SecureSocket ssl?; decimal loginTimeout = 0; boolean autoCommit = true; decimal connectTimeout = 30; decimal socketTimeout?; + boolean useXADatasource = false; |}; # Client configuration record for connection initialization. diff --git a/ballerina/tests/15-xa-transaction.bal b/ballerina/tests/15-xa-transaction.bal new file mode 100644 index 00000000..3f6f72c1 --- /dev/null +++ b/ballerina/tests/15-xa-transaction.bal @@ -0,0 +1,166 @@ +// Copyright (c) 2022 WSO2 LLC. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you 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. + +import ballerina/test; +import ballerina/sql; + +int DBCLIENT2_PORT = 1522; + +type XAResultCount record { + int COUNTVAL; +}; + +// Following test cases are disabled, since they need 2 db instances to run. +// Due to a constraint 2 Oracle Docker containers cannot be run on the same machine. +// Currently verified it manually with a local instance running on port 1522 and docker image running on port 1521 + +@test:Config { + enable: false, + groups: ["transaction", "xa-transaction"] +} +function testXATransactionSuccess() returns error? { + Client dbClient1 = check new (HOST, USER, PASSWORD, DATABASE, PORT, connectionPool = {maxOpenConnections: 1}, + options = { + useXADatasource: true + } + ); + Client dbClient2 = check new (HOST, USER, PASSWORD, DATABASE, DBCLIENT2_PORT, connectionPool = {maxOpenConnections: 1}, + options = { + useXADatasource: true + } + ); + + transaction { + _ = check dbClient1->execute(`insert into Customers (customerId, name, creditLimit, country) + values (1, 'Anne', 1000, 'UK')`); + _ = check dbClient2->execute(`insert into Salary (id, value) values (1, 1000)`); + check commit; + } on fail { + test:assertFail(msg = "Transaction failed"); + } + + int count1 = check getCustomerCount(dbClient1, 1); + int count2 = check getSalaryCount(dbClient2, 1); + test:assertEquals(count1, 1, "First transaction failed"); + test:assertEquals(count2, 1, "Second transaction failed"); + + check dbClient1.close(); + check dbClient2.close(); +} + +@test:Config { + enable: false, + groups: ["transaction", "xa-transaction"] +} +function testXATransactionFailureWithDataSource() returns error? { + Client dbClient1 = check new (HOST, USER, PASSWORD, DATABASE, PORT, connectionPool = {maxOpenConnections: 1}, + options = { + useXADatasource: true + } + ); + Client dbClient2 = check new (HOST, USER, PASSWORD, DATABASE, DBCLIENT2_PORT, connectionPool = {maxOpenConnections: 1}, + options = { + useXADatasource: true + } + ); + + transaction { + // Intentionally fail first statement + _ = check dbClient1->execute(`insert into CustomersTrx (customerId, name, creditLimit, country) + values (30, 'Anne', 1000, 'UK')`); + _ = check dbClient2->execute(`insert into Salary (id, value) values (10, 1000)`); + check commit; + } on fail error e { + test:assertTrue(e.message().includes("Duplicate"), msg = "Transaction failed as expected"); + } + + int count1 = check getCustomerTrxCount(dbClient1, 30); + int count2 = check getSalaryCount(dbClient2, 20); + test:assertEquals(count1, 1, "First transaction should have failed"); + test:assertEquals(count2, 0, "Second transaction should not have been executed"); + + check dbClient1.close(); + check dbClient2.close(); +} + +@test:Config { + enable: false, + groups: ["transaction", "xa-transaction"] +} +function testXATransactionPartialSuccessWithDataSource() returns error? { + Client dbClient1 = check new (HOST, USER, PASSWORD, DATABASE, PORT, connectionPool = {maxOpenConnections: 1}, + options = { + useXADatasource: true + } + ); + Client dbClient2 = check new (HOST, USER, PASSWORD, DATABASE, DBCLIENT2_PORT, connectionPool = {maxOpenConnections: 1}, + options = { + useXADatasource: true + } + ); + + transaction { + _ = check dbClient1->execute(`insert into Customers (customerId, name, creditLimit, country) + values (30, 'Anne', 1000, 'UK')`); + // Intentionally fail second statement + _ = check dbClient2->execute(`insert into SalaryTrx (id, value) values (20, 1000)`); + check commit; + } on fail error e { + test:assertTrue(e.message().includes("Duplicate"), msg = "Transaction failed as expected"); + } + + int count1 = check getCustomerCount(dbClient1, 30); + int count2 = check getSalaryTrxCount(dbClient2, 20); + test:assertEquals(count1, 0, "First transaction is not rolledback"); + test:assertEquals(count2, 1, "Second transaction has succeeded"); + + check dbClient1.close(); + check dbClient2.close(); +} + +isolated function getCustomerCount(Client dbClient, int id) returns int|error { + stream streamData = dbClient->query(`Select COUNT(*) as + countVal from Customers where customerId = ${id}`); + return getResult(streamData); +} + +isolated function getCustomerTrxCount(Client dbClient, int id) returns int|error { + stream streamData = dbClient->query(`Select COUNT(*) as + countVal from CustomersTrx where customerId = ${id}`); + return getResult(streamData); +} + +isolated function getSalaryCount(Client dbClient, int id) returns int|error { + stream streamData = dbClient->query(`Select COUNT(*) as countval + from Salary where id = ${id}`); + return getResult(streamData); +} + +isolated function getSalaryTrxCount(Client dbClient, int id) returns int|error { + stream streamData = dbClient->query(`Select COUNT(*) as countval + from SalaryTrx where id = ${id}`); + return getResult(streamData); +} + +isolated function getResult(stream streamData) returns int|error { + record {|XAResultCount value;|}? data = check streamData.next(); + check streamData.close(); + XAResultCount? value = data?.value; + if value is XAResultCount { + return value.COUNTVAL; + } + return 0; +} diff --git a/ballerina/tests/resources/sql-scripts/run-sql-scripts.sh b/ballerina/tests/resources/sql-scripts/run-sql-scripts.sh index fb7108cb..6c17a93a 100755 --- a/ballerina/tests/resources/sql-scripts/run-sql-scripts.sh +++ b/ballerina/tests/resources/sql-scripts/run-sql-scripts.sh @@ -10,4 +10,6 @@ sqlplus -S admin/password@localhost/ORCLCDB.localdomain <<< @procedures/stored-p sqlplus -S admin/password@localhost/ORCLCDB.localdomain <<< @query/complex-params-test-data.sql sqlplus -S admin/password@localhost/ORCLCDB.localdomain <<< @query/simple-params-test-data.sql sqlplus -S admin/password@localhost/ORCLCDB.localdomain <<< @transaction/local-transaction-test-data.sql +sqlplus -S admin/password@localhost/ORCLCDB.localdomain <<< @transaction/xa-transaction-test-data-1.sql +sqlplus -S admin/password@localhost/ORCLCDB.localdomain <<< @transaction/xa-transaction-test-data-2.sql sqlplus -S admin/password@localhost/ORCLCDB.localdomain <<< @error/error-test-data.sql diff --git a/ballerina/tests/resources/sql-scripts/transaction/xa-transaction-test-data-1.sql b/ballerina/tests/resources/sql-scripts/transaction/xa-transaction-test-data-1.sql new file mode 100644 index 00000000..c083559e --- /dev/null +++ b/ballerina/tests/resources/sql-scripts/transaction/xa-transaction-test-data-1.sql @@ -0,0 +1,16 @@ +CREATE TABLE Customers( + customerId NUMBER, + name VARCHAR(300), + creditLimit DOUBLE PRECISION, + country VARCHAR(300) +); + +CREATE TABLE CustomersTrx( + customerId INTEGER, + name VARCHAR(300), + creditLimit DOUBLE PRECISION, + country VARCHAR(300), + PRIMARY KEY (customerId) +); + +INSERT INTO CustomersTrx VALUES (30, 'Oliver', 200000, 'UK'); diff --git a/ballerina/tests/resources/sql-scripts/transaction/xa-transaction-test-data-2.sql b/ballerina/tests/resources/sql-scripts/transaction/xa-transaction-test-data-2.sql new file mode 100644 index 00000000..ce5cc549 --- /dev/null +++ b/ballerina/tests/resources/sql-scripts/transaction/xa-transaction-test-data-2.sql @@ -0,0 +1,13 @@ +CREATE TABLE Salary ( + ID INTEGER, + VALUE DOUBLE PRECISION +); + +CREATE TABLE SalaryTrx ( + ID INTEGER, + VALUE DOUBLE PRECISION, + PRIMARY KEY (ID) +); + +INSERT INTO SalaryTrx VALUES (20, 30000); + diff --git a/changelog.md b/changelog.md index 8e1fb973..0aad4860 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- [Support for XA transaction](https://github.com/ballerina-platform/ballerina-standard-library/issues/3599) ### Changed diff --git a/native/src/main/java/io/ballerina/stdlib/oracledb/Constants.java b/native/src/main/java/io/ballerina/stdlib/oracledb/Constants.java index 1f189313..4556a1d3 100644 --- a/native/src/main/java/io/ballerina/stdlib/oracledb/Constants.java +++ b/native/src/main/java/io/ballerina/stdlib/oracledb/Constants.java @@ -57,6 +57,7 @@ private Options () {} public static final BString LOGIN_TIMEOUT_SECONDS = StringUtils.fromString("loginTimeout"); public static final BString CONNECT_TIMEOUT_SECONDS = StringUtils.fromString("connectTimeout"); public static final BString SOCKET_TIMEOUT_SECONDS = StringUtils.fromString("socketTimeout"); + public static final BString USE_XA_DATASOURCE = StringUtils.fromString("useXADatasource"); } /** @@ -266,5 +267,6 @@ private BallerinaArrayTypes() {} public static final String PROTOCOL_TCP = "TCP"; public static final String PROTOCOL_TCPS = "TCPS"; public static final String ORACLE_DATASOURCE_NAME = "oracle.jdbc.pool.OracleDataSource"; + public static final String ORACLE_XA_DATASOURCE_NAME = "oracle.jdbc.xa.client.OracleXADataSource"; public static final String CUSTOM_RESULT_ITERATOR_OBJECT = "CustomResultIterator"; } diff --git a/native/src/main/java/io/ballerina/stdlib/oracledb/nativeimpl/ClientProcessor.java b/native/src/main/java/io/ballerina/stdlib/oracledb/nativeimpl/ClientProcessor.java index 9fc043cf..961ab134 100644 --- a/native/src/main/java/io/ballerina/stdlib/oracledb/nativeimpl/ClientProcessor.java +++ b/native/src/main/java/io/ballerina/stdlib/oracledb/nativeimpl/ClientProcessor.java @@ -58,6 +58,7 @@ public static Object createClient( BMap datasourceOptions = null; Properties poolProperties = null; String protocol = Constants.PROTOCOL_TCP; + String dataSourceName = Constants.ORACLE_DATASOURCE_NAME; if (options != null) { datasourceOptions = Utils.generateOptionsMap(options); @@ -65,6 +66,9 @@ public static Object createClient( if (options.getMapValue(Constants.Options.SSL) != null) { protocol = Constants.PROTOCOL_TCPS; } + if (options.getBooleanValue(Constants.Options.USE_XA_DATASOURCE)) { + dataSourceName = Constants.ORACLE_XA_DATASOURCE_NAME; + } } StringBuilder url = new StringBuilder(Constants.DRIVER); url.append("(DESCRIPTION=(ADDRESS="); @@ -75,7 +79,6 @@ public static Object createClient( url.append("(CONNECT_DATA=(SERVICE_NAME=").append(database).append("))"); url.append(")"); BMap connectionPool = clientConfig.getMapValue(Constants.ClientConfiguration.CONNECTION_POOL_OPTIONS); - String dataSourceName = Constants.ORACLE_DATASOURCE_NAME; SQLDatasource.SQLDatasourceParams sqlDatasourceParams = new SQLDatasource.SQLDatasourceParams() .setUrl(url.toString()) .setUser(user)