Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2201.2.x Backport] Add support for XA transaction #466

Merged
merged 5 commits into from
Nov 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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