Skip to content

Commit

Permalink
Merge pull request #466 from niveathika/2201.2.x
Browse files Browse the repository at this point in the history
[2201.2.x Backport] Add support for XA transaction
  • Loading branch information
niveathika authored Nov 11, 2022
2 parents f4f2fe7 + f3292d2 commit 1a2e3f6
Show file tree
Hide file tree
Showing 12 changed files with 229 additions and 21 deletions.
6 changes: 3 additions & 3 deletions ballerina/Ballerina.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion ballerina/CompilerPlugin.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
8 changes: 4 additions & 4 deletions ballerina/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down Expand Up @@ -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"},
Expand All @@ -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"},
Expand Down Expand Up @@ -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"},
Expand Down
27 changes: 15 additions & 12 deletions ballerina/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions ballerina/client.bal
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
166 changes: 166 additions & 0 deletions ballerina/tests/15-xa-transaction.bal
Original file line number Diff line number Diff line change
@@ -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<XAResultCount, sql:Error?> 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<XAResultCount, sql:Error?> 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<XAResultCount, sql:Error?> 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<XAResultCount, sql:Error?> streamData = dbClient->query(`Select COUNT(*) as countval
from SalaryTrx where id = ${id}`);
return getResult(streamData);
}

isolated function getResult(stream<XAResultCount, sql:Error?> 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;
}
2 changes: 2 additions & 0 deletions ballerina/tests/resources/sql-scripts/run-sql-scripts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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');
Original file line number Diff line number Diff line change
@@ -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);

1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

/**
Expand Down Expand Up @@ -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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,17 @@ public static Object createClient(
BMap<BString, Object> datasourceOptions = null;
Properties poolProperties = null;
String protocol = Constants.PROTOCOL_TCP;
String dataSourceName = Constants.ORACLE_DATASOURCE_NAME;

if (options != null) {
datasourceOptions = Utils.generateOptionsMap(options);
poolProperties = Utils.generatePoolProperties(options);
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=");
Expand All @@ -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)
Expand Down

0 comments on commit 1a2e3f6

Please sign in to comment.