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

Add H2 database support for the persist SQL #57

Closed
wants to merge 16 commits into from
Closed
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/Module.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This package provides relational database support for the `bal persist` feature, which provides functionality to store and query data from a relational database through a data model instead of writing SQL.

Currently, this package supports MySQL, MSSQL, and PostgreSQL databases. However, we are also planning to add support for other relational databases such as Oracle.
Currently, this package supports MySQL, MSSQL, H2 and PostgreSQL databases. However, we are also planning to add support for other relational databases such as Oracle.

## How to use with `bal persist`

Expand All @@ -13,7 +13,7 @@ By default, `bal persist` utilizes the in-memory data store. Therefore, you must
1. Initialize `bal persist` and integrate to `bal build` using the following command,

```
$ bal persist add --datastore [mysql/mssql/postgresql] --module <module_name>
$ bal persist add --datastore [mysql/mssql/postgresql/h2] --module <module_name>
```

2. After defining the entities, build the application using the following command,
Expand All @@ -33,7 +33,7 @@ By default, `bal persist` utilizes the in-memory data store. Therefore, you must
2. Generate the persist client using the following command,

```
$ bal persist generate --datastore [mysql/mssql/postgresql] --module <module_name>
$ bal persist generate --datastore [mysql/mssql/postgresql/h2] --module <module_name>
```

## Supported Ballerina Types
Expand Down
100 changes: 50 additions & 50 deletions ballerina/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -204,65 +204,65 @@ task stopMySQLTestDockerContainer() {
}

task createMSSQLTestDockerImage(type: Exec) {
if (!Os.isFamily(Os.FAMILY_WINDOWS)) {
def standardOutput = new ByteArrayOutputStream()
commandLine 'sh', '-c', "docker build -f $project.projectDir/tests/resources/mssql/Dockerfile -t ballerina-persist-mssql" +
" -q $project.projectDir/tests/resources/mssql/"
doLast {
checkExecResult(executionResult, 'Error', standardOutput)
sleep(10 * 1000)
}
}
if (!Os.isFamily(Os.FAMILY_WINDOWS)) {
def standardOutput = new ByteArrayOutputStream()
commandLine 'sh', '-c', "docker build -f $project.projectDir/tests/resources/mssql/Dockerfile -t ballerina-persist-mssql" +
" -q $project.projectDir/tests/resources/mssql/"
doLast {
checkExecResult(executionResult, 'Error', standardOutput)
sleep(10 * 1000)
}
}
}

def checkMSSQLTestDockerContainerStatus(containerName) {
if (!Os.isFamily(Os.FAMILY_WINDOWS)) {
try {
return exec {
commandLine 'sh', '-c',
"docker exec ${containerName} /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Test123#"
}.exitValue
} catch (all) {
return 1;
}
}
if (!Os.isFamily(Os.FAMILY_WINDOWS)) {
try {
return exec {
commandLine 'sh', '-c',
"docker exec ${containerName} /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Test123#"
}.exitValue
} catch (all) {
return 1;
}
}
}

task startMSSQLTestDockerContainer(type: Exec) {
if (!Os.isFamily(Os.FAMILY_WINDOWS)) {
def standardOutput = new ByteArrayOutputStream()
commandLine 'sh', '-c',
"docker run --rm -d --name ballerina-persist-mssql -e ACCEPT_EULA=1 -e SA_PASSWORD=Test123# -p 1433:1433 -d ballerina-persist-mssql"
def healthCheck = 1;
def counter = 0;
doLast {
checkExecResult(executionResult, 'Error', standardOutput)
while (healthCheck != 0 && counter < 12) {
sleep(5 * 1000)
healthCheck = checkMSSQLTestDockerContainerStatus("ballerina-persist-mssql")
counter = counter + 1;
}
if (healthCheck != 0) {
throw new GradleException("Docker container 'ballerina-persist-mssql' health test exceeded timeout!")
}
}
}
if (!Os.isFamily(Os.FAMILY_WINDOWS)) {
def standardOutput = new ByteArrayOutputStream()
commandLine 'sh', '-c',
"docker run --rm -d --name ballerina-persist-mssql -e ACCEPT_EULA=1 -e SA_PASSWORD=Test123# -p 1433:1433 -d ballerina-persist-mssql"
def healthCheck = 1;
def counter = 0;
doLast {
checkExecResult(executionResult, 'Error', standardOutput)
while (healthCheck != 0 && counter < 12) {
sleep(5 * 1000)
healthCheck = checkMSSQLTestDockerContainerStatus("ballerina-persist-mssql")
counter = counter + 1;
}
if (healthCheck != 0) {
throw new GradleException("Docker container 'ballerina-persist-mssql' health test exceeded timeout!")
}
}
}
}

task stopMSSQLTestDockerContainer() {
doLast {
if (!Os.isFamily(Os.FAMILY_WINDOWS)) {
try {
def stdOut = new ByteArrayOutputStream()
exec {
commandLine 'sh', '-c', "docker stop ballerina-persist-mssql"
standardOutput = stdOut
}
} catch (all) {
println("Process can safely ignore stopTestDockerContainer task")
}
}
}
doLast {
if (!Os.isFamily(Os.FAMILY_WINDOWS)) {
try {
def stdOut = new ByteArrayOutputStream()
exec {
commandLine 'sh', '-c', "docker stop ballerina-persist-mssql"
standardOutput = stdOut
}
} catch (all) {
println("Process can safely ignore stopTestDockerContainer task")
}
}
}
}

task createPostgreSQLTestDockerImage(type: Exec) {
Expand Down
9 changes: 9 additions & 0 deletions ballerina/constants.bal
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,12 @@ public final DataSourceSpecifics & readonly POSTGRESQL_SPECIFICS = {
duplicateKeyStartIndicator: "Detail: Key ",
duplicateKeyEndIndicator: " already exists."
};

public final DataSourceSpecifics & readonly H2_SPECIFICS = {
quoteOpen: "\"",
quoteClose: "\"",
constraintViolationErrorMessage: "Referential integrity constraint violation",
duplicateEntryErrorMessage: "Unique index or primary key violation",
duplicateKeyStartIndicator: "Unique index or primary key violation: \"",
duplicateKeyEndIndicator: " ON "
};
5 changes: 5 additions & 0 deletions ballerina/tests/Config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ password="postgres"
host="localhost"
port=5432
database="test"

[h2]
url="jdbc:h2:./build/test"
user="sa"
password=""
228 changes: 228 additions & 0 deletions ballerina/tests/h2-all-types-tests.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// Copyright (c) 2024 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/persist;

@test:Config {
groups: ["all-types", "h2"]
}
function h2AllTypesCreateTest() returns error? {
H2TestEntitiesClient testEntitiesClient = check new ();

int[] ids = check testEntitiesClient->/alltypes.post([allTypes1, allTypes2]);
test:assertEquals(ids, [allTypes1.id, allTypes2.id]);

AllTypes allTypesRetrieved = check testEntitiesClient->/alltypes/[allTypes1.id].get();
test:assertEquals(allTypesRetrieved, allTypes1Expected);

allTypesRetrieved = check testEntitiesClient->/alltypes/[allTypes2.id].get();
test:assertEquals(allTypesRetrieved, allTypes2Expected);

check testEntitiesClient.close();
}

@test:Config {
groups: ["all-types", "h2"]
}
function h2AllTypesCreateOptionalTest() returns error? {
H2TestEntitiesClient testEntitiesClient = check new ();

int[] ids = check testEntitiesClient->/alltypes.post([allTypes3]);
test:assertEquals(ids, [allTypes3.id]);

AllTypes allTypesRetrieved = check testEntitiesClient->/alltypes/[allTypes3.id].get();
test:assertEquals(allTypesRetrieved, allTypes3Expected);

check testEntitiesClient.close();
}

@test:Config {
groups: ["all-types", "h2"],
dependsOn: [h2AllTypesCreateTest, h2AllTypesCreateOptionalTest]
}
function h2AllTypesReadTest() returns error? {
H2TestEntitiesClient testEntitiesClient = check new ();

stream<AllTypes, error?> allTypesStream = testEntitiesClient->/alltypes.get();
AllTypes[] allTypes = check from AllTypes allTypesRecord in allTypesStream
select allTypesRecord;

test:assertEquals(allTypes, [allTypes1Expected, allTypes2Expected, allTypes3Expected]);
check testEntitiesClient.close();
}

@test:Config {
groups: ["all-types", "h2", "dependent"],
dependsOn: [h2AllTypesCreateTest, h2AllTypesCreateOptionalTest]
}
function h2AllTypesReadDependentTest() returns error? {
H2TestEntitiesClient testEntitiesClient = check new ();

stream<AllTypesDependent, error?> allTypesStream = testEntitiesClient->/alltypes.get();
AllTypesDependent[] allTypes = check from AllTypesDependent allTypesRecord in allTypesStream
select allTypesRecord;

test:assertEquals(allTypes, [
{
booleanType: allTypes1Expected.booleanType,
intType: allTypes1Expected.intType,
floatType: allTypes1Expected.floatType,
decimalType: allTypes1Expected.decimalType,
stringType: allTypes1Expected.stringType,
byteArrayType: allTypes1Expected.byteArrayType,
dateType: allTypes1Expected.dateType,
timeOfDayType: allTypes1Expected.timeOfDayType,
civilType: allTypes1Expected.civilType,
booleanTypeOptional: allTypes1Expected.booleanTypeOptional,
intTypeOptional: allTypes1Expected.intTypeOptional,
floatTypeOptional: allTypes1Expected.floatTypeOptional,
decimalTypeOptional: allTypes1Expected.decimalTypeOptional,
stringTypeOptional: allTypes1Expected.stringTypeOptional,
dateTypeOptional: allTypes1Expected.dateTypeOptional,
timeOfDayTypeOptional: allTypes1Expected.timeOfDayTypeOptional,
civilTypeOptional: allTypes1Expected.civilTypeOptional
},
{
booleanType: allTypes2Expected.booleanType,
intType: allTypes2Expected.intType,
floatType: allTypes2Expected.floatType,
decimalType: allTypes2Expected.decimalType,
stringType: allTypes2Expected.stringType,
byteArrayType: allTypes2Expected.byteArrayType,
dateType: allTypes2Expected.dateType,
timeOfDayType: allTypes2Expected.timeOfDayType,
civilType: allTypes2Expected.civilType,
booleanTypeOptional: allTypes2Expected.booleanTypeOptional,
intTypeOptional: allTypes2Expected.intTypeOptional,
floatTypeOptional: allTypes2Expected.floatTypeOptional,
decimalTypeOptional: allTypes2Expected.decimalTypeOptional,
stringTypeOptional: allTypes2Expected.stringTypeOptional,
dateTypeOptional: allTypes2Expected.dateTypeOptional,
timeOfDayTypeOptional: allTypes2Expected.timeOfDayTypeOptional,
civilTypeOptional: allTypes2Expected.civilTypeOptional
},
{
booleanType: allTypes3Expected.booleanType,
intType: allTypes3Expected.intType,
floatType: allTypes3Expected.floatType,
decimalType: allTypes3Expected.decimalType,
stringType: allTypes3Expected.stringType,
byteArrayType: allTypes3Expected.byteArrayType,
dateType: allTypes3Expected.dateType,
timeOfDayType: allTypes3Expected.timeOfDayType,
civilType: allTypes3Expected.civilType,
booleanTypeOptional: allTypes3Expected.booleanTypeOptional,
intTypeOptional: allTypes3Expected.intTypeOptional,
floatTypeOptional: allTypes3Expected.floatTypeOptional,
decimalTypeOptional: allTypes3Expected.decimalTypeOptional,
stringTypeOptional: allTypes3Expected.stringTypeOptional,
dateTypeOptional: allTypes3Expected.dateTypeOptional,
timeOfDayTypeOptional: allTypes3Expected.timeOfDayTypeOptional,
civilTypeOptional: allTypes3Expected.civilTypeOptional
}
]);
check testEntitiesClient.close();
}

@test:Config {
groups: ["all-types", "h2"],
dependsOn: [h2AllTypesCreateTest, h2AllTypesCreateOptionalTest]
}
function h2AllTypesReadOneTest() returns error? {
H2TestEntitiesClient testEntitiesClient = check new ();

AllTypes allTypesRetrieved = check testEntitiesClient->/alltypes/[allTypes1.id].get();
test:assertEquals(allTypesRetrieved, allTypes1Expected);

allTypesRetrieved = check testEntitiesClient->/alltypes/[allTypes2.id].get();
test:assertEquals(allTypesRetrieved, allTypes2Expected);

allTypesRetrieved = check testEntitiesClient->/alltypes/[allTypes3.id].get();
test:assertEquals(allTypesRetrieved, allTypes3Expected);

check testEntitiesClient.close();
}

@test:Config {
groups: ["all-types", "h2"]
}
function h2AllTypesReadOneTestNegative() returns error? {
H2TestEntitiesClient testEntitiesClient = check new ();

AllTypes|persist:Error allTypesRetrieved = testEntitiesClient->/alltypes/[4].get();
if allTypesRetrieved is persist:NotFoundError {
test:assertEquals(allTypesRetrieved.message(), "A record with the key '4' does not exist for the entity 'AllTypes'.");
}
else {
test:assertFail("persist:NotFoundError expected.");
}

check testEntitiesClient.close();
}

@test:Config {
groups: ["all-types", "h2"],
dependsOn: [h2AllTypesReadOneTest, h2AllTypesReadTest, h2AllTypesReadDependentTest]
}
function h2AllTypesUpdateTest() returns error? {
H2TestEntitiesClient testEntitiesClient = check new ();

AllTypes allTypes = check testEntitiesClient->/alltypes/[allTypes1.id].put({
booleanType: allTypes3.booleanType,
intType: allTypes1Updated.intType,
floatType: allTypes1Updated.floatType,
decimalType: allTypes1Updated.decimalType,
stringType: allTypes1Updated.stringType,
byteArrayType: allTypes1Updated.byteArrayType,
dateType: allTypes1Updated.dateType,
timeOfDayType: allTypes1Updated.timeOfDayType,
civilType: allTypes1Updated.civilType,
booleanTypeOptional: allTypes1Updated.booleanTypeOptional,
intTypeOptional: allTypes1Updated.intTypeOptional,
floatTypeOptional: allTypes1Updated.floatTypeOptional,
decimalTypeOptional: allTypes1Updated.decimalTypeOptional,
stringTypeOptional: allTypes1Updated.stringTypeOptional,
dateTypeOptional: allTypes1Updated.dateTypeOptional,
timeOfDayTypeOptional: allTypes1Updated.timeOfDayTypeOptional,
civilTypeOptional: allTypes1Updated.civilTypeOptional,
enumType: allTypes1Updated.enumType,
enumTypeOptional: allTypes1Updated.enumTypeOptional
});
test:assertEquals(allTypes, allTypes1UpdatedExpected);

AllTypes allTypesRetrieved = check testEntitiesClient->/alltypes/[allTypes1.id].get();
test:assertEquals(allTypesRetrieved, allTypes1UpdatedExpected);
check testEntitiesClient.close();
}

@test:Config {
groups: ["all-types", "h2"],
dependsOn: [h2AllTypesUpdateTest]
}
function h2AllTypesDeleteTest() returns error? {
H2TestEntitiesClient testEntitiesClient = check new ();

AllTypes allTypes = check testEntitiesClient->/alltypes/[allTypes2.id].delete();
test:assertEquals(allTypes, allTypes2Expected);

stream<AllTypes, error?> allTypesStream = testEntitiesClient->/alltypes.get();
AllTypes[] allTypesCollection = check from AllTypes allTypesRecord in allTypesStream
select allTypesRecord;

test:assertEquals(allTypesCollection, [allTypes1UpdatedExpected, allTypes3Expected]);
check testEntitiesClient.close();
}
Loading
Loading