diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index b60a683..7338544 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerinax" name = "persist.sql" -version = "1.3.0" +version = "1.4.0" authors = ["Ballerina"] keywords = ["persist", "sql", "mysql", "mssql", "sql-server"] repository = "https://github.com/ballerina-platform/module-ballerinax-persist.sql" @@ -15,8 +15,8 @@ graalvmCompatible = true [[platform.java17.dependency]] groupId = "io.ballerina.stdlib" artifactId = "persist.sql-native" -version = "1.3.0" -path = "../native/build/libs/persist.sql-native-1.3.0.jar" +version = "1.4.0" +path = "../native/build/libs/persist.sql-native-1.4.0-SNAPSHOT.jar" [[platform.java17.dependency]] groupId = "io.ballerina.stdlib" diff --git a/ballerina/CompilerPlugin.toml b/ballerina/CompilerPlugin.toml index 157fac9..0ed7b40 100644 --- a/ballerina/CompilerPlugin.toml +++ b/ballerina/CompilerPlugin.toml @@ -3,7 +3,7 @@ id = "persist.sql-compiler-plugin" class = "io.ballerina.stdlib.persist.sql.compiler.PersistSqlCompilerPlugin" [[dependency]] -path = "../compiler-plugin/build/libs/persist.sql-compiler-plugin-1.3.0.jar" +path = "../compiler-plugin/build/libs/persist.sql-compiler-plugin-1.4.0-SNAPSHOT.jar" [[dependency]] path = "./lib/persist-native-1.3.0.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 2f17694..2ba9bb0 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -44,7 +44,7 @@ dependencies = [ [[package]] org = "ballerina" name = "crypto" -version = "2.7.0" +version = "2.7.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, @@ -66,7 +66,7 @@ dependencies = [ [[package]] org = "ballerina" name = "http" -version = "2.11.0" +version = "2.11.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "auth"}, @@ -294,7 +294,7 @@ modules = [ [[package]] org = "ballerina" name = "sql" -version = "1.13.0" +version = "1.13.2" dependencies = [ {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, @@ -383,6 +383,31 @@ modules = [ {org = "ballerinai", packageName = "transaction", moduleName = "transaction"} ] +[[package]] +org = "ballerinax" +name = "h2.driver" +version = "1.1.0" +scope = "testOnly" +modules = [ + {org = "ballerinax", packageName = "h2.driver", moduleName = "h2.driver"} +] + +[[package]] +org = "ballerinax" +name = "java.jdbc" +version = "1.11.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "sql"}, + {org = "ballerina", name = "time"} +] +modules = [ + {org = "ballerinax", packageName = "java.jdbc", moduleName = "java.jdbc"} +] + [[package]] org = "ballerinax" name = "mssql" @@ -435,7 +460,7 @@ modules = [ [[package]] org = "ballerinax" name = "persist.sql" -version = "1.3.0" +version = "1.4.0" dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "log"}, @@ -444,6 +469,8 @@ dependencies = [ {org = "ballerina", name = "test"}, {org = "ballerina", name = "time"}, {org = "ballerinai", name = "transaction"}, + {org = "ballerinax", name = "h2.driver"}, + {org = "ballerinax", name = "java.jdbc"}, {org = "ballerinax", name = "mssql"}, {org = "ballerinax", name = "mssql.driver"}, {org = "ballerinax", name = "mysql"}, diff --git a/ballerina/Module.md b/ballerina/Module.md index 755864b..6136cd9 100644 --- a/ballerina/Module.md +++ b/ballerina/Module.md @@ -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` @@ -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 + $ bal persist add --datastore [mysql/mssql/postgresql/h2] --module ``` 2. After defining the entities, build the application using the following command, @@ -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 + $ bal persist generate --datastore [mysql/mssql/postgresql/h2] --module ``` ## Supported Ballerina Types diff --git a/ballerina/build.gradle b/ballerina/build.gradle index 19c6bd2..aae4a76 100644 --- a/ballerina/build.gradle +++ b/ballerina/build.gradle @@ -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) { diff --git a/ballerina/constants.bal b/ballerina/constants.bal index 755693a..b09b8c2 100644 --- a/ballerina/constants.bal +++ b/ballerina/constants.bal @@ -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 " +}; diff --git a/ballerina/tests/Config.toml b/ballerina/tests/Config.toml index 15feb58..62d9c85 100644 --- a/ballerina/tests/Config.toml +++ b/ballerina/tests/Config.toml @@ -22,3 +22,8 @@ password="postgres" host="localhost" port=5432 database="test" + +[h2] +url="jdbc:h2:./build/test" +user="sa" +password="" diff --git a/ballerina/tests/h2-all-types-tests.bal b/ballerina/tests/h2-all-types-tests.bal new file mode 100644 index 0000000..78228a3 --- /dev/null +++ b/ballerina/tests/h2-all-types-tests.bal @@ -0,0 +1,228 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.org). +// +// 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 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 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 allTypesStream = testEntitiesClient->/alltypes.get(); + AllTypes[] allTypesCollection = check from AllTypes allTypesRecord in allTypesStream + select allTypesRecord; + + test:assertEquals(allTypesCollection, [allTypes1UpdatedExpected, allTypes3Expected]); + check testEntitiesClient.close(); +} diff --git a/ballerina/tests/h2-associations-tests.bal b/ballerina/tests/h2-associations-tests.bal new file mode 100644 index 0000000..aeff6af --- /dev/null +++ b/ballerina/tests/h2-associations-tests.bal @@ -0,0 +1,288 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.org). +// +// 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: ["associations", "h2"], + dependsOn: [h2EmployeeDeleteTestNegative] +} +function h2EmployeeRelationsTest() returns error? { + H2RainierClient rainierClient = check new (); + + Employee employee21 = { + empNo: "employee-21", + firstName: "Tom", + lastName: "Scott", + birthDate: {year: 1992, month: 11, day: 13}, + gender: MALE, + hireDate: {year: 2022, month: 8, day: 1}, + departmentDeptNo: "department-22", + workspaceWorkspaceId: "workspace-22" + }; + + Workspace workspace22 = { + workspaceId: "workspace-22", + workspaceType: "medium", + locationBuildingCode: "building-22" + }; + + BuildingInsert building22 = { + buildingCode: "building-22", + city: "Manhattan", + state: "New York", + country: "USA", + postalCode: "10570", + 'type: "owned" + }; + + Department department22 = { + deptNo: "department-22", + deptName: "Marketing" + }; + + _ = check rainierClient->/buildings.post([building22]); + _ = check rainierClient->/departments.post([department22]); + _ = check rainierClient->/workspaces.post([workspace22]); + _ = check rainierClient->/employees.post([employee21]); + + stream employeeStream = rainierClient->/employees.get(); + EmployeeInfo[] employees = check from EmployeeInfo employee in employeeStream + select employee; + + EmployeeInfo retrieved = check rainierClient->/employees/["employee-21"].get(); + + EmployeeInfo expected = { + firstName: "Tom", + lastName: "Scott", + department: { + deptName: "Marketing" + }, + workspace: { + workspaceId: "workspace-22", + workspaceType: "medium", + locationBuildingCode: "building-22" + } + }; + + test:assertTrue(employees.indexOf(expected) is int, "Expected EmployeeInfo not found."); + test:assertEquals(retrieved, expected); + check rainierClient.close(); +} + +@test:Config { + groups: ["associations", "h2"], + dependsOn: [h2EmployeeDeleteTestNegative] +} +function h2DepartmentRelationsTest() returns error? { + H2RainierClient rainierClient = check new (); + + Employee employee11 = { + empNo: "employee-11", + firstName: "Tom", + lastName: "Scott", + birthDate: {year: 1992, month: 11, day: 13}, + gender: MALE, + hireDate: {year: 2022, month: 8, day: 1}, + departmentDeptNo: "department-12", + workspaceWorkspaceId: "workspace-12" + }; + + Employee employee12 = { + empNo: "employee-12", + firstName: "Jane", + lastName: "Doe", + birthDate: {year: 1996, month: 9, day: 15}, + gender: FEMALE, + hireDate: {year: 2022, month: 6, day: 1}, + departmentDeptNo: "department-12", + workspaceWorkspaceId: "workspace-12" + }; + + Workspace workspace12 = { + workspaceId: "workspace-12", + workspaceType: "medium", + locationBuildingCode: "building-12" + }; + + BuildingInsert building12 = { + buildingCode: "building-12", + city: "Manhattan", + state: "New York", + country: "USA", + postalCode: "10570", + 'type: "owned" + }; + + Department department12 = { + deptNo: "department-12", + deptName: "Marketing" + }; + + _ = check rainierClient->/buildings.post([building12]); + _ = check rainierClient->/departments.post([department12]); + _ = check rainierClient->/workspaces.post([workspace12]); + _ = check rainierClient->/employees.post([employee11, employee12]); + + stream departmentStream = rainierClient->/departments.get(); + DepartmentInfo[] departments = check from DepartmentInfo department in departmentStream + select department; + + DepartmentInfo retrieved = check rainierClient->/departments/["department-12"].get(); + + DepartmentInfo expected = { + deptNo: "department-12", + deptName: "Marketing", + employees: [ + { + firstName: "Tom", + lastName: "Scott" + }, + { + firstName: "Jane", + lastName: "Doe" + } + ] + }; + + test:assertTrue(departments.indexOf(expected) is int, "Expected DepartmentInfo not found."); + test:assertEquals(retrieved, expected); + check rainierClient.close(); +} + +@test:Config { + groups: ["associations", "h2"], + dependsOn: [h2EmployeeRelationsTest] +} +function h2WorkspaceRelationsTest() returns error? { + H2RainierClient rainierClient = check new (); + + Employee employee22 = { + empNo: "employee-22", + firstName: "James", + lastName: "David", + birthDate: {year: 1996, month: 11, day: 13}, + gender: FEMALE, + hireDate: {year: 2021, month: 8, day: 1}, + departmentDeptNo: "department-22", + workspaceWorkspaceId: "workspace-22" + }; + _ = check rainierClient->/employees.post([employee22]); + + stream workspaceStream = rainierClient->/workspaces.get(); + WorkspaceInfo[] workspaces = check from WorkspaceInfo workspace in workspaceStream + select workspace; + + WorkspaceInfo retrieved = check rainierClient->/workspaces/["workspace-22"].get(); + + WorkspaceInfo expected = { + workspaceType: "medium", + location: { + buildingCode: "building-22", + city: "Manhattan", + state: "New York", + country: "USA", + postalCode: "10570", + 'type: "owned" + }, + employees: [ + { + empNo: "employee-21", + firstName: "Tom", + lastName: "Scott", + birthDate: {year: 1992, month: 11, day: 13}, + gender: MALE, + hireDate: {year: 2022, month: 8, day: 1}, + departmentDeptNo: "department-22", + workspaceWorkspaceId: "workspace-22" + }, + { + empNo: "employee-22", + firstName: "James", + lastName: "David", + birthDate: {year: 1996, month: 11, day: 13}, + gender: FEMALE, + hireDate: {year: 2021, month: 8, day: 1}, + departmentDeptNo: "department-22", + workspaceWorkspaceId: "workspace-22" + } + ] + }; + + boolean found = false; + _ = from WorkspaceInfo workspace in workspaces + do { + if workspace == expected { + found = true; + } + }; + + if !found { + test:assertFail("Expected WorkspaceInfo not found."); + } + + test:assertEquals(retrieved, expected); + + check rainierClient.close(); +} + +@test:Config { + groups: ["associations", "h2"], + dependsOn: [h2EmployeeRelationsTest] +} +function h2BuildingRelationsTest() returns error? { + H2RainierClient rainierClient = check new (); + + stream buildingStream = rainierClient->/buildings.get(); + BuildingInfo[] buildings = check from BuildingInfo building in buildingStream + select building; + + BuildingInfo retrieved = check rainierClient->/buildings/["building-22"].get(); + + BuildingInfo expected = { + buildingCode: "building-22", + city: "Manhattan", + state: "New York", + country: "USA", + postalCode: "10570", + 'type: "owned", + workspaces: [ + { + workspaceId: "workspace-22", + workspaceType: "medium", + locationBuildingCode: "building-22" + } + ] + }; + + boolean found = false; + _ = from BuildingInfo building in buildings + do { + if (building.buildingCode == "building-22") { + found = true; + test:assertEquals(building, expected); + } + }; + + if !found { + test:assertFail("Expected BuildingInfo not found."); + } + + test:assertEquals(retrieved, expected); + + check rainierClient.close(); +} diff --git a/ballerina/tests/h2-building-tests.bal b/ballerina/tests/h2-building-tests.bal new file mode 100644 index 0000000..e4360f6 --- /dev/null +++ b/ballerina/tests/h2-building-tests.bal @@ -0,0 +1,226 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.org). +// +// 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: ["building", "h2"] +} +function h2BuildingCreateTest() returns error? { + H2RainierClient rainierClient = check new (); + + string[] buildingCodes = check rainierClient->/buildings.post([building1]); + test:assertEquals(buildingCodes, [building1.buildingCode]); + + Building buildingRetrieved = check rainierClient->/buildings/[building1.buildingCode].get(); + test:assertEquals(buildingRetrieved, building1); + check rainierClient.close(); +} + +@test:Config { + groups: ["building", "h2"] +} +function h2BuildingCreateTest2() returns error? { + H2RainierClient rainierClient = check new (); + + string[] buildingCodes = check rainierClient->/buildings.post([building2, building3]); + + test:assertEquals(buildingCodes, [building2.buildingCode, building3.buildingCode]); + + Building buildingRetrieved = check rainierClient->/buildings/[building2.buildingCode].get(); + test:assertEquals(buildingRetrieved, building2); + + buildingRetrieved = check rainierClient->/buildings/[building3.buildingCode].get(); + test:assertEquals(buildingRetrieved, building3); + + check rainierClient.close(); +} + +@test:Config { + groups: ["building", "h2"] +} +function h2BuildingCreateTestNegative() returns error? { + H2RainierClient rainierClient = check new (); + + string[]|error building = rainierClient->/buildings.post([invalidBuilding]); + if building is persist:Error { + test:assertTrue(building.message().includes("Value too long for column \"buildingCode CHARACTER VARYING(36)")); + } else { + test:assertFail("Error expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["building", "h2"], + dependsOn: [h2BuildingCreateTest] +} +function h2BuildingReadOneTest() returns error? { + H2RainierClient rainierClient = check new (); + + Building buildingRetrieved = check rainierClient->/buildings/[building1.buildingCode].get(); + test:assertEquals(buildingRetrieved, building1); + check rainierClient.close(); +} + +@test:Config { + groups: ["building", "h2"], + dependsOn: [h2BuildingCreateTest] +} +function h2BuildingReadOneTestNegative() returns error? { + H2RainierClient rainierClient = check new (); + + Building|error buildingRetrieved = rainierClient->/buildings/["invalid-building-code"].get(); + if buildingRetrieved is persist:NotFoundError { + test:assertEquals(buildingRetrieved.message(), "A record with the key 'invalid-building-code' does not exist for the entity 'Building'."); + } else { + test:assertFail("persist:NotFoundError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["building", "h2"], + dependsOn: [h2BuildingCreateTest, h2BuildingCreateTest2] +} +function h2BuildingReadManyTest() returns error? { + H2RainierClient rainierClient = check new (); + + stream buildingStream = rainierClient->/buildings.get(); + Building[] buildings = check from Building building in buildingStream + select building; + + test:assertEquals(buildings, [building1, building2, building3]); + check rainierClient.close(); +} + +@test:Config { + groups: ["building", "h2", "dependent"], + dependsOn: [h2BuildingCreateTest, h2BuildingCreateTest2] +} +function h2BuildingReadManyDependentTest() returns error? { + H2RainierClient rainierClient = check new (); + + stream buildingStream = rainierClient->/buildings.get(); + BuildingInfo2[] buildings = check from BuildingInfo2 building in buildingStream + select building; + + test:assertEquals(buildings, [ + {city: building1.city, state: building1.state, country: building1.country, postalCode: building1.postalCode, 'type: building1.'type}, + {city: building2.city, state: building2.state, country: building2.country, postalCode: building2.postalCode, 'type: building2.'type}, + {city: building3.city, state: building3.state, country: building3.country, postalCode: building3.postalCode, 'type: building3.'type} + ]); + check rainierClient.close(); +} + +@test:Config { + groups: ["building", "h2"], + dependsOn: [h2BuildingReadOneTest, h2BuildingReadManyTest, h2BuildingReadManyDependentTest] +} +function h2BuildingUpdateTest() returns error? { + H2RainierClient rainierClient = check new (); + + Building building = check rainierClient->/buildings/[building1.buildingCode].put({ + city: "Galle", + state: "Southern Province", + postalCode: "10890", + 'type: "owned" + }); + + test:assertEquals(building, updatedBuilding1); + + Building buildingRetrieved = check rainierClient->/buildings/[building1.buildingCode].get(); + test:assertEquals(buildingRetrieved, updatedBuilding1); + check rainierClient.close(); +} + +@test:Config { + groups: ["building", "h2"], + dependsOn: [h2BuildingReadOneTest, h2BuildingReadManyTest, h2BuildingReadManyDependentTest] +} +function h2BuildingUpdateTestNegative1() returns error? { + H2RainierClient rainierClient = check new (); + + Building|error building = rainierClient->/buildings/["invalid-building-code"].put({ + city: "Galle", + state: "Southern Province", + postalCode: "10890" + }); + + if building is persist:NotFoundError { + test:assertEquals(building.message(), "A record with the key 'invalid-building-code' does not exist for the entity 'Building'."); + } else { + test:assertFail("persist:NotFoundError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["building", "h2"], + dependsOn: [h2BuildingReadOneTest, h2BuildingReadManyTest, h2BuildingReadManyDependentTest] +} +function h2BuildingUpdateTestNegative2() returns error? { + H2RainierClient rainierClient = check new (); + + Building|error building = rainierClient->/buildings/[building1.buildingCode].put({ + city: "unncessarily-long-city-name-to-force-error-on-update", + state: "Southern Province", + postalCode: "10890" + }); + + if building is persist:Error { + test:assertTrue(building.message().includes("unncessarily-long-city-name-to-force-error-on-update")); + } else { + test:assertFail("persist:NotFoundError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["building", "h2"], + dependsOn: [h2BuildingUpdateTest, h2BuildingUpdateTestNegative2] +} +function h2BuildingDeleteTest() returns error? { + H2RainierClient rainierClient = check new (); + + Building building = check rainierClient->/buildings/[building1.buildingCode].delete(); + test:assertEquals(building, updatedBuilding1); + + stream buildingStream = rainierClient->/buildings.get(); + Building[] buildings = check from Building building2 in buildingStream + select building2; + + test:assertEquals(buildings, [building2, building3]); + check rainierClient.close(); +} + +@test:Config { + groups: ["building", "h2"], + dependsOn: [h2BuildingDeleteTest] +} +function h2BuildingDeleteTestNegative() returns error? { + H2RainierClient rainierClient = check new (); + + Building|error building = rainierClient->/buildings/[building1.buildingCode].delete(); + + if building is error { + test:assertEquals(building.message(), string `A record with the key '${building1.buildingCode}' does not exist for the entity 'Building'.`); + } else { + test:assertFail("persist:NotFoundError expected."); + } + check rainierClient.close(); +} diff --git a/ballerina/tests/h2-composite-key-tests.bal b/ballerina/tests/h2-composite-key-tests.bal new file mode 100644 index 0000000..d7d316e --- /dev/null +++ b/ballerina/tests/h2-composite-key-tests.bal @@ -0,0 +1,196 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.org). +// +// 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: ["composite-key", "h2"] +} +function h2CompositeKeyCreateTest() returns error? { + H2RainierClient rainierClient = check new (); + + [string, string][] ids = check rainierClient->/orderitems.post([orderItem1, orderItem2]); + test:assertEquals(ids, [[orderItem1.orderId, orderItem1.itemId], [orderItem2.orderId, orderItem2.itemId]]); + + OrderItem orderItemRetrieved = check rainierClient->/orderitems/[orderItem1.orderId]/[orderItem1.itemId].get(); + test:assertEquals(orderItemRetrieved, orderItem1); + + orderItemRetrieved = check rainierClient->/orderitems/[orderItem2.orderId]/[orderItem2.itemId].get(); + test:assertEquals(orderItemRetrieved, orderItem2); + + check rainierClient.close(); +} + +@test:Config { + groups: ["composite-key", "h2"], + dependsOn: [h2CompositeKeyCreateTest] +} +function h2CompositeKeyCreateTestNegative() returns error? { + H2RainierClient rainierClient = check new (); + + [string, string][]|error ids = rainierClient->/orderitems.post([orderItem1]); + if ids is persist:AlreadyExistsError { + test:assertEquals(ids.message(), "A record with the key 'PUBLIC.PRIMARY_KEY_6' already exists for the entity 'OrderItem'."); + } else { + test:assertFail("persist:AlreadyExistsError expected"); + } + + check rainierClient.close(); +} + +@test:Config { + groups: ["composite-key", "h2"], + dependsOn: [h2CompositeKeyCreateTest] +} +function h2CompositeKeyReadManyTest() returns error? { + H2RainierClient rainierClient = check new (); + + stream orderItemStream = rainierClient->/orderitems.get(); + OrderItem[] orderitem = check from OrderItem orderItem in orderItemStream + select orderItem; + + test:assertEquals(orderitem, [orderItem1, orderItem2]); + check rainierClient.close(); +} + +@test:Config { + groups: ["composite-key", "h2"], + dependsOn: [h2CompositeKeyCreateTest] +} +function h2CompositeKeyReadOneTest() returns error? { + H2RainierClient rainierClient = check new (); + OrderItem orderItem = check rainierClient->/orderitems/[orderItem1.orderId]/[orderItem1.itemId].get(); + test:assertEquals(orderItem, orderItem1); + check rainierClient.close(); +} + +@test:Config { + groups: ["composite-key2"], + dependsOn: [h2CompositeKeyCreateTest] +} +function h2CompositeKeyReadOneTest2() returns error? { + H2RainierClient rainierClient = check new (); + OrderItem orderItem = check rainierClient->/orderitems/[orderItem1.orderId]/[orderItem1.itemId].get(); + test:assertEquals(orderItem, orderItem1); + check rainierClient.close(); +} + +@test:Config { + groups: ["composite-key", "h2"], + dependsOn: [h2CompositeKeyCreateTest] +} +function h2CompositeKeyReadOneTestNegative1() returns error? { + H2RainierClient rainierClient = check new (); + OrderItem|error orderItem = rainierClient->/orderitems/["invalid-order-id"]/[orderItem1.itemId].get(); + + if orderItem is persist:NotFoundError { + test:assertEquals(orderItem.message(), "A record with the key '{\"orderId\":\"invalid-order-id\",\"itemId\":\"item-1\"}' does not exist for the entity 'OrderItem'."); + } else { + test:assertFail("Error expected."); + } + + check rainierClient.close(); +} + +@test:Config { + groups: ["composite-key", "h2"], + dependsOn: [h2CompositeKeyCreateTest] +} +function h2CompositeKeyReadOneTestNegative2() returns error? { + H2RainierClient rainierClient = check new (); + OrderItem|error orderItem = rainierClient->/orderitems/[orderItem1.orderId]/["invalid-item-id"].get(); + + if orderItem is persist:NotFoundError { + test:assertEquals(orderItem.message(), "A record with the key '{\"orderId\":\"order-1\",\"itemId\":\"invalid-item-id\"}' does not exist for the entity 'OrderItem'."); + } else { + test:assertFail("Error expected."); + } + + check rainierClient.close(); +} + +@test:Config { + groups: ["composite-key", "h2"], + dependsOn: [h2CompositeKeyCreateTest, h2CompositeKeyReadOneTest, h2CompositeKeyReadManyTest, h2CompositeKeyReadOneTest2] +} +function h2CompositeKeyUpdateTest() returns error? { + H2RainierClient rainierClient = check new (); + + OrderItem orderItem = check rainierClient->/orderitems/[orderItem2.orderId]/[orderItem2.itemId].put({ + quantity: orderItem2Updated.quantity, + notes: orderItem2Updated.notes + }); + test:assertEquals(orderItem, orderItem2Updated); + + orderItem = check rainierClient->/orderitems/[orderItem2.orderId]/[orderItem2.itemId].get(); + test:assertEquals(orderItem, orderItem2Updated); + + check rainierClient.close(); +} + +@test:Config { + groups: ["composite-key", "h2"], + dependsOn: [h2CompositeKeyCreateTest, h2CompositeKeyReadOneTest, h2CompositeKeyReadManyTest, h2CompositeKeyReadOneTest2] +} +function h2CompositeKeyUpdateTestNegative() returns error? { + H2RainierClient rainierClient = check new (); + + OrderItem|error orderItem = rainierClient->/orderitems/[orderItem1.orderId]/[orderItem2.itemId].put({ + quantity: 239, + notes: "updated notes" + }); + if orderItem is persist:NotFoundError { + test:assertEquals(orderItem.message(), "A record with the key '{\"orderId\":\"order-1\",\"itemId\":\"item-2\"}' does not exist for the entity 'OrderItem'."); + } else { + test:assertFail("Error expected."); + } + + check rainierClient.close(); +} + +@test:Config { + groups: ["composite-key", "h2"], + dependsOn: [h2CompositeKeyUpdateTest] +} +function h2CompositeKeyDeleteTest() returns error? { + H2RainierClient rainierClient = check new (); + + OrderItem orderItem = check rainierClient->/orderitems/[orderItem2.orderId]/[orderItem2.itemId].delete(); + test:assertEquals(orderItem, orderItem2Updated); + + OrderItem|error orderItemRetrieved = rainierClient->/orderitems/[orderItem2.orderId]/[orderItem2.itemId].get(); + test:assertTrue(orderItemRetrieved is persist:NotFoundError); + + check rainierClient.close(); +} + +@test:Config { + groups: ["composite-key", "h2"], + dependsOn: [h2CompositeKeyUpdateTest] +} +function h2CompositeKeyDeleteTestNegative() returns error? { + H2RainierClient rainierClient = check new (); + + OrderItem|error orderItem = rainierClient->/orderitems/["invalid-order-id"]/[orderItem2.itemId].delete(); + if orderItem is persist:NotFoundError { + test:assertEquals(orderItem.message(), "A record with the key '{\"orderId\":\"invalid-order-id\",\"itemId\":\"item-2\"}' does not exist for the entity 'OrderItem'."); + } else { + test:assertFail("Error expected."); + } + + check rainierClient.close(); +} diff --git a/ballerina/tests/h2-department-tests.bal b/ballerina/tests/h2-department-tests.bal new file mode 100644 index 0000000..7903f4a --- /dev/null +++ b/ballerina/tests/h2-department-tests.bal @@ -0,0 +1,217 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.org). +// +// 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: ["department", "h2"] +} +function h2DepartmentCreateTest() returns error? { + H2RainierClient rainierClient = check new (); + + string[] deptNos = check rainierClient->/departments.post([department1]); + test:assertEquals(deptNos, [department1.deptNo]); + + Department departmentRetrieved = check rainierClient->/departments/[department1.deptNo].get(); + test:assertEquals(departmentRetrieved, department1); + check rainierClient.close(); +} + +@test:Config { + groups: ["department", "h2"] +} +function h2DepartmentCreateTest2() returns error? { + H2RainierClient rainierClient = check new (); + + string[] deptNos = check rainierClient->/departments.post([department2, department3]); + + test:assertEquals(deptNos, [department2.deptNo, department3.deptNo]); + + Department departmentRetrieved = check rainierClient->/departments/[department2.deptNo].get(); + test:assertEquals(departmentRetrieved, department2); + + departmentRetrieved = check rainierClient->/departments/[department3.deptNo].get(); + test:assertEquals(departmentRetrieved, department3); + check rainierClient.close(); +} + +@test:Config { + groups: ["department", "h2"] +} +function h2DepartmentCreateTestNegative() returns error? { + H2RainierClient rainierClient = check new (); + + string[]|error department = rainierClient->/departments.post([invalidDepartment]); + if department is persist:Error { + test:assertTrue(department.message().includes(".Value too long for column \"deptNo CHARACTER VARYING(36)")); + } else { + test:assertFail("Error expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["department", "h2"], + dependsOn: [h2DepartmentCreateTest] +} +function h2DepartmentReadOneTest() returns error? { + H2RainierClient rainierClient = check new (); + + Department departmentRetrieved = check rainierClient->/departments/[department1.deptNo].get(); + test:assertEquals(departmentRetrieved, department1); + check rainierClient.close(); +} + +@test:Config { + groups: ["department", "h2"], + dependsOn: [h2DepartmentCreateTest] +} +function h2DepartmentReadOneTestNegative() returns error? { + H2RainierClient rainierClient = check new (); + + Department|error departmentRetrieved = rainierClient->/departments/["invalid-department-id"].get(); + if departmentRetrieved is persist:NotFoundError { + test:assertEquals(departmentRetrieved.message(), "A record with the key 'invalid-department-id' does not exist for the entity 'Department'."); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["department", "h2"], + dependsOn: [h2DepartmentCreateTest, h2DepartmentCreateTest2] +} +function h2DepartmentReadManyTest() returns error? { + H2RainierClient rainierClient = check new (); + stream departmentStream = rainierClient->/departments.get(); + Department[] departments = check from Department department in departmentStream + select department; + + test:assertEquals(departments, [department1, department2, department3]); + check rainierClient.close(); +} + +@test:Config { + groups: ["department", "h2", "dependent"], + dependsOn: [h2DepartmentCreateTest, h2DepartmentCreateTest2] +} +function h2DepartmentReadManyTestDependent() returns error? { + H2RainierClient rainierClient = check new (); + + stream departmentStream = rainierClient->/departments.get(); + DepartmentInfo2[] departments = check from DepartmentInfo2 department in departmentStream + select department; + + test:assertEquals(departments, [ + {deptName: department1.deptName}, + {deptName: department2.deptName}, + {deptName: department3.deptName} + ]); + check rainierClient.close(); +} + +@test:Config { + groups: ["department", "h2"], + dependsOn: [h2DepartmentReadOneTest, h2DepartmentReadManyTest, h2DepartmentReadManyTestDependent] +} +function h2DepartmentUpdateTest() returns error? { + H2RainierClient rainierClient = check new (); + + Department department = check rainierClient->/departments/[department1.deptNo].put({ + deptName: "Finance & Legalities" + }); + + test:assertEquals(department, updatedDepartment1); + + Department departmentRetrieved = check rainierClient->/departments/[department1.deptNo].get(); + test:assertEquals(departmentRetrieved, updatedDepartment1); + check rainierClient.close(); +} + +@test:Config { + groups: ["department", "h2"], + dependsOn: [h2DepartmentReadOneTest, h2DepartmentReadManyTest, h2DepartmentReadManyTestDependent] +} +function h2DepartmentUpdateTestNegative1() returns error? { + H2RainierClient rainierClient = check new (); + + Department|error department = rainierClient->/departments/["invalid-department-id"].put({ + deptName: "Human Resources" + }); + + if department is persist:NotFoundError { + test:assertEquals(department.message(), "A record with the key 'invalid-department-id' does not exist for the entity 'Department'."); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["department", "h2"], + dependsOn: [h2DepartmentReadOneTest, h2DepartmentReadManyTest, h2DepartmentReadManyTestDependent] +} +function h2DepartmentUpdateTestNegative2() returns error? { + H2RainierClient rainierClient = check new (); + + Department|error department = rainierClient->/departments/[department1.deptNo].put({ + deptName: "unncessarily-long-department-name-to-force-error-on-update" + }); + + if department is persist:Error { + test:assertTrue(department.message().includes("Value too long for column \"deptName CHARACTER VARYING(30)")); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["department", "h2"], + dependsOn: [h2DepartmentUpdateTest, h2DepartmentUpdateTestNegative2] +} +function h2DepartmentDeleteTest() returns error? { + H2RainierClient rainierClient = check new (); + + Department department = check rainierClient->/departments/[department1.deptNo].delete(); + test:assertEquals(department, updatedDepartment1); + + stream departmentStream = rainierClient->/departments.get(); + Department[] departments = check from Department department2 in departmentStream + select department2; + + test:assertEquals(departments, [department2, department3]); + check rainierClient.close(); +} + +@test:Config { + groups: ["department", "h2"], + dependsOn: [h2DepartmentDeleteTest] +} +function h2DepartmentDeleteTestNegative() returns error? { + H2RainierClient rainierClient = check new (); + + Department|error department = rainierClient->/departments/[department1.deptNo].delete(); + + if department is persist:NotFoundError { + test:assertEquals(department.message(), string `A record with the key '${department1.deptNo}' does not exist for the entity 'Department'.`); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} diff --git a/ballerina/tests/h2-employee-tests.bal b/ballerina/tests/h2-employee-tests.bal new file mode 100644 index 0000000..3e7de65 --- /dev/null +++ b/ballerina/tests/h2-employee-tests.bal @@ -0,0 +1,260 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.org). +// +// 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: ["employee", "h2"], + dependsOn: [h2WorkspaceDeleteTestNegative, h2DepartmentDeleteTestNegative] +} +function h2EmployeeCreateTest() returns error? { + H2RainierClient rainierClient = check new (); + + string[] empNos = check rainierClient->/employees.post([employee1]); + test:assertEquals(empNos, [employee1.empNo]); + + Employee employeeRetrieved = check rainierClient->/employees/[employee1.empNo].get(); + test:assertEquals(employeeRetrieved, employee1); + check rainierClient.close(); +} + +@test:Config { + groups: ["employee", "h2"], + dependsOn: [h2WorkspaceDeleteTestNegative, h2DepartmentDeleteTestNegative] +} +function h2EmployeeCreateTest2() returns error? { + H2RainierClient rainierClient = check new (); + + string[] empNos = check rainierClient->/employees.post([employee2, employee3]); + + test:assertEquals(empNos, [employee2.empNo, employee3.empNo]); + + Employee employeeRetrieved = check rainierClient->/employees/[employee2.empNo].get(); + test:assertEquals(employeeRetrieved, employee2); + + employeeRetrieved = check rainierClient->/employees/[employee3.empNo].get(); + test:assertEquals(employeeRetrieved, employee3); + check rainierClient.close(); +} + +@test:Config { + groups: ["employee", "h2"] +} +function h2EmployeeCreateTestNegative() returns error? { + H2RainierClient rainierClient = check new (); + + string[]|error employee = rainierClient->/employees.post([invalidEmployee]); + if employee is persist:Error { + test:assertTrue(employee.message().includes("Value too long for column \"empNo CHARACTER VARYING(36)")); + } else { + test:assertFail("Error expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["employee", "h2"], + dependsOn: [h2EmployeeCreateTest] +} +function h2EmployeeReadOneTest() returns error? { + H2RainierClient rainierClient = check new (); + + Employee employeeRetrieved = check rainierClient->/employees/[employee1.empNo].get(); + test:assertEquals(employeeRetrieved, employee1); + check rainierClient.close(); +} + +@test:Config { + groups: ["employee", "h2"], + dependsOn: [h2EmployeeCreateTest] +} +function h2EmployeeReadOneTestNegative() returns error? { + H2RainierClient rainierClient = check new (); + + Employee|error employeeRetrieved = rainierClient->/employees/["invalid-employee-id"].get(); + if employeeRetrieved is persist:NotFoundError { + test:assertEquals(employeeRetrieved.message(), "A record with the key 'invalid-employee-id' does not exist for the entity 'Employee'."); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["employee", "h2"], + dependsOn: [h2EmployeeCreateTest, h2EmployeeCreateTest2] +} +function h2EmployeeReadManyTest() returns error? { + H2RainierClient rainierClient = check new (); + + stream employeeStream = rainierClient->/employees.get(); + Employee[] employees = check from Employee employee in employeeStream + select employee; + + test:assertEquals(employees, [employee1, employee2, employee3]); + check rainierClient.close(); +} + +@test:Config { + groups: ["dependent", "employee"], + dependsOn: [h2EmployeeCreateTest, h2EmployeeCreateTest2] +} +function h2EmployeeReadManyDependentTest1() returns error? { + H2RainierClient rainierClient = check new (); + + stream employeeStream = rainierClient->/employees.get(); + EmployeeName[] employees = check from EmployeeName employee in employeeStream + select employee; + + test:assertEquals(employees, [ + {firstName: employee1.firstName, lastName: employee1.lastName}, + {firstName: employee2.firstName, lastName: employee2.lastName}, + {firstName: employee3.firstName, lastName: employee3.lastName} + ]); + check rainierClient.close(); +} + +@test:Config { + groups: ["dependent", "employee"], + dependsOn: [h2EmployeeCreateTest, h2EmployeeCreateTest2] +} +function h2EmployeeReadManyDependentTest2() returns error? { + H2RainierClient rainierClient = check new (); + + stream employeeStream = rainierClient->/employees.get(); + EmployeeInfo2[] employees = check from EmployeeInfo2 employee in employeeStream + select employee; + + test:assertEquals(employees, [ + {empNo: employee1.empNo, birthDate: employee1.birthDate, departmentDeptNo: employee1.departmentDeptNo, workspaceWorkspaceId: employee1.workspaceWorkspaceId}, + {empNo: employee2.empNo, birthDate: employee2.birthDate, departmentDeptNo: employee2.departmentDeptNo, workspaceWorkspaceId: employee2.workspaceWorkspaceId}, + {empNo: employee3.empNo, birthDate: employee3.birthDate, departmentDeptNo: employee3.departmentDeptNo, workspaceWorkspaceId: employee3.workspaceWorkspaceId} + ]); + check rainierClient.close(); +} + +@test:Config { + groups: ["employee", "h2"], + dependsOn: [h2EmployeeReadOneTest, h2EmployeeReadManyTest, h2EmployeeReadManyDependentTest1, h2EmployeeReadManyDependentTest2] +} +function h2EmployeeUpdateTest() returns error? { + H2RainierClient rainierClient = check new (); + + Employee employee = check rainierClient->/employees/[employee1.empNo].put({ + lastName: "Jones", + departmentDeptNo: "department-3", + birthDate: {year: 1994, month: 11, day: 13} + }); + + test:assertEquals(employee, updatedEmployee1); + + Employee employeeRetrieved = check rainierClient->/employees/[employee1.empNo].get(); + test:assertEquals(employeeRetrieved, updatedEmployee1); + check rainierClient.close(); +} + +@test:Config { + groups: ["employee", "h2"], + dependsOn: [h2EmployeeReadOneTest, h2EmployeeReadManyTest, h2EmployeeReadManyDependentTest1, h2EmployeeReadManyDependentTest2] +} +function h2EmployeeUpdateTestNegative1() returns error? { + H2RainierClient rainierClient = check new (); + + Employee|error employee = rainierClient->/employees/["invalid-employee-id"].put({ + lastName: "Jones" + }); + + if employee is persist:NotFoundError { + test:assertEquals(employee.message(), "A record with the key 'invalid-employee-id' does not exist for the entity 'Employee'."); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["employee", "h2"], + dependsOn: [h2EmployeeReadOneTest, h2EmployeeReadManyTest, h2EmployeeReadManyDependentTest1, h2EmployeeReadManyDependentTest2] +} +function h2EmployeeUpdateTestNegative2() returns error? { + H2RainierClient rainierClient = check new (); + + Employee|error employee = rainierClient->/employees/[employee1.empNo].put({ + firstName: "unncessarily-long-employee-name-to-force-error-on-update" + }); + + if employee is persist:Error { + test:assertTrue(employee.message().includes("Value too long for column \"firstName CHARACTER VARYING(30)")); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["employee", "h2"], + dependsOn: [h2EmployeeReadOneTest, h2EmployeeReadManyTest, h2EmployeeReadManyDependentTest1, h2EmployeeReadManyDependentTest2] +} +function h2EmployeeUpdateTestNegative3() returns error? { + H2RainierClient rainierClient = check new (); + + Employee|error employee = rainierClient->/employees/[employee1.empNo].put({ + workspaceWorkspaceId: "invalid-workspaceWorkspaceId" + }); + + if employee is persist:ConstraintViolationError { + test:assertTrue(employee.message().includes("Referential integrity constraint violation: \"CONSTRAINT_4AFD: PUBLIC.Employee FOREIGN KEY(workspaceWorkspaceId) REFERENCES PUBLIC.Workspace(workspaceId)")); + } else { + test:assertFail("persist:persist:ConstraintViolationError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["employee", "h2"], + dependsOn: [h2EmployeeUpdateTest, h2EmployeeUpdateTestNegative2, h2EmployeeUpdateTestNegative3] +} +function h2EmployeeDeleteTest() returns error? { + H2RainierClient rainierClient = check new (); + + Employee employee = check rainierClient->/employees/[employee1.empNo].delete(); + test:assertEquals(employee, updatedEmployee1); + + stream employeeStream = rainierClient->/employees.get(); + Employee[] employees = check from Employee employee2 in employeeStream + select employee2; + + test:assertEquals(employees, [employee2, employee3]); + check rainierClient.close(); +} + +@test:Config { + groups: ["employee", "h2"], + dependsOn: [h2EmployeeDeleteTest] +} +function h2EmployeeDeleteTestNegative() returns error? { + H2RainierClient rainierClient = check new (); + + Employee|error employee = rainierClient->/employees/[employee1.empNo].delete(); + + if employee is persist:NotFoundError { + test:assertEquals(employee.message(), string `A record with the key '${employee1.empNo}' does not exist for the entity 'Employee'.`); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} diff --git a/ballerina/tests/h2-id-fields-tests.bal b/ballerina/tests/h2-id-fields-tests.bal new file mode 100644 index 0000000..6f4c5ea --- /dev/null +++ b/ballerina/tests/h2-id-fields-tests.bal @@ -0,0 +1,476 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.org). +// +// 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; + +@test:Config { + groups: ["id-fields", "h2"] +} +function h2IntIdFieldTest() returns error? { + H2TestEntitiesClient testEntitiesClient = check new (); + IntIdRecord intIdRecord1 = { + id: 1, + randomField: "test1" + }; + IntIdRecord intIdRecord2 = { + id: 2, + randomField: "test2" + }; + IntIdRecord intIdRecord3 = { + id: 3, + randomField: "test3" + }; + IntIdRecord intIdRecord1Updated = { + id: 1, + randomField: "test1Updated" + }; + + // create + int[] ids = check testEntitiesClient->/intidrecords.post([intIdRecord1, intIdRecord2, intIdRecord3]); + test:assertEquals(ids, [intIdRecord1.id, intIdRecord2.id, intIdRecord3.id]); + + // read one + IntIdRecord retrievedRecord1 = check testEntitiesClient->/intidrecords/[intIdRecord1.id].get(); + test:assertEquals(intIdRecord1, retrievedRecord1); + + // read one dependent + IntIdRecordDependent retrievedRecord1Dependent = check testEntitiesClient->/intidrecords/[intIdRecord1.id].get(); + test:assertEquals({randomField: intIdRecord1.randomField}, retrievedRecord1Dependent); + + // read + IntIdRecord[] intIdRecords = check from IntIdRecord intIdRecord in testEntitiesClient->/intidrecords.get(IntIdRecord) + select intIdRecord; + test:assertEquals(intIdRecords, [intIdRecord1, intIdRecord2, intIdRecord3]); + + // read dependent + IntIdRecordDependent[] intIdRecordsDependent = check from IntIdRecordDependent intIdRecord in testEntitiesClient->/intidrecords.get(IntIdRecordDependent) + select intIdRecord; + test:assertEquals(intIdRecordsDependent, [{randomField: intIdRecord1.randomField}, {randomField: intIdRecord2.randomField}, {randomField: intIdRecord3.randomField}]); + + // update + retrievedRecord1 = check testEntitiesClient->/intidrecords/[intIdRecord1.id].put({randomField: intIdRecord1Updated.randomField}); + test:assertEquals(intIdRecord1Updated, retrievedRecord1); + retrievedRecord1 = check testEntitiesClient->/intidrecords/[intIdRecord1.id].get(); + test:assertEquals(intIdRecord1Updated, retrievedRecord1); + + // delete + IntIdRecord retrievedRecord2 = check testEntitiesClient->/intidrecords/[intIdRecord2.id].delete(); + test:assertEquals(intIdRecord2, retrievedRecord2); + intIdRecords = check from IntIdRecord intIdRecord in testEntitiesClient->/intidrecords.get(IntIdRecord) + select intIdRecord; + test:assertEquals(intIdRecords, [intIdRecord1Updated, intIdRecord3]); + + check testEntitiesClient.close(); +} + +@test:Config { + groups: ["id-fields", "h2"] +} +function h2StringIdFieldTest() returns error? { + H2TestEntitiesClient testEntitiesClient = check new (); + StringIdRecord stringIdRecord1 = { + id: "id-1", + randomField: "test1" + }; + StringIdRecord stringIdRecord2 = { + id: "id-2", + randomField: "test2" + }; + StringIdRecord stringIdRecord3 = { + id: "id-3", + randomField: "test3" + }; + StringIdRecord stringIdRecord1Updated = { + id: "id-1", + randomField: "test1Updated" + }; + + // create + string[] ids = check testEntitiesClient->/stringidrecords.post([stringIdRecord1, stringIdRecord2, stringIdRecord3]); + test:assertEquals(ids, [stringIdRecord1.id, stringIdRecord2.id, stringIdRecord3.id]); + + // read one + StringIdRecord retrievedRecord1 = check testEntitiesClient->/stringidrecords/[stringIdRecord1.id].get(); + test:assertEquals(stringIdRecord1, retrievedRecord1); + + // read one dependent + StringIdRecordDependent retrievedRecord1Dependent = check testEntitiesClient->/stringidrecords/[stringIdRecord1.id].get(); + test:assertEquals({randomField: stringIdRecord1.randomField}, retrievedRecord1Dependent); + + // read + StringIdRecord[] stringIdRecords = check from StringIdRecord stringIdRecord in testEntitiesClient->/stringidrecords.get(StringIdRecord) + select stringIdRecord; + test:assertEquals(stringIdRecords, [stringIdRecord1, stringIdRecord2, stringIdRecord3]); + + // read dependent + StringIdRecordDependent[] stringIdRecordsDependent = check from StringIdRecordDependent stringIdRecord in testEntitiesClient->/stringidrecords.get(StringIdRecordDependent) + select stringIdRecord; + test:assertEquals(stringIdRecordsDependent, [{randomField: stringIdRecord1.randomField}, {randomField: stringIdRecord2.randomField}, {randomField: stringIdRecord3.randomField}]); + + // update + retrievedRecord1 = check testEntitiesClient->/stringidrecords/[stringIdRecord1.id].put({randomField: stringIdRecord1Updated.randomField}); + test:assertEquals(stringIdRecord1Updated, retrievedRecord1); + retrievedRecord1 = check testEntitiesClient->/stringidrecords/[stringIdRecord1.id].get(); + test:assertEquals(stringIdRecord1Updated, retrievedRecord1); + + // delete + StringIdRecord retrievedRecord2 = check testEntitiesClient->/stringidrecords/[stringIdRecord2.id].delete(); + test:assertEquals(stringIdRecord2, retrievedRecord2); + stringIdRecords = check from StringIdRecord stringIdRecord in testEntitiesClient->/stringidrecords.get(StringIdRecord) + select stringIdRecord; + test:assertEquals(stringIdRecords, [stringIdRecord1Updated, stringIdRecord3]); + + check testEntitiesClient.close(); +} + +@test:Config { + groups: ["id-fields", "h2"] +} +function h2FloatIdFieldTest() returns error? { + H2TestEntitiesClient testEntitiesClient = check new (); + FloatIdRecord floatIdRecord1 = { + id: 1.0, + randomField: "test1" + }; + FloatIdRecord floatIdRecord2 = { + id: 2.0, + randomField: "test2" + }; + FloatIdRecord floatIdRecord3 = { + id: 3.0, + randomField: "test3" + }; + FloatIdRecord floatIdRecord1Updated = { + id: 1.0, + randomField: "test1Updated" + }; + + // create + float[] ids = check testEntitiesClient->/floatidrecords.post([floatIdRecord1, floatIdRecord2, floatIdRecord3]); + test:assertEquals(ids, [floatIdRecord1.id, floatIdRecord2.id, floatIdRecord3.id]); + + // read one + FloatIdRecord retrievedRecord1 = check testEntitiesClient->/floatidrecords/[floatIdRecord1.id].get(); + test:assertEquals(floatIdRecord1, retrievedRecord1); + + // read one dependent + FloatIdRecordDependent retrievedRecord1Dependent = check testEntitiesClient->/floatidrecords/[floatIdRecord1.id].get(); + test:assertEquals({randomField: floatIdRecord1.randomField}, retrievedRecord1Dependent); + + // read + FloatIdRecord[] floatIdRecords = check from FloatIdRecord floatIdRecord in testEntitiesClient->/floatidrecords.get(FloatIdRecord) + select floatIdRecord; + test:assertEquals(floatIdRecords, [floatIdRecord1, floatIdRecord2, floatIdRecord3]); + + // read dependent + FloatIdRecordDependent[] floatIdRecordsDependent = check from FloatIdRecordDependent floatIdRecord in testEntitiesClient->/floatidrecords.get(FloatIdRecordDependent) + select floatIdRecord; + test:assertEquals(floatIdRecordsDependent, [{randomField: floatIdRecord1.randomField}, {randomField: floatIdRecord2.randomField}, {randomField: floatIdRecord3.randomField}]); + + // update + retrievedRecord1 = check testEntitiesClient->/floatidrecords/[floatIdRecord1.id].put({randomField: floatIdRecord1Updated.randomField}); + test:assertEquals(floatIdRecord1Updated, retrievedRecord1); + retrievedRecord1 = check testEntitiesClient->/floatidrecords/[floatIdRecord1.id].get(); + test:assertEquals(floatIdRecord1Updated, retrievedRecord1); + + // delete + FloatIdRecord retrievedRecord2 = check testEntitiesClient->/floatidrecords/[floatIdRecord2.id].delete(); + test:assertEquals(floatIdRecord2, retrievedRecord2); + floatIdRecords = check from FloatIdRecord floatIdRecord in testEntitiesClient->/floatidrecords.get(FloatIdRecord) + select floatIdRecord; + test:assertEquals(floatIdRecords, [floatIdRecord1Updated, floatIdRecord3]); +} + +@test:Config { + groups: ["id-fields", "h2"] +} +function h2DecimalIdFieldTest() returns error? { + H2TestEntitiesClient testEntitiesClient = check new (); + DecimalIdRecord decimalIdRecord1 = { + id: 1.1d, + randomField: "test1" + }; + DecimalIdRecord decimalIdRecord2 = { + id: 2.2d, + randomField: "test2" + }; + DecimalIdRecord decimalIdRecord3 = { + id: 3.3d, + randomField: "test3" + }; + DecimalIdRecord decimalIdRecord1Updated = { + id: 1.1d, + randomField: "test1Updated" + }; + + // create + decimal[] ids = check testEntitiesClient->/decimalidrecords.post([decimalIdRecord1, decimalIdRecord2, decimalIdRecord3]); + test:assertEquals(ids, [decimalIdRecord1.id, decimalIdRecord2.id, decimalIdRecord3.id]); + + // read one + DecimalIdRecord retrievedRecord1 = check testEntitiesClient->/decimalidrecords/[decimalIdRecord1.id].get(); + test:assertEquals(decimalIdRecord1, retrievedRecord1); + + // read one dependent + DecimalIdRecordDependent retrievedRecord1Dependent = check testEntitiesClient->/decimalidrecords/[decimalIdRecord1.id].get(); + test:assertEquals({randomField: decimalIdRecord1.randomField}, retrievedRecord1Dependent); + + // read + DecimalIdRecord[] decimalIdRecords = check from DecimalIdRecord decimalIdRecord in testEntitiesClient->/decimalidrecords.get(DecimalIdRecord) + select decimalIdRecord; + test:assertEquals(decimalIdRecords, [decimalIdRecord1, decimalIdRecord2, decimalIdRecord3]); + + // read dependent + DecimalIdRecordDependent[] decimalIdRecordsDependent = check from DecimalIdRecordDependent decimalIdRecord in testEntitiesClient->/decimalidrecords.get(DecimalIdRecordDependent) + select decimalIdRecord; + test:assertEquals(decimalIdRecordsDependent, [{randomField: decimalIdRecord1.randomField}, {randomField: decimalIdRecord2.randomField}, {randomField: decimalIdRecord3.randomField}]); + + // update + retrievedRecord1 = check testEntitiesClient->/decimalidrecords/[decimalIdRecord1.id].put({randomField: decimalIdRecord1Updated.randomField}); + test:assertEquals(decimalIdRecord1Updated, retrievedRecord1); + retrievedRecord1 = check testEntitiesClient->/decimalidrecords/[decimalIdRecord1.id].get(); + test:assertEquals(decimalIdRecord1Updated, retrievedRecord1); + + // delete + DecimalIdRecord retrievedRecord2 = check testEntitiesClient->/decimalidrecords/[decimalIdRecord2.id].delete(); + test:assertEquals(decimalIdRecord2, retrievedRecord2); + decimalIdRecords = check from DecimalIdRecord decimalIdRecord in testEntitiesClient->/decimalidrecords.get(DecimalIdRecord) + select decimalIdRecord; + test:assertEquals(decimalIdRecords, [decimalIdRecord1Updated, decimalIdRecord3]); + + check testEntitiesClient.close(); +} + +@test:Config { + groups: ["id-fields", "h2"] +} +function h2BooleanIdFieldTest() returns error? { + H2TestEntitiesClient testEntitiesClient = check new (); + BooleanIdRecord booleanIdRecord1 = { + id: true, + randomField: "test1" + }; + BooleanIdRecord booleanIdRecord2 = { + id: false, + randomField: "test2" + }; + BooleanIdRecord booleanIdRecord1Updated = { + id: true, + randomField: "test1Updated" + }; + + // create + boolean[] ids = check testEntitiesClient->/booleanidrecords.post([booleanIdRecord1, booleanIdRecord2]); + test:assertEquals(ids, [booleanIdRecord1.id, booleanIdRecord2.id]); + + // read one + BooleanIdRecord retrievedRecord1 = check testEntitiesClient->/booleanidrecords/[booleanIdRecord1.id].get(); + test:assertEquals(booleanIdRecord1, retrievedRecord1); + + // read one dependent + BooleanIdRecordDependent retrievedRecord1Dependent = check testEntitiesClient->/booleanidrecords/[booleanIdRecord1.id].get(); + test:assertEquals({randomField: booleanIdRecord1.randomField}, retrievedRecord1Dependent); + + // read + BooleanIdRecord[] booleanIdRecords = check from BooleanIdRecord booleanIdRecord in testEntitiesClient->/booleanidrecords.get(BooleanIdRecord) + select booleanIdRecord; + test:assertEquals(booleanIdRecords, [booleanIdRecord1, booleanIdRecord2]); + + // read dependent + BooleanIdRecordDependent[] booleanIdRecordsDependent = check from BooleanIdRecordDependent booleanIdRecord in testEntitiesClient->/booleanidrecords.get(BooleanIdRecordDependent) + select booleanIdRecord; + test:assertEquals(booleanIdRecordsDependent, [{randomField: booleanIdRecord1.randomField}, {randomField: booleanIdRecord2.randomField}]); + + // update + retrievedRecord1 = check testEntitiesClient->/booleanidrecords/[booleanIdRecord1.id].put({randomField: booleanIdRecord1Updated.randomField}); + test:assertEquals(booleanIdRecord1Updated, retrievedRecord1); + retrievedRecord1 = check testEntitiesClient->/booleanidrecords/[booleanIdRecord1.id].get(); + test:assertEquals(booleanIdRecord1Updated, retrievedRecord1); + + // delete + BooleanIdRecord retrievedRecord2 = check testEntitiesClient->/booleanidrecords/[booleanIdRecord2.id].delete(); + test:assertEquals(booleanIdRecord2, retrievedRecord2); + booleanIdRecords = check from BooleanIdRecord booleanIdRecord in testEntitiesClient->/booleanidrecords.get(BooleanIdRecord) + select booleanIdRecord; + test:assertEquals(booleanIdRecords, [booleanIdRecord1Updated]); + + check testEntitiesClient.close(); +} + +@test:Config { + groups: ["id-fields", "h2"] +} +function h2AllTypesIdFieldTest() returns error? { + H2TestEntitiesClient testEntitiesClient = check new (); + AllTypesIdRecord allTypesIdRecord1 = { + intType: 1, + stringType: "id-1", + floatType: 1.0, + booleanType: true, + decimalType: 1.1d, + randomField: "test1" + }; + AllTypesIdRecord allTypesIdRecord2 = { + intType: 2, + stringType: "id-2", + floatType: 2.0, + booleanType: false, + decimalType: 2.2d, + randomField: "test2" + }; + AllTypesIdRecord allTypesIdRecord1Updated = { + intType: 1, + stringType: "id-1", + floatType: 1.0, + booleanType: true, + decimalType: 1.1d, + randomField: "test1Updated" + }; + + // create + [boolean, int, float, decimal, string][] ids = check testEntitiesClient->/alltypesidrecords.post([allTypesIdRecord1, allTypesIdRecord2]); + test:assertEquals(ids, [ + [allTypesIdRecord1.booleanType, allTypesIdRecord1.intType, allTypesIdRecord1.floatType, allTypesIdRecord1.decimalType, allTypesIdRecord1.stringType], + [allTypesIdRecord2.booleanType, allTypesIdRecord2.intType, allTypesIdRecord2.floatType, allTypesIdRecord2.decimalType, allTypesIdRecord2.stringType] + ]); + + // read one + AllTypesIdRecord retrievedRecord1 = check testEntitiesClient->/alltypesidrecords/[allTypesIdRecord1.booleanType]/[allTypesIdRecord1.intType]/[allTypesIdRecord1.floatType]/[allTypesIdRecord1.decimalType]/[allTypesIdRecord1.stringType].get(); + test:assertEquals(allTypesIdRecord1, retrievedRecord1); + + // read one dependent + AllTypesIdRecordDependent retrievedRecord1Dependent = check testEntitiesClient->/alltypesidrecords/[allTypesIdRecord1.booleanType]/[allTypesIdRecord1.intType]/[allTypesIdRecord1.floatType]/[allTypesIdRecord1.decimalType]/[allTypesIdRecord1.stringType].get(); + test:assertEquals({randomField: allTypesIdRecord1.randomField}, retrievedRecord1Dependent); + + // read + AllTypesIdRecord[] allTypesIdRecords = check from AllTypesIdRecord allTypesIdRecord in testEntitiesClient->/alltypesidrecords.get(AllTypesIdRecord) + order by allTypesIdRecord.intType + select allTypesIdRecord; + test:assertEquals(allTypesIdRecords, [allTypesIdRecord1, allTypesIdRecord2]); + + // read dependent + AllTypesIdRecordDependent[] allTypesIdRecordsDependent = check from AllTypesIdRecordDependent allTypesIdRecord in testEntitiesClient->/alltypesidrecords.get(AllTypesIdRecordDependent) + select allTypesIdRecord; + test:assertEquals(allTypesIdRecordsDependent, [{randomField: allTypesIdRecord1.randomField}, {randomField: allTypesIdRecord2.randomField}]); + + // update + retrievedRecord1 = check testEntitiesClient->/alltypesidrecords/[allTypesIdRecord1.booleanType]/[allTypesIdRecord1.intType]/[allTypesIdRecord1.floatType]/[allTypesIdRecord1.decimalType]/[allTypesIdRecord1.stringType].put({randomField: allTypesIdRecord1Updated.randomField}); + test:assertEquals(allTypesIdRecord1Updated, retrievedRecord1); + retrievedRecord1 = check testEntitiesClient->/alltypesidrecords/[allTypesIdRecord1.booleanType]/[allTypesIdRecord1.intType]/[allTypesIdRecord1.floatType]/[allTypesIdRecord1.decimalType]/[allTypesIdRecord1.stringType].get(); + test:assertEquals(allTypesIdRecord1Updated, retrievedRecord1); + + // delete + AllTypesIdRecord retrievedRecord2 = check testEntitiesClient->/alltypesidrecords/[allTypesIdRecord2.booleanType]/[allTypesIdRecord2.intType]/[allTypesIdRecord2.floatType]/[allTypesIdRecord2.decimalType]/[allTypesIdRecord2.stringType].delete(); + test:assertEquals(allTypesIdRecord2, retrievedRecord2); + allTypesIdRecords = check from AllTypesIdRecord allTypesIdRecord in testEntitiesClient->/alltypesidrecords.get(AllTypesIdRecord) + order by allTypesIdRecord.intType + select allTypesIdRecord; + test:assertEquals(allTypesIdRecords, [allTypesIdRecord1Updated]); + + check testEntitiesClient.close(); +} + +@test:Config { + groups: ["id-fields", "h2", "associations"], + dependsOn: [h2AllTypesIdFieldTest] +} +function h2CompositeAssociationsTest() returns error? { + H2TestEntitiesClient testEntitiesClient = check new (); + + CompositeAssociationRecord compositeAssociationRecord1 = { + id: "id-1", + randomField: "test1", + alltypesidrecordIntType: 1, + alltypesidrecordStringType: "id-1", + alltypesidrecordFloatType: 1.0, + alltypesidrecordBooleanType: true, + alltypesidrecordDecimalType: 1.10 + }; + + CompositeAssociationRecord compositeAssociationRecord2 = { + id: "id-2", + randomField: "test2", + alltypesidrecordIntType: 1, + alltypesidrecordStringType: "id-1", + alltypesidrecordFloatType: 1.0, + alltypesidrecordBooleanType: true, + alltypesidrecordDecimalType: 1.10 + }; + + CompositeAssociationRecord compositeAssociationRecordUpdated1 = { + id: "id-1", + randomField: "test1Updated", + alltypesidrecordIntType: 1, + alltypesidrecordStringType: "id-1", + alltypesidrecordFloatType: 1.0, + alltypesidrecordBooleanType: true, + alltypesidrecordDecimalType: 1.10 + }; + + AllTypesIdRecordOptionalized allTypesIdRecord1 = { + intType: 1, + stringType: "id-1", + floatType: 1.0, + booleanType: true, + decimalType: 1.10, + randomField: "test1Updated" + }; + + // create + string[] ids = check testEntitiesClient->/compositeassociationrecords.post([compositeAssociationRecord1, compositeAssociationRecord2]); + test:assertEquals(ids, [compositeAssociationRecord1.id, compositeAssociationRecord2.id]); + + // read one + CompositeAssociationRecord retrievedRecord1 = check testEntitiesClient->/compositeassociationrecords/[compositeAssociationRecord1.id].get(); + test:assertEquals(compositeAssociationRecord1, retrievedRecord1); + + // read one dependent + CompositeAssociationRecordDependent retrievedRecord1Dependent = check testEntitiesClient->/compositeassociationrecords/[compositeAssociationRecord1.id].get(); + test:assertEquals({ + randomField: compositeAssociationRecord1.randomField, + alltypesidrecordIntType: compositeAssociationRecord1.alltypesidrecordIntType, + alltypesidrecordDecimalType: compositeAssociationRecord1.alltypesidrecordDecimalType, + allTypesIdRecord: {intType: allTypesIdRecord1.intType, stringType: allTypesIdRecord1.stringType, booleanType: allTypesIdRecord1.booleanType, randomField: allTypesIdRecord1.randomField} + }, retrievedRecord1Dependent); + + // read + CompositeAssociationRecord[] compositeAssociationRecords = check from CompositeAssociationRecord compositeAssociationRecord in testEntitiesClient->/compositeassociationrecords.get(CompositeAssociationRecord) + select compositeAssociationRecord; + test:assertEquals(compositeAssociationRecords, [compositeAssociationRecord1, compositeAssociationRecord2]); + + // read dependent + CompositeAssociationRecordDependent[] compositeAssociationRecordsDependent = check from CompositeAssociationRecordDependent compositeAssociationRecord in testEntitiesClient->/compositeassociationrecords.get(CompositeAssociationRecordDependent) + select compositeAssociationRecord; + test:assertEquals(compositeAssociationRecordsDependent, [ + {randomField: compositeAssociationRecord1.randomField, alltypesidrecordIntType: compositeAssociationRecord1.alltypesidrecordIntType, alltypesidrecordDecimalType: compositeAssociationRecord1.alltypesidrecordDecimalType, allTypesIdRecord: {intType: allTypesIdRecord1.intType, stringType: allTypesIdRecord1.stringType, booleanType: allTypesIdRecord1.booleanType, randomField: allTypesIdRecord1.randomField}}, + {randomField: compositeAssociationRecord2.randomField, alltypesidrecordIntType: compositeAssociationRecord2.alltypesidrecordIntType, alltypesidrecordDecimalType: compositeAssociationRecord2.alltypesidrecordDecimalType, allTypesIdRecord: {intType: allTypesIdRecord1.intType, stringType: allTypesIdRecord1.stringType, booleanType: allTypesIdRecord1.booleanType, randomField: allTypesIdRecord1.randomField}} + ]); + + // update + retrievedRecord1 = check testEntitiesClient->/compositeassociationrecords/[compositeAssociationRecord1.id].put({randomField: "test1Updated"}); + test:assertEquals(compositeAssociationRecordUpdated1, retrievedRecord1); + retrievedRecord1 = check testEntitiesClient->/compositeassociationrecords/[compositeAssociationRecord1.id].get(); + test:assertEquals(compositeAssociationRecordUpdated1, retrievedRecord1); + + // delete + CompositeAssociationRecord retrievedRecord2 = check testEntitiesClient->/compositeassociationrecords/[compositeAssociationRecord2.id].delete(); + test:assertEquals(compositeAssociationRecord2, retrievedRecord2); + compositeAssociationRecords = check from CompositeAssociationRecord compositeAssociationRecord in testEntitiesClient->/compositeassociationrecords.get(CompositeAssociationRecord) + select compositeAssociationRecord; + test:assertEquals(compositeAssociationRecords, [compositeAssociationRecordUpdated1]); + + check testEntitiesClient.close(); +} diff --git a/ballerina/tests/h2-native-tests.bal b/ballerina/tests/h2-native-tests.bal new file mode 100644 index 0000000..b1391e3 --- /dev/null +++ b/ballerina/tests/h2-native-tests.bal @@ -0,0 +1,303 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.org). +// +// 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: ["native", "h2"], + dependsOn: [h2EmployeeRelationsTest, h2WorkspaceRelationsTest, h2BuildingRelationsTest, h2DepartmentRelationsTest] +} +function h2NativeExecuteTest() returns error? { + H2RainierClient rainierClient = check new (); + _ = check rainierClient->executeNativeSQL(`DELETE FROM "Employee"`); + _ = check rainierClient->executeNativeSQL(`DELETE FROM "Workspace"`); + _ = check rainierClient->executeNativeSQL(`DELETE FROM "Building"`); + _ = check rainierClient->executeNativeSQL(`DELETE FROM "Department"`); + + ExecutionResult executionResult = check rainierClient->executeNativeSQL(` + INSERT INTO "Department" ("deptNo", "deptName") + VALUES + (${departmentNative1.deptNo}, ${departmentNative1.deptName}), + (${departmentNative2.deptNo}, ${departmentNative2.deptName}), + (${departmentNative3.deptNo}, ${departmentNative3.deptName}) + `); + test:assertEquals(executionResult, {affectedRowCount: 3, lastInsertId: "department-native-1"}); + + executionResult = check rainierClient->executeNativeSQL(` + INSERT INTO "Building" ("buildingCode", "city", "state", "country", "postalCode", "type") + VALUES + (${buildingNative1.buildingCode}, ${buildingNative1.city}, ${buildingNative1.state}, ${buildingNative1.country}, ${buildingNative1.postalCode}, ${buildingNative1.'type}), + (${buildingNative2.buildingCode}, ${buildingNative2.city}, ${buildingNative2.state}, ${buildingNative2.country}, ${buildingNative2.postalCode}, ${buildingNative2.'type}), + (${buildingNative3.buildingCode}, ${buildingNative3.city}, ${buildingNative3.state}, ${buildingNative3.country}, ${buildingNative3.postalCode}, ${buildingNative3.'type}) + `); + test:assertEquals(executionResult, {affectedRowCount: 3, lastInsertId: "building-native-1"}); + + executionResult = check rainierClient->executeNativeSQL(` + INSERT INTO "Workspace" ("workspaceId", "workspaceType", "locationBuildingCode") + VALUES + (${workspaceNative1.workspaceId}, ${workspaceNative1.workspaceType}, ${workspaceNative1.locationBuildingCode}), + (${workspaceNative2.workspaceId}, ${workspaceNative2.workspaceType}, ${workspaceNative2.locationBuildingCode}), + (${workspaceNative3.workspaceId}, ${workspaceNative3.workspaceType}, ${workspaceNative3.locationBuildingCode}) + `); + test:assertEquals(executionResult, {affectedRowCount: 3, lastInsertId: "workspace-native-1"}); + + executionResult = check rainierClient->executeNativeSQL(` + INSERT INTO "Employee" ("empNo", "firstName", "lastName", "birthDate", "gender", "hireDate", "departmentDeptNo", "workspaceWorkspaceId") + VALUES + (${employeeNative1.empNo}, ${employeeNative1.firstName}, ${employeeNative1.lastName}, ${employeeNative1.birthDate}, ${employeeNative1.gender}, ${employeeNative1.hireDate}, ${employeeNative1.departmentDeptNo}, ${employeeNative1.workspaceWorkspaceId}), + (${employeeNative2.empNo}, ${employeeNative2.firstName}, ${employeeNative2.lastName}, ${employeeNative2.birthDate}, ${employeeNative2.gender}, ${employeeNative2.hireDate}, ${employeeNative2.departmentDeptNo}, ${employeeNative2.workspaceWorkspaceId}), + (${employeeNative3.empNo}, ${employeeNative3.firstName}, ${employeeNative3.lastName}, ${employeeNative3.birthDate}, ${employeeNative3.gender}, ${employeeNative3.hireDate}, ${employeeNative3.departmentDeptNo}, ${employeeNative3.workspaceWorkspaceId}) + `); + test:assertEquals(executionResult, {affectedRowCount: 3, lastInsertId: "employee-native-1"}); + + check rainierClient.close(); +} + +@test:Config { + groups: ["native", "h2"], + dependsOn: [h2NativeExecuteTest] +} +function h2NativeExecuteTestNegative1() returns error? { + H2RainierClient rainierClient = check new (); + ExecutionResult|persist:Error executionResult = rainierClient->executeNativeSQL(` + INSERT INTO "Department" ("deptNo", "deptName") + VALUES (${departmentNative1.deptNo}, ${departmentNative1.deptName}) + `); + + if executionResult is persist:Error { + test:assertTrue(executionResult.message().includes("Unique index or primary key violation: \"PUBLIC.PRIMARY_KEY_A9 ON PUBLIC.Department(deptNo) VALUES ( /* 6 */ 'department-native-1'")); + } else { + test:assertFail("persist:Error expected."); + } + + check rainierClient.close(); +} + +@test:Config { + groups: ["native", "h2"], + dependsOn: [h2NativeExecuteTest] +} +function h2NativeExecuteTestNegative2() returns error? { + H2RainierClient rainierClient = check new (); + ExecutionResult|persist:Error executionResult = rainierClient->executeNativeSQL(` + INSERT INTO "Departments" ("deptNo", "deptName") + VALUES (${departmentNative1.deptNo}, ${departmentNative1.deptName}) + `); + + if executionResult is persist:Error { + test:assertTrue(executionResult.message().includes("Table \"Departments\" not found;")); + } else { + test:assertFail("persist:Error expected."); + } + + check rainierClient.close(); +} + +@test:Config { + groups: ["native", "h2"], + dependsOn: [h2NativeExecuteTest] +} +function h2NativeQueryTest() returns error? { + H2RainierClient rainierClient = check new (); + stream departmentStream = rainierClient->queryNativeSQL(`SELECT * FROM "Department"`); + Department[] departments = check from Department department in departmentStream + select department; + check departmentStream.close(); + test:assertEquals(departments, [departmentNative1, departmentNative2, departmentNative3]); + + stream buildingStream = rainierClient->queryNativeSQL(`SELECT * FROM "Building"`); + Building[] buildings = check from Building building in buildingStream + select building; + check buildingStream.close(); + test:assertEquals(buildings, [buildingNative1, buildingNative2, buildingNative3]); + + stream workspaceStream = rainierClient->queryNativeSQL(`SELECT * FROM "Workspace"`); + Workspace[] workspaces = check from Workspace workspace in workspaceStream + select workspace; + check workspaceStream.close(); + test:assertEquals(workspaces, [workspaceNative1, workspaceNative2, workspaceNative3]); + + stream employeeStream = rainierClient->queryNativeSQL(`SELECT * FROM "Employee"`); + Employee[] employees = check from Employee employee in employeeStream + select employee; + check employeeStream.close(); + test:assertEquals(employees, [employeeNative1, employeeNative2, employeeNative3]); + + check rainierClient.close(); +} + +@test:Config { + groups: ["native", "h2"], + dependsOn: [h2NativeExecuteTest] +} +function h2NativeQueryTestNegative() returns error? { + H2RainierClient rainierClient = check new (); + stream departmentStream = rainierClient->queryNativeSQL(`SELECT * FROM "Departments"`); + Department[]|persist:Error departments = from Department department in departmentStream + select department; + check departmentStream.close(); + + if departments is persist:Error { + test:assertTrue(departments.message().includes("Table \"Departments\" not found")); + } else { + test:assertFail("persist:Error expected."); + } + + check rainierClient.close(); +} + +@test:Config { + groups: ["native", "h2"], + dependsOn: [h2NativeExecuteTest] +} +function h2NativeQueryComplexTest() returns error? { + H2RainierClient rainierClient = check new (); + stream employeeInfoStream = rainierClient->queryNativeSQL(` + SELECT + "firstName", + "lastName", + department."deptName" AS "department.deptName", + workspace."workspaceId" AS "workspace.workspaceId", + workspace."workspaceType" AS "workspace.workspaceType", + workspace."locationBuildingCode" AS "workspace.locationBuildingCode" + FROM "Employee" + INNER JOIN + "Department" department ON "Employee"."departmentDeptNo" = department."deptNo" + INNER JOIN + "Workspace" workspace ON "Employee"."workspaceWorkspaceId" = workspace."workspaceId" + `); + EmployeeInfo[] employees = check from EmployeeInfo employee in employeeInfoStream + select employee; + check employeeInfoStream.close(); + test:assertEquals(employees, [employeeInfoNative1, employeeInfoNative2, employeeInfoNative3]); + + check rainierClient.close(); +} + +@test:Config { + groups: ["transactions", "h2", "native"], + dependsOn: [h2NativeExecuteTestNegative1, h2NativeQueryTest, h2NativeQueryTestNegative, h2NativeQueryComplexTest] +} +function h2NativeTransactionTest() returns error? { + H2RainierClient rainierClient = check new (); + _ = check rainierClient->executeNativeSQL(`DELETE FROM "Employee"`); + _ = check rainierClient->executeNativeSQL(`DELETE FROM "Workspace"`); + _ = check rainierClient->executeNativeSQL(`DELETE FROM "Building"`); + _ = check rainierClient->executeNativeSQL(`DELETE FROM "Department"`); + + transaction { + ExecutionResult executionResult = check rainierClient->executeNativeSQL(` + INSERT INTO "Building" ("buildingCode", "city", "state", "country", "postalCode", "type") + VALUES + (${building31.buildingCode}, ${building31.city}, ${building31.state}, ${building31.country}, ${building31.postalCode}, ${building31.'type}), + (${building32.buildingCode}, ${building32.city}, ${building32.state}, ${building32.country}, ${building32.postalCode}, ${building32.'type}) + `); + test:assertEquals(executionResult, {affectedRowCount: 2, lastInsertId: "building-31"}); + + stream buildingStream = rainierClient->queryNativeSQL(`SELECT * FROM "Building"`); + Building[] buildings = check from Building building in buildingStream + select building; + check buildingStream.close(); + test:assertEquals(buildings, [building31, building32]); + + executionResult = check rainierClient->executeNativeSQL(` + INSERT INTO "Building" ("buildingCode", "city", "state", "country", "postalCode", "type") + VALUES + (${building31.buildingCode}, ${building31.city}, ${building31.state}, ${building31.country}, ${building31.postalCode}, ${building31.'type}) + `); + check commit; + } on fail error e { + test:assertTrue(e is persist:Error, "persist:Error expected"); + } + + stream buildingStream = rainierClient->queryNativeSQL(`SELECT * FROM "Building"`); + Building[] buildings = check from Building building in buildingStream + select building; + check buildingStream.close(); + test:assertEquals(buildings, []); + + check rainierClient.close(); +} + +@test:Config { + groups: ["transactions", "h2", "native"], + dependsOn: [h2NativeExecuteTestNegative1, h2NativeQueryTest, h2NativeQueryTestNegative, h2NativeQueryComplexTest] +} +function h2NativeTransactionTest2() returns error? { + H2RainierClient rainierClient = check new (); + + ExecutionResult executionResult = check rainierClient->executeNativeSQL(` + INSERT INTO "Building" ("buildingCode", "city", "state", "country", "postalCode", "type") + VALUES + (${building33.buildingCode}, ${building33.city}, ${building33.state}, ${building33.country}, ${building33.postalCode}, ${building33.'type}) + `); + test:assertEquals(executionResult, {affectedRowCount: 1, lastInsertId: "building-33"}); + + stream buildingStream = rainierClient->queryNativeSQL(`SELECT * FROM "Building" WHERE "buildingCode" = ${building33.buildingCode}`); + Building[] buildings = check from Building building in buildingStream + select building; + check buildingStream.close(); + test:assertEquals(buildings, [building33]); + + transaction { + _ = check rainierClient->executeNativeSQL(` + UPDATE "Building" + SET + "city" = ${building33Updated.city}, + "state" = ${building33Updated.state}, + "country" = ${building33Updated.country} + WHERE "buildingCode" = ${building33.buildingCode} + `); + check commit; + } + + stream buildingStream3 = rainierClient->queryNativeSQL(`SELECT * FROM "Building" WHERE "buildingCode" = ${building33.buildingCode}`); + Building[] buildings3 = check from Building building in buildingStream3 + select building; + check buildingStream3.close(); + test:assertEquals(buildings3, [building33Updated]); + + check rainierClient.close(); +} + +@test:Config { + groups: ["h2", "native"], + dependsOn: [h2AllTypesDeleteTest] +} +function h2NativeAllTypesTest() returns error? { + H2RainierClient rainierClient = check new (); + _ = check rainierClient->executeNativeSQL(`DELETE FROM "AllTypes"`); + + ExecutionResult executionResult = check rainierClient->executeNativeSQL(` + INSERT INTO "AllTypes" ( + "id", "booleanType", "intType", "floatType", "decimalType", "stringType", "byteArrayType", "dateType", "timeOfDayType", "civilType", "booleanTypeOptional", "intTypeOptional", + "floatTypeOptional", "decimalTypeOptional", "stringTypeOptional", "dateTypeOptional", "timeOfDayTypeOptional", "civilTypeOptional", "enumType", "enumTypeOptional" + ) VALUES ( + ${allTypes1.id}, ${allTypes1.booleanType}, ${allTypes1.intType}, ${allTypes1.floatType}, ${allTypes1.decimalType}, ${allTypes1.stringType}, ${allTypes1.byteArrayType}, + ${allTypes1.dateType}, ${allTypes1.timeOfDayType}, ${allTypes1.civilType}, ${allTypes1.booleanTypeOptional}, ${allTypes1.intTypeOptional}, ${allTypes1.floatTypeOptional}, + ${allTypes1.decimalTypeOptional}, ${allTypes1.stringTypeOptional}, ${allTypes1.dateTypeOptional}, ${allTypes1.timeOfDayTypeOptional}, ${allTypes1.civilTypeOptional}, ${allTypes1.enumType}, ${allTypes1.enumTypeOptional} + ) + `); + test:assertEquals(executionResult, {affectedRowCount: 1, lastInsertId: 1}); + + stream allTypesStream = rainierClient->queryNativeSQL(`SELECT * FROM "AllTypes" WHERE "id" = ${allTypes1.id}`); + AllTypes[] allTypes = check from AllTypes allType in allTypesStream + select allType; + check allTypesStream.close(); + test:assertEquals(allTypes, [allTypes1]); + check rainierClient.close(); +} diff --git a/ballerina/tests/h2-transaction-tests.bal b/ballerina/tests/h2-transaction-tests.bal new file mode 100644 index 0000000..03a246e --- /dev/null +++ b/ballerina/tests/h2-transaction-tests.bal @@ -0,0 +1,75 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.org). +// +// 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: ["transactions", "h2"] +} +function h2TransactionTest() returns error? { + H2RainierClient rainierClient = check new (); + + transaction { + string[] buildingCodes = check rainierClient->/buildings.post([building31, building32]); + test:assertEquals(buildingCodes, [building31.buildingCode, building32.buildingCode]); + + buildingCodes = check rainierClient->/buildings.post([building31]); + check commit; + } on fail error e { + test:assertTrue(e is persist:AlreadyExistsError, "AlreadyExistsError expected"); + } + + Building|persist:Error buildingRetrieved = rainierClient->/buildings/[building31.buildingCode].get(); + test:assertTrue(buildingRetrieved is persist:NotFoundError, "NotFoundError expected"); + + buildingRetrieved = rainierClient->/buildings/[building32.buildingCode].get(); + test:assertTrue(buildingRetrieved is persist:NotFoundError, "NotFoundError expected"); + + check rainierClient.close(); +} + +@test:Config { + groups: ["transactions", "h2"] +} +function h2TransactionTest2() returns error? { + H2RainierClient rainierClient = check new (); + + _ = check rainierClient->/buildings.post([building33]); + Building buildingRetrieved = check rainierClient->/buildings/[building33.buildingCode].get(); + test:assertEquals(buildingRetrieved, building33); + + transaction { + Building building = check rainierClient->/buildings/[building33.buildingCode].put({ + city: "ColomboUpdated", + state: "Western ProvinceUpdated", + country: "Sri LankaUpdated" + }); + + test:assertEquals(building, building33Updated); + + // below should retrieve the updated building record + buildingRetrieved = check rainierClient->/buildings/[building33.buildingCode].get(); + test:assertEquals(buildingRetrieved, building33Updated); + + check commit; + } + + buildingRetrieved = check rainierClient->/buildings/[building33.buildingCode].get(); + test:assertEquals(buildingRetrieved, building33Updated); + + check rainierClient.close(); +} diff --git a/ballerina/tests/h2-workspace-tests.bal b/ballerina/tests/h2-workspace-tests.bal new file mode 100644 index 0000000..70b8cc2 --- /dev/null +++ b/ballerina/tests/h2-workspace-tests.bal @@ -0,0 +1,252 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.org). +// +// 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: ["workspace", "h2"], + dependsOn: [h2BuildingDeleteTestNegative] +} +function h2WorkspaceCreateTest() returns error? { + H2RainierClient rainierClient = check new (); + + string[] workspaceIds = check rainierClient->/workspaces.post([workspace1]); + test:assertEquals(workspaceIds, [workspace1.workspaceId]); + + Workspace workspaceRetrieved = check rainierClient->/workspaces/[workspace1.workspaceId].get(); + test:assertEquals(workspaceRetrieved, workspace1); +} + +@test:Config { + groups: ["workspace", "h2"] +} +function h2WorkspaceCreateTest2() returns error? { + H2RainierClient rainierClient = check new (); + + string[] workspaceIds = check rainierClient->/workspaces.post([workspace2, workspace3]); + + test:assertEquals(workspaceIds, [workspace2.workspaceId, workspace3.workspaceId]); + + Workspace workspaceRetrieved = check rainierClient->/workspaces/[workspace2.workspaceId].get(); + test:assertEquals(workspaceRetrieved, workspace2); + + workspaceRetrieved = check rainierClient->/workspaces/[workspace3.workspaceId].get(); + test:assertEquals(workspaceRetrieved, workspace3); + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "h2"] +} +function h2WorkspaceCreateTestNegative() returns error? { + H2RainierClient rainierClient = check new (); + + string[]|error workspace = rainierClient->/workspaces.post([invalidWorkspace]); + if workspace is persist:Error { + test:assertTrue(workspace.message().includes("Value too long for column \"workspaceId CHARACTER VARYING(36)")); + } else { + test:assertFail("Error expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "h2"], + dependsOn: [h2WorkspaceCreateTest] +} +function h2WorkspaceReadOneTest() returns error? { + H2RainierClient rainierClient = check new (); + + Workspace workspaceRetrieved = check rainierClient->/workspaces/[workspace1.workspaceId].get(); + test:assertEquals(workspaceRetrieved, workspace1); + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "h2"], + dependsOn: [h2WorkspaceCreateTest] +} +function h2WorkspaceReadOneDependentTest() returns error? { + H2RainierClient rainierClient = check new (); + + WorkspaceInfo2 workspaceRetrieved = check rainierClient->/workspaces/[workspace1.workspaceId].get(); + test:assertEquals(workspaceRetrieved, + { + workspaceType: workspace1.workspaceType, + locationBuildingCode: workspace1.locationBuildingCode + } + ); + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "h2"], + dependsOn: [h2WorkspaceCreateTest] +} +function h2WorkspaceReadOneTestNegative() returns error? { + H2RainierClient rainierClient = check new (); + + Workspace|error workspaceRetrieved = rainierClient->/workspaces/["invalid-workspace-id"].get(); + if workspaceRetrieved is persist:NotFoundError { + test:assertEquals(workspaceRetrieved.message(), "A record with the key 'invalid-workspace-id' does not exist for the entity 'Workspace'."); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "h2"], + dependsOn: [h2WorkspaceCreateTest, h2WorkspaceCreateTest2] +} +function h2WorkspaceReadManyTest() returns error? { + H2RainierClient rainierClient = check new (); + + stream workspaceStream = rainierClient->/workspaces.get(); + Workspace[] workspaces = check from Workspace workspace in workspaceStream + select workspace; + + test:assertEquals(workspaces, [workspace1, workspace2, workspace3]); + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "h2", "dependent"], + dependsOn: [h2WorkspaceCreateTest, h2WorkspaceCreateTest2] +} +function h2WorkspaceReadManyDependentTest() returns error? { + H2RainierClient rainierClient = check new (); + + stream workspaceStream = rainierClient->/workspaces.get(); + WorkspaceInfo2[] workspaces = check from WorkspaceInfo2 workspace in workspaceStream + select workspace; + + test:assertEquals(workspaces, [ + {workspaceType: workspace1.workspaceType, locationBuildingCode: workspace1.locationBuildingCode}, + {workspaceType: workspace2.workspaceType, locationBuildingCode: workspace2.locationBuildingCode}, + {workspaceType: workspace3.workspaceType, locationBuildingCode: workspace3.locationBuildingCode} + ]); + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "h2"], + dependsOn: [h2WorkspaceReadOneTest, h2WorkspaceReadManyTest, h2WorkspaceReadManyDependentTest] +} +function h2WorkspaceUpdateTest() returns error? { + H2RainierClient rainierClient = check new (); + + Workspace workspace = check rainierClient->/workspaces/[workspace1.workspaceId].put({ + workspaceType: "large" + }); + + test:assertEquals(workspace, updatedWorkspace1); + + Workspace workspaceRetrieved = check rainierClient->/workspaces/[workspace1.workspaceId].get(); + test:assertEquals(workspaceRetrieved, updatedWorkspace1); + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "h2"], + dependsOn: [h2WorkspaceCreateTest, h2WorkspaceCreateTest2] +} +function h2WorkspaceReadWithClauses() returns error? { + H2RainierClient rainierClient = check new (); + string value = "small"; + string value2 = "medium"; + string id = "\"Workspace\".\"workspaceId\""; + int count = 2; + stream workspaceStream = rainierClient->/workspaces.get(whereClause = `"Workspace"."workspaceType" = ${value} OR "Workspace"."workspaceType" = ${value2}`, orderByClause = `"Workspace"."workspaceId" DESC `, limitClause = ` ${count}`, groupByClause = `${id}`); + Workspace[] workspaces = check from Workspace workspace in workspaceStream + select workspace; + test:assertEquals(workspaces, [workspace3, workspace2]); + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "h2"], + dependsOn: [h2WorkspaceReadOneTest, h2WorkspaceReadManyTest, h2WorkspaceReadManyDependentTest, h2WorkspaceReadWithClauses] +} +function h2WorkspaceUpdateTestNegative1() returns error? { + H2RainierClient rainierClient = check new (); + + Workspace|error workspace = rainierClient->/workspaces/["invalid-workspace-id"].put({ + workspaceType: "large" + }); + + if workspace is persist:NotFoundError { + test:assertEquals(workspace.message(), "A record with the key 'invalid-workspace-id' does not exist for the entity 'Workspace'."); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "h2"], + dependsOn: [h2WorkspaceReadOneTest, h2WorkspaceReadManyTest, h2WorkspaceReadManyDependentTest, h2WorkspaceReadWithClauses] +} +function h2WorkspaceUpdateTestNegative2() returns error? { + H2RainierClient rainierClient = check new (); + + Workspace|error workspace = rainierClient->/workspaces/[workspace1.workspaceId].put({ + workspaceType: "unncessarily-long-workspace-type-to-force-error-on-update" + }); + + if workspace is persist:Error { + test:assertTrue(workspace.message().includes("Value too long for column \"workspaceType CHARACTER VARYING(10)")); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "h2"], + dependsOn: [h2WorkspaceUpdateTest, h2WorkspaceUpdateTestNegative2] +} +function h2WorkspaceDeleteTest() returns error? { + H2RainierClient rainierClient = check new (); + + Workspace workspace = check rainierClient->/workspaces/[workspace1.workspaceId].delete(); + test:assertEquals(workspace, updatedWorkspace1); + + stream workspaceStream = rainierClient->/workspaces.get(); + Workspace[] workspaces = check from Workspace workspace2 in workspaceStream + select workspace2; + + test:assertEquals(workspaces, [workspace2, workspace3]); + check rainierClient.close(); +} + +@test:Config { + groups: ["workspace", "h2"], + dependsOn: [h2WorkspaceDeleteTest] +} +function h2WorkspaceDeleteTestNegative() returns error? { + H2RainierClient rainierClient = check new (); + + Workspace|error workspace = rainierClient->/workspaces/[workspace1.workspaceId].delete(); + + if workspace is persist:NotFoundError { + test:assertEquals(workspace.message(), string `A record with the key '${workspace1.workspaceId}' does not exist for the entity 'Workspace'.`); + } else { + test:assertFail("NotFoundError expected."); + } + check rainierClient.close(); +} diff --git a/ballerina/tests/h2_hospital_persist_client.bal b/ballerina/tests/h2_hospital_persist_client.bal new file mode 100644 index 0000000..9785982 --- /dev/null +++ b/ballerina/tests/h2_hospital_persist_client.bal @@ -0,0 +1,254 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com). +// +// 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. + +// AUTO-GENERATED FILE. DO NOT MODIFY. +// This file is an auto-generated file by Ballerina persistence layer for model. +// It should not be modified by hand. +import ballerina/jballerina.java; +import ballerina/persist; +import ballerina/sql; +import ballerinax/h2.driver as _; +import ballerinax/java.jdbc as jdbc; + +const APPOINTMENT = "appointments"; +const PATIENT = "patients"; +const DOCTOR = "doctors"; + +public isolated client class H2HospitalClient { + *persist:AbstractPersistClient; + + private final jdbc:Client dbClient; + + private final map persistClients; + + private final record {|SQLMetadata...;|} & readonly metadata = { + [APPOINTMENT]: { + entityName: "Appointment", + tableName: "appointment", + fieldMetadata: { + id: {columnName: "id"}, + reason: {columnName: "reason"}, + appointmentTime: {columnName: "appointmentTime"}, + status: {columnName: "status"}, + patientId: {columnName: "patient_id"}, + doctorId: {columnName: "doctorId"}, + "patient.id": {relation: {entityName: "patient", refField: "id", refColumn: "IDP"}}, + "patient.name": {relation: {entityName: "patient", refField: "name"}}, + "patient.age": {relation: {entityName: "patient", refField: "age"}}, + "patient.address": {relation: {entityName: "patient", refField: "address", refColumn: "ADD_RESS"}}, + "patient.phoneNumber": {relation: {entityName: "patient", refField: "phoneNumber"}}, + "patient.gender": {relation: {entityName: "patient", refField: "gender"}}, + "doctor.id": {relation: {entityName: "doctor", refField: "id"}}, + "doctor.name": {relation: {entityName: "doctor", refField: "name"}}, + "doctor.specialty": {relation: {entityName: "doctor", refField: "specialty"}}, + "doctor.phoneNumber": {relation: {entityName: "doctor", refField: "phoneNumber", refColumn: "phone_number"}}, + "doctor.salary": {relation: {entityName: "doctor", refField: "salary"}} + }, + keyFields: ["id"], + joinMetadata: { + patient: {entity: Patient, fieldName: "patient", refTable: "patients", refColumns: ["IDP"], joinColumns: ["patient_id"], 'type: ONE_TO_MANY}, + doctor: {entity: Doctor, fieldName: "doctor", refTable: "Doctor", refColumns: ["id"], joinColumns: ["doctorId"], 'type: ONE_TO_MANY} + } + }, + [PATIENT]: { + entityName: "Patient", + tableName: "patients", + fieldMetadata: { + id: {columnName: "IDP", dbGenerated: true}, + name: {columnName: "name"}, + age: {columnName: "age"}, + address: {columnName: "ADD_RESS"}, + phoneNumber: {columnName: "phoneNumber"}, + gender: {columnName: "gender"}, + "appointments[].id": {relation: {entityName: "appointments", refField: "id"}}, + "appointments[].reason": {relation: {entityName: "appointments", refField: "reason"}}, + "appointments[].appointmentTime": {relation: {entityName: "appointments", refField: "appointmentTime"}}, + "appointments[].status": {relation: {entityName: "appointments", refField: "status"}}, + "appointments[].patientId": {relation: {entityName: "appointments", refField: "patientId", refColumn: "patient_id"}}, + "appointments[].doctorId": {relation: {entityName: "appointments", refField: "doctorId"}} + }, + keyFields: ["id"], + joinMetadata: {appointments: {entity: Appointment, fieldName: "appointments", refTable: "appointment", refColumns: ["patient_id"], joinColumns: ["IDP"], 'type: MANY_TO_ONE}} + }, + [DOCTOR]: { + entityName: "Doctor", + tableName: "Doctor", + fieldMetadata: { + id: {columnName: "id"}, + name: {columnName: "name"}, + specialty: {columnName: "specialty"}, + phoneNumber: {columnName: "phone_number"}, + salary: {columnName: "salary"}, + "appointments[].id": {relation: {entityName: "appointments", refField: "id"}}, + "appointments[].reason": {relation: {entityName: "appointments", refField: "reason"}}, + "appointments[].appointmentTime": {relation: {entityName: "appointments", refField: "appointmentTime"}}, + "appointments[].status": {relation: {entityName: "appointments", refField: "status"}}, + "appointments[].patientId": {relation: {entityName: "appointments", refField: "patientId", refColumn: "patient_id"}}, + "appointments[].doctorId": {relation: {entityName: "appointments", refField: "doctorId"}} + }, + keyFields: ["id"], + joinMetadata: {appointments: {entity: Appointment, fieldName: "appointments", refTable: "appointment", refColumns: ["doctorId"], joinColumns: ["id"], 'type: MANY_TO_ONE}} + } + }; + + public isolated function init() returns persist:Error? { + jdbc:Client|error dbClient = new (url = h2.url, user = h2.user, password = h2.password, options = {...h2.connectionOptions}); + if dbClient is error { + return error(dbClient.message()); + } + self.dbClient = dbClient; + self.persistClients = { + [APPOINTMENT]: check new (dbClient, self.metadata.get(APPOINTMENT), H2_SPECIFICS), + [PATIENT]: check new (dbClient, self.metadata.get(PATIENT), H2_SPECIFICS), + [DOCTOR]: check new (dbClient, self.metadata.get(DOCTOR), H2_SPECIFICS) + }; + } + + isolated resource function get appointments(AppointmentTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "query" + } external; + + isolated resource function get appointments/[int id](AppointmentTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "queryOne" + } external; + + isolated resource function post appointments(AppointmentInsert[] data) returns int[]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(APPOINTMENT); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from AppointmentInsert inserted in data + select inserted.id; + } + + isolated resource function put appointments/[int id](AppointmentUpdate value) returns Appointment|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(APPOINTMENT); + } + _ = check sqlClient.runUpdateQuery(id, value); + return self->/appointments/[id].get(); + } + + isolated resource function delete appointments/[int id]() returns Appointment|persist:Error { + Appointment result = check self->/appointments/[id].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(APPOINTMENT); + } + _ = check sqlClient.runDeleteQuery(id); + return result; + } + + isolated resource function get patients(PatientTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "query" + } external; + + isolated resource function get patients/[int id](PatientTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "queryOne" + } external; + + isolated resource function post patients(PatientInsert[] data) returns int[]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(PATIENT); + } + sql:ExecutionResult[] result = check sqlClient.runBatchInsertQuery(data); + return from sql:ExecutionResult inserted in result + where inserted.lastInsertId != () + select inserted.lastInsertId; + } + + isolated resource function put patients/[int id](PatientUpdate value) returns Patient|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(PATIENT); + } + _ = check sqlClient.runUpdateQuery(id, value); + return self->/patients/[id].get(); + } + + isolated resource function delete patients/[int id]() returns Patient|persist:Error { + Patient result = check self->/patients/[id].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(PATIENT); + } + _ = check sqlClient.runDeleteQuery(id); + return result; + } + + isolated resource function get doctors(DoctorTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "query" + } external; + + isolated resource function get doctors/[int id](DoctorTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "queryOne" + } external; + + isolated resource function post doctors(DoctorInsert[] data) returns int[]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(DOCTOR); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from DoctorInsert inserted in data + select inserted.id; + } + + isolated resource function put doctors/[int id](DoctorUpdate value) returns Doctor|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(DOCTOR); + } + _ = check sqlClient.runUpdateQuery(id, value); + return self->/doctors/[id].get(); + } + + isolated resource function delete doctors/[int id]() returns Doctor|persist:Error { + Doctor result = check self->/doctors/[id].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(DOCTOR); + } + _ = check sqlClient.runDeleteQuery(id); + return result; + } + + remote isolated function queryNativeSQL(sql:ParameterizedQuery sqlQuery, typedesc rowType = <>) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor" + } external; + + remote isolated function executeNativeSQL(sql:ParameterizedQuery sqlQuery) returns ExecutionResult|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor" + } external; + + public isolated function close() returns persist:Error? { + error? result = self.dbClient.close(); + if result is error { + return error(result.message()); + } + return result; + } +} + diff --git a/ballerina/tests/h2_hospital_tests.bal b/ballerina/tests/h2_hospital_tests.bal new file mode 100644 index 0000000..7e6a942 --- /dev/null +++ b/ballerina/tests/h2_hospital_tests.bal @@ -0,0 +1,363 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com). +// +// 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/log; +import ballerina/persist; +import ballerina/test; + +@test:Config { + groups: ["annotation", "h2"] +} +function testCreatePatientH2() returns error? { + H2HospitalClient h2DbHospital = check new (); + PatientInsert patient = { + name: "John Doe", + age: 30, + phoneNumber: "0771690000", + gender: "MALE", + address: "123, Main Street, Colombo 05" + }; + int[] unionResult = check h2DbHospital->/patients.post([patient]); + test:assertEquals(unionResult[0], 1, "Patient should be created"); +} + +@test:Config { + groups: ["annotation", "h2"] +} +function testCreateDoctorH2() returns error? { + H2HospitalClient h2DbHospital = check new (); + DoctorInsert doctor = { + id: 1, + name: "Doctor Mouse", + specialty: "Physician", + phoneNumber: "077100100", + salary: 20000 + }; + int[] res = check h2DbHospital->/doctors.post([doctor]); + test:assertEquals(res[0], 1, "Doctor should be created"); +} + +@test:Config { + groups: ["annotation", "h2"], + dependsOn: [testCreateDoctorH2] +} +function testCreateDoctorAlreadyExistsH2() returns error? { + H2HospitalClient h2DbHospital = check new (); + DoctorInsert doctor = { + id: 1, + name: "Doctor Mouse", + specialty: "Physician", + phoneNumber: "077100100", + salary: 20000.00 + }; + int[]|persist:Error res = h2DbHospital->/doctors.post([doctor]); + if !(res is persist:AlreadyExistsError) { + test:assertFail("Doctor should not be created"); + } +} + +@test:Config { + groups: ["annotation", "h2"], + dependsOn: [testCreatePatientH2, testCreateDoctorH2] +} +function testCreateAppointmentH2() returns error? { + H2HospitalClient h2DbHospital = check new (); + AppointmentInsert appointment = { + id: 1, + patientId: 1, + doctorId: 1, + appointmentTime: {year: 2023, month: 7, day: 1, hour: 10, minute: 30}, + status: "SCHEDULED", + reason: "Headache" + }; + int[] res = check h2DbHospital->/appointments.post([appointment]); + test:assertEquals(res[0], 1, "Appointment should be created"); +} + +@test:Config { + groups: ["annotation", "h2"], + dependsOn: [testCreatePatientH2, testCreateDoctorH2, testCreateAppointmentH2] +} +function testCreateAppointmentAlreadyExistsH2() returns error? { + H2HospitalClient h2DbHospital = check new (); + AppointmentInsert appointment = { + id: 1, + patientId: 1, + doctorId: 1, + appointmentTime: {year: 2023, month: 7, day: 1, hour: 10, minute: 30}, + status: "SCHEDULED", + reason: "Headache" + }; + int[]|persist:Error res = h2DbHospital->/appointments.post([appointment]); + if !(res is persist:AlreadyExistsError) { + test:assertFail("Appointment should not be created"); + } +} + +@test:Config { + groups: ["annotation", "h2"], + dependsOn: [testCreateDoctorH2] +} +function testGetDoctorsH2() returns error? { + H2HospitalClient h2DbHospital = check new (); + stream doctors = h2DbHospital->/doctors.get(); + Doctor[]|persist:Error doctorsArr = from Doctor doctor in doctors + select doctor; + Doctor[] expected = [ + {id: 1, name: "Doctor Mouse", specialty: "Physician", phoneNumber: "077100100", salary: 20000} + ]; + test:assertEquals(doctorsArr, expected, "Doctor details should be returned"); +} + +@test:Config { + groups: ["annotation", "h2"], + dependsOn: [testCreatePatientH2] +} +function testGetPatientByIdH2() returns error? { + H2HospitalClient h2DbHospital = check new (); + Patient|persist:Error patient = h2DbHospital->/patients/[1].get(); + Patient expected = {"id": 1, "name": "John Doe", "age": 30, "address": "123, Main Street, Colombo 05", "phoneNumber": "0771690000", "gender": "MALE"}; + test:assertEquals(patient, expected, "Patient details should be returned"); +} + +@test:Config { + groups: ["annotation", "h2"] +} +function testGetPatientNotFoundH2() returns error? { + H2HospitalClient h2DbHospital = check new (); + Patient|persist:Error patient = h2DbHospital->/patients/[10].get(); + if !(patient is persist:NotFoundError) { + test:assertFail("Patient should be not found"); + } +} + +@test:Config { + groups: ["annotation", "h2"], + dependsOn: [testCreateAppointmentH2] +} +function testGetAppointmentByDoctorH2() returns error? { + H2HospitalClient h2DbHospital = check new (); + stream appointments = h2DbHospital->/appointments(); + AppointmentWithRelations[]|persist:Error? filteredAppointments = from AppointmentWithRelations appointment in appointments + where appointment.doctorId == 1 && + appointment.appointmentTime?.year == 2023 && + appointment.appointmentTime?.month == 7 && + appointment.appointmentTime?.day == 1 + select appointment; + AppointmentWithRelations[] expected = [ + { + "id": 1, + "doctorId": 1, + "patientId": 1, + "reason": "Headache", + "appointmentTime": { + "year": 2023, + "month": 7, + "day": 1, + "hour": 10, + "minute": 30, + "second": 0 + }, + "status": "SCHEDULED", + "patient": { + "id": 1, + "name": "John Doe", + "age": 30, + "address": "123, Main Street, Colombo 05", + "phoneNumber": "0771690000", + "gender": "MALE" + }, + "doctor": { + "id": 1, + "name": "Doctor Mouse", + "specialty": "Physician", + "phoneNumber": "077100100", + "salary": 20000 + } + } + ]; + test:assertEquals(filteredAppointments, expected, "Appointment details should be returned"); + + stream appointments2 = h2DbHospital->/appointments(); + Appointment[]|persist:Error? filteredAppointments2 = from Appointment appointment in appointments2 + where appointment.doctorId == 5 && + appointment.appointmentTime.year == 2023 && + appointment.appointmentTime.month == 7 && + appointment.appointmentTime.day == 1 + select appointment; + test:assertEquals(filteredAppointments2, [], "Appointment details should be empty"); +} + +@test:Config { + groups: ["annotation", "h2"], + dependsOn: [testCreateAppointmentH2] +} +function testGetAppointmentByPatientH2() returns error? { + H2HospitalClient h2DbHospital = check new (); + stream appointments = h2DbHospital->/appointments(); + AppointmentWithRelations[]|persist:Error? filteredAppointments = from AppointmentWithRelations appointment in appointments + where appointment.patientId == 1 + select appointment; + AppointmentWithRelations[] expected = [ + { + "id": 1, + "doctorId": 1, + "patientId": 1, + "reason": "Headache", + "appointmentTime": { + "year": 2023, + "month": 7, + "day": 1, + "hour": 10, + "minute": 30, + "second": 0 + }, + "status": "SCHEDULED", + "patient": { + "id": 1, + "name": "John Doe", + "age": 30, + "address": "123, Main Street, Colombo 05", + "phoneNumber": "0771690000", + "gender": "MALE" + }, + "doctor": { + "id": 1, + "name": "Doctor Mouse", + "specialty": "Physician", + "phoneNumber": "077100100", + "salary": 20000 + } + } + ]; + test:assertEquals(filteredAppointments, expected, "Appointment details should be returned"); + stream appointments2 = h2DbHospital->/appointments(); + AppointmentWithRelations[]|persist:Error? filteredAppointments2 = from AppointmentWithRelations appointment in appointments2 + where appointment.patientId == 5 + select appointment; + test:assertEquals(filteredAppointments2, [], "Appointment details should be empty"); +} + +@test:Config { + groups: ["annotation", "h2"], + dependsOn: [testCreateAppointmentH2, testGetAppointmentByDoctorH2, testGetAppointmentByPatientH2] +} +function testPatchAppointmentH2() returns error? { + H2HospitalClient h2DbHospital = check new (); + Appointment|persist:Error result = h2DbHospital->/appointments/[1].put({status: "STARTED"}); + if result is persist:Error { + test:assertFail("Appointment should be updated"); + } + stream appointments = h2DbHospital->/appointments(); + AppointmentWithRelations[]|persist:Error? filteredAppointments = from AppointmentWithRelations appointment in appointments + where appointment.patientId == 1 + select appointment; + AppointmentWithRelations[] expected = [ + { + "id": 1, + "doctorId": 1, + "patientId": 1, + "reason": "Headache", + "appointmentTime": { + "year": 2023, + "month": 7, + "day": 1, + "hour": 10, + "minute": 30, + "second": 0 + }, + "status": "STARTED", + "patient": { + "id": 1, + "name": "John Doe", + "age": 30, + "address": "123, Main Street, Colombo 05", + "phoneNumber": "0771690000", + "gender": "MALE" + }, + "doctor": { + "id": 1, + "name": "Doctor Mouse", + "specialty": "Physician", + "phoneNumber": "077100100", + "salary": 20000 + } + } + ]; + test:assertEquals(filteredAppointments, expected, "Appointment details should be updated"); + Appointment|persist:Error result2 = h2DbHospital->/appointments/[0].put({status: "STARTED"}); + if !(result2 is persist:NotFoundError) { + test:assertFail("Appointment should not be found"); + } +} + +@test:Config { + groups: ["annotation", "h2"], + dependsOn: [testCreateAppointmentH2, testGetAppointmentByDoctorH2, testGetAppointmentByPatientH2, testPatchAppointmentH2] +} +function testDeleteAppointmentByPatientIdH2() returns error? { + H2HospitalClient h2DbHospital = check new (); + stream appointments = h2DbHospital->/appointments; + Appointment[]|persist:Error result = from Appointment appointment in appointments + where appointment.patientId == 1 + && appointment.appointmentTime.year == 2023 + && appointment.appointmentTime.month == 7 + && appointment.appointmentTime.day == 1 + select appointment; + if (result is persist:Error) { + test:assertFail("Appointment should be found"); + } + foreach Appointment appointment in result { + Appointment|persist:Error result2 = h2DbHospital->/appointments/[appointment.id].delete(); + if result2 is persist:Error { + test:assertFail("Appointment should be deleted"); + } + } + stream appointments2 = h2DbHospital->/appointments; + Appointment[]|persist:Error result3 = from Appointment appointment in appointments2 + where appointment.patientId == 1 + && appointment.appointmentTime.year == 2023 + && appointment.appointmentTime.month == 7 + && appointment.appointmentTime.day == 1 + select appointment; + test:assertEquals(result3, [], "Appointment details should be empty"); +} + +@test:Config { + groups: ["annotation", "h2"], + dependsOn: [testGetPatientByIdH2, testDeleteAppointmentByPatientIdH2] +} +function testDeletePatientH2() returns error? { + H2HospitalClient h2DbHospital = check new (); + Patient|persist:Error result = h2DbHospital->/patients/[1].delete(); + if result is persist:Error { + log:printError("Error: ", result); + test:assertFail("Patient should be deleted"); + } +} + +@test:Config { + groups: ["annotation", "h2"], + dependsOn: [testGetDoctorsH2, testDeleteAppointmentByPatientIdH2] +} +function testDeleteDoctorH2() returns error? { + H2HospitalClient h2DbHospital = check new (); + Doctor|persist:Error result = h2DbHospital->/doctors/[1].delete(); + if result is persist:Error { + log:printError("Error: ", result); + test:assertFail("Patient should be deleted"); + } +} diff --git a/ballerina/tests/h2_rainier_generated_client.bal b/ballerina/tests/h2_rainier_generated_client.bal new file mode 100644 index 0000000..d3203f5 --- /dev/null +++ b/ballerina/tests/h2_rainier_generated_client.bal @@ -0,0 +1,362 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.org). +// +// 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/jballerina.java; +import ballerina/persist; +import ballerina/sql; +import ballerinax/h2.driver as _; +import ballerinax/java.jdbc as jdbc; + +const EMPLOYEE = "employees"; +const WORKSPACE = "workspaces"; +const BUILDING = "buildings"; +const DEPARTMENT = "departments"; +const ORDER_ITEM = "orderitems"; + +public isolated client class H2RainierClient { + *persist:AbstractPersistClient; + + private final jdbc:Client dbClient; + + private final map persistClients; + + private final record {|SQLMetadata...;|} & readonly metadata = { + [EMPLOYEE] : { + entityName: "Employee", + tableName: "Employee", + fieldMetadata: { + empNo: {columnName: "empNo"}, + firstName: {columnName: "firstName"}, + lastName: {columnName: "lastName"}, + birthDate: {columnName: "birthDate"}, + gender: {columnName: "gender"}, + hireDate: {columnName: "hireDate"}, + departmentDeptNo: {columnName: "departmentDeptNo"}, + workspaceWorkspaceId: {columnName: "workspaceWorkspaceId"}, + "department.deptNo": {relation: {entityName: "department", refField: "deptNo"}}, + "department.deptName": {relation: {entityName: "department", refField: "deptName"}}, + "workspace.workspaceId": {relation: {entityName: "workspace", refField: "workspaceId"}}, + "workspace.workspaceType": {relation: {entityName: "workspace", refField: "workspaceType"}}, + "workspace.locationBuildingCode": {relation: {entityName: "workspace", refField: "locationBuildingCode"}} + }, + keyFields: ["empNo"], + joinMetadata: { + department: {entity: Department, fieldName: "department", refTable: "Department", refColumns: ["deptNo"], joinColumns: ["departmentDeptNo"], 'type: ONE_TO_MANY}, + workspace: {entity: Workspace, fieldName: "workspace", refTable: "Workspace", refColumns: ["workspaceId"], joinColumns: ["workspaceWorkspaceId"], 'type: ONE_TO_MANY} + } + }, + [WORKSPACE] : { + entityName: "Workspace", + tableName: "Workspace", + fieldMetadata: { + workspaceId: {columnName: "workspaceId"}, + workspaceType: {columnName: "workspaceType"}, + locationBuildingCode: {columnName: "locationBuildingCode"}, + "location.buildingCode": {relation: {entityName: "location", refField: "buildingCode"}}, + "location.city": {relation: {entityName: "location", refField: "city"}}, + "location.state": {relation: {entityName: "location", refField: "state"}}, + "location.country": {relation: {entityName: "location", refField: "country"}}, + "location.postalCode": {relation: {entityName: "location", refField: "postalCode"}}, + "location.type": {relation: {entityName: "location", refField: "type"}}, + "employees[].empNo": {relation: {entityName: "employees", refField: "empNo"}}, + "employees[].firstName": {relation: {entityName: "employees", refField: "firstName"}}, + "employees[].lastName": {relation: {entityName: "employees", refField: "lastName"}}, + "employees[].birthDate": {relation: {entityName: "employees", refField: "birthDate"}}, + "employees[].gender": {relation: {entityName: "employees", refField: "gender"}}, + "employees[].hireDate": {relation: {entityName: "employees", refField: "hireDate"}}, + "employees[].departmentDeptNo": {relation: {entityName: "employees", refField: "departmentDeptNo"}}, + "employees[].workspaceWorkspaceId": {relation: {entityName: "employees", refField: "workspaceWorkspaceId"}} + }, + keyFields: ["workspaceId"], + joinMetadata: { + location: {entity: Building, fieldName: "location", refTable: "Building", refColumns: ["buildingCode"], joinColumns: ["locationBuildingCode"], 'type: ONE_TO_MANY}, + employees: {entity: Employee, fieldName: "employees", refTable: "Employee", refColumns: ["workspaceWorkspaceId"], joinColumns: ["workspaceId"], 'type: MANY_TO_ONE} + } + }, + [BUILDING] : { + entityName: "Building", + tableName: "Building", + fieldMetadata: { + buildingCode: {columnName: "buildingCode"}, + city: {columnName: "city"}, + state: {columnName: "state"}, + country: {columnName: "country"}, + postalCode: {columnName: "postalCode"}, + 'type: {columnName: "type"}, + "workspaces[].workspaceId": {relation: {entityName: "workspaces", refField: "workspaceId"}}, + "workspaces[].workspaceType": {relation: {entityName: "workspaces", refField: "workspaceType"}}, + "workspaces[].locationBuildingCode": {relation: {entityName: "workspaces", refField: "locationBuildingCode"}} + }, + keyFields: ["buildingCode"], + joinMetadata: {workspaces: {entity: Workspace, fieldName: "workspaces", refTable: "Workspace", refColumns: ["locationBuildingCode"], joinColumns: ["buildingCode"], 'type: MANY_TO_ONE}} + }, + [DEPARTMENT] : { + entityName: "Department", + tableName: "Department", + fieldMetadata: { + deptNo: {columnName: "deptNo"}, + deptName: {columnName: "deptName"}, + "employees[].empNo": {relation: {entityName: "employees", refField: "empNo"}}, + "employees[].firstName": {relation: {entityName: "employees", refField: "firstName"}}, + "employees[].lastName": {relation: {entityName: "employees", refField: "lastName"}}, + "employees[].birthDate": {relation: {entityName: "employees", refField: "birthDate"}}, + "employees[].gender": {relation: {entityName: "employees", refField: "gender"}}, + "employees[].hireDate": {relation: {entityName: "employees", refField: "hireDate"}}, + "employees[].departmentDeptNo": {relation: {entityName: "employees", refField: "departmentDeptNo"}}, + "employees[].workspaceWorkspaceId": {relation: {entityName: "employees", refField: "workspaceWorkspaceId"}} + }, + keyFields: ["deptNo"], + joinMetadata: {employees: {entity: Employee, fieldName: "employees", refTable: "Employee", refColumns: ["departmentDeptNo"], joinColumns: ["deptNo"], 'type: MANY_TO_ONE}} + }, + [ORDER_ITEM] : { + entityName: "OrderItem", + tableName: "OrderItem", + fieldMetadata: { + orderId: {columnName: "orderId"}, + itemId: {columnName: "itemId"}, + quantity: {columnName: "quantity"}, + notes: {columnName: "notes"} + }, + keyFields: ["orderId", "itemId"] + } + }; + + public isolated function init() returns persist:Error? { + jdbc:Client|error dbClient = new (url = h2.url, user = h2.user, password = h2.password, options = {...h2.connectionOptions}); + if dbClient is error { + return error(dbClient.message()); + } + self.dbClient = dbClient; + self.persistClients = { + [EMPLOYEE] : check new (dbClient, self.metadata.get(EMPLOYEE), H2_SPECIFICS), + [WORKSPACE] : check new (dbClient, self.metadata.get(WORKSPACE), H2_SPECIFICS), + [BUILDING] : check new (dbClient, self.metadata.get(BUILDING), H2_SPECIFICS), + [DEPARTMENT] : check new (dbClient, self.metadata.get(DEPARTMENT), H2_SPECIFICS), + [ORDER_ITEM] : check new (dbClient, self.metadata.get(ORDER_ITEM), H2_SPECIFICS) + }; + } + + isolated resource function get employees(EmployeeTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "query" + } external; + + isolated resource function get employees/[string empNo](EmployeeTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "queryOne" + } external; + + isolated resource function post employees(EmployeeInsert[] data) returns string[]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(EMPLOYEE); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from EmployeeInsert inserted in data + select inserted.empNo; + } + + isolated resource function put employees/[string empNo](EmployeeUpdate value) returns Employee|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(EMPLOYEE); + } + _ = check sqlClient.runUpdateQuery(empNo, value); + return self->/employees/[empNo].get(); + } + + isolated resource function delete employees/[string empNo]() returns Employee|persist:Error { + Employee result = check self->/employees/[empNo].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(EMPLOYEE); + } + _ = check sqlClient.runDeleteQuery(empNo); + return result; + } + + isolated resource function get workspaces(WorkspaceTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "query" + } external; + + isolated resource function get workspaces/[string workspaceId](WorkspaceTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "queryOne" + } external; + + isolated resource function post workspaces(WorkspaceInsert[] data) returns string[]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(WORKSPACE); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from WorkspaceInsert inserted in data + select inserted.workspaceId; + } + + isolated resource function put workspaces/[string workspaceId](WorkspaceUpdate value) returns Workspace|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(WORKSPACE); + } + _ = check sqlClient.runUpdateQuery(workspaceId, value); + return self->/workspaces/[workspaceId].get(); + } + + isolated resource function delete workspaces/[string workspaceId]() returns Workspace|persist:Error { + Workspace result = check self->/workspaces/[workspaceId].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(WORKSPACE); + } + _ = check sqlClient.runDeleteQuery(workspaceId); + return result; + } + + isolated resource function get buildings(BuildingTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "query" + } external; + + isolated resource function get buildings/[string buildingCode](BuildingTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "queryOne" + } external; + + isolated resource function post buildings(BuildingInsert[] data) returns string[]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(BUILDING); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from BuildingInsert inserted in data + select inserted.buildingCode; + } + + isolated resource function put buildings/[string buildingCode](BuildingUpdate value) returns Building|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(BUILDING); + } + _ = check sqlClient.runUpdateQuery(buildingCode, value); + return self->/buildings/[buildingCode].get(); + } + + isolated resource function delete buildings/[string buildingCode]() returns Building|persist:Error { + Building result = check self->/buildings/[buildingCode].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(BUILDING); + } + _ = check sqlClient.runDeleteQuery(buildingCode); + return result; + } + + isolated resource function get departments(DepartmentTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "query" + } external; + + isolated resource function get departments/[string deptNo](DepartmentTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "queryOne" + } external; + + isolated resource function post departments(DepartmentInsert[] data) returns string[]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(DEPARTMENT); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from DepartmentInsert inserted in data + select inserted.deptNo; + } + + isolated resource function put departments/[string deptNo](DepartmentUpdate value) returns Department|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(DEPARTMENT); + } + _ = check sqlClient.runUpdateQuery(deptNo, value); + return self->/departments/[deptNo].get(); + } + + isolated resource function delete departments/[string deptNo]() returns Department|persist:Error { + Department result = check self->/departments/[deptNo].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(DEPARTMENT); + } + _ = check sqlClient.runDeleteQuery(deptNo); + return result; + } + + isolated resource function get orderitems(OrderItemTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "query" + } external; + + isolated resource function get orderitems/[string orderId]/[string itemId](OrderItemTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "queryOne" + } external; + + isolated resource function post orderitems(OrderItemInsert[] data) returns [string, string][]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(ORDER_ITEM); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from OrderItemInsert inserted in data + select [inserted.orderId, inserted.itemId]; + } + + isolated resource function put orderitems/[string orderId]/[string itemId](OrderItemUpdate value) returns OrderItem|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(ORDER_ITEM); + } + _ = check sqlClient.runUpdateQuery({"orderId": orderId, "itemId": itemId}, value); + return self->/orderitems/[orderId]/[itemId].get(); + } + + isolated resource function delete orderitems/[string orderId]/[string itemId]() returns OrderItem|persist:Error { + OrderItem result = check self->/orderitems/[orderId]/[itemId].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(ORDER_ITEM); + } + _ = check sqlClient.runDeleteQuery({"orderId": orderId, "itemId": itemId}); + return result; + } + + remote isolated function queryNativeSQL(sql:ParameterizedQuery sqlQuery, typedesc rowType = <>) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor" + } external; + + remote isolated function executeNativeSQL(sql:ParameterizedQuery sqlQuery) returns ExecutionResult|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor" + } external; + + public isolated function close() returns persist:Error? { + error? result = self.dbClient.close(); + if result is error { + return error(result.message()); + } + return result; + } +} + diff --git a/ballerina/tests/h2_test_entities_generated_client.bal b/ballerina/tests/h2_test_entities_generated_client.bal new file mode 100644 index 0000000..d530b39 --- /dev/null +++ b/ballerina/tests/h2_test_entities_generated_client.bal @@ -0,0 +1,501 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.org). +// +// 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/jballerina.java; +import ballerina/persist; +import ballerina/sql; +import ballerinax/h2.driver as _; +import ballerinax/java.jdbc as jdbc; + +const ALL_TYPES = "alltypes"; +const STRING_ID_RECORD = "stringidrecords"; +const INT_ID_RECORD = "intidrecords"; +const FLOAT_ID_RECORD = "floatidrecords"; +const DECIMAL_ID_RECORD = "decimalidrecords"; +const BOOLEAN_ID_RECORD = "booleanidrecords"; +const COMPOSITE_ASSOCIATION_RECORD = "compositeassociationrecords"; +const ALL_TYPES_ID_RECORD = "alltypesidrecords"; + +public isolated client class H2TestEntitiesClient { + *persist:AbstractPersistClient; + + private final jdbc:Client dbClient; + + private final map persistClients; + + private final record {|SQLMetadata...;|} & readonly metadata = { + [ALL_TYPES] : { + entityName: "AllTypes", + tableName: "AllTypes", + fieldMetadata: { + id: {columnName: "id"}, + booleanType: {columnName: "booleanType"}, + intType: {columnName: "intType"}, + floatType: {columnName: "floatType"}, + decimalType: {columnName: "decimalType"}, + stringType: {columnName: "stringType"}, + byteArrayType: {columnName: "byteArrayType"}, + dateType: {columnName: "dateType"}, + timeOfDayType: {columnName: "timeOfDayType"}, + civilType: {columnName: "civilType"}, + booleanTypeOptional: {columnName: "booleanTypeOptional"}, + intTypeOptional: {columnName: "intTypeOptional"}, + floatTypeOptional: {columnName: "floatTypeOptional"}, + decimalTypeOptional: {columnName: "decimalTypeOptional"}, + stringTypeOptional: {columnName: "stringTypeOptional"}, + dateTypeOptional: {columnName: "dateTypeOptional"}, + timeOfDayTypeOptional: {columnName: "timeOfDayTypeOptional"}, + civilTypeOptional: {columnName: "civilTypeOptional"}, + enumType: {columnName: "enumType"}, + enumTypeOptional: {columnName: "enumTypeOptional"} + }, + keyFields: ["id"] + }, + [STRING_ID_RECORD] : { + entityName: "StringIdRecord", + tableName: "StringIdRecord", + fieldMetadata: { + id: {columnName: "id"}, + randomField: {columnName: "randomField"} + }, + keyFields: ["id"] + }, + [INT_ID_RECORD] : { + entityName: "IntIdRecord", + tableName: "IntIdRecord", + fieldMetadata: { + id: {columnName: "id"}, + randomField: {columnName: "randomField"} + }, + keyFields: ["id"] + }, + [FLOAT_ID_RECORD] : { + entityName: "FloatIdRecord", + tableName: "FloatIdRecord", + fieldMetadata: { + id: {columnName: "id"}, + randomField: {columnName: "randomField"} + }, + keyFields: ["id"] + }, + [DECIMAL_ID_RECORD] : { + entityName: "DecimalIdRecord", + tableName: "DecimalIdRecord", + fieldMetadata: { + id: {columnName: "id"}, + randomField: {columnName: "randomField"} + }, + keyFields: ["id"] + }, + [BOOLEAN_ID_RECORD] : { + entityName: "BooleanIdRecord", + tableName: "BooleanIdRecord", + fieldMetadata: { + id: {columnName: "id"}, + randomField: {columnName: "randomField"} + }, + keyFields: ["id"] + }, + [COMPOSITE_ASSOCIATION_RECORD] : { + entityName: "CompositeAssociationRecord", + tableName: "CompositeAssociationRecord", + fieldMetadata: { + id: {columnName: "id"}, + randomField: {columnName: "randomField"}, + alltypesidrecordBooleanType: {columnName: "alltypesidrecordBooleanType"}, + alltypesidrecordIntType: {columnName: "alltypesidrecordIntType"}, + alltypesidrecordFloatType: {columnName: "alltypesidrecordFloatType"}, + alltypesidrecordDecimalType: {columnName: "alltypesidrecordDecimalType"}, + alltypesidrecordStringType: {columnName: "alltypesidrecordStringType"}, + "allTypesIdRecord.booleanType": {relation: {entityName: "allTypesIdRecord", refField: "booleanType"}}, + "allTypesIdRecord.intType": {relation: {entityName: "allTypesIdRecord", refField: "intType"}}, + "allTypesIdRecord.floatType": {relation: {entityName: "allTypesIdRecord", refField: "floatType"}}, + "allTypesIdRecord.decimalType": {relation: {entityName: "allTypesIdRecord", refField: "decimalType"}}, + "allTypesIdRecord.stringType": {relation: {entityName: "allTypesIdRecord", refField: "stringType"}}, + "allTypesIdRecord.randomField": {relation: {entityName: "allTypesIdRecord", refField: "randomField"}} + }, + keyFields: ["id"], + joinMetadata: {allTypesIdRecord: {entity: AllTypesIdRecord, fieldName: "allTypesIdRecord", refTable: "AllTypesIdRecord", refColumns: ["booleanType", "intType", "floatType", "decimalType", "stringType"], joinColumns: ["alltypesidrecordBooleanType", "alltypesidrecordIntType", "alltypesidrecordFloatType", "alltypesidrecordDecimalType", "alltypesidrecordStringType"], 'type: ONE_TO_ONE}} + }, + [ALL_TYPES_ID_RECORD] : { + entityName: "AllTypesIdRecord", + tableName: "AllTypesIdRecord", + fieldMetadata: { + booleanType: {columnName: "booleanType"}, + intType: {columnName: "intType"}, + floatType: {columnName: "floatType"}, + decimalType: {columnName: "decimalType"}, + stringType: {columnName: "stringType"}, + randomField: {columnName: "randomField"}, + "compositeAssociationRecord.id": {relation: {entityName: "compositeAssociationRecord", refField: "id"}}, + "compositeAssociationRecord.randomField": {relation: {entityName: "compositeAssociationRecord", refField: "randomField"}}, + "compositeAssociationRecord.alltypesidrecordBooleanType": {relation: {entityName: "compositeAssociationRecord", refField: "alltypesidrecordBooleanType"}}, + "compositeAssociationRecord.alltypesidrecordIntType": {relation: {entityName: "compositeAssociationRecord", refField: "alltypesidrecordIntType"}}, + "compositeAssociationRecord.alltypesidrecordFloatType": {relation: {entityName: "compositeAssociationRecord", refField: "alltypesidrecordFloatType"}}, + "compositeAssociationRecord.alltypesidrecordDecimalType": {relation: {entityName: "compositeAssociationRecord", refField: "alltypesidrecordDecimalType"}}, + "compositeAssociationRecord.alltypesidrecordStringType": {relation: {entityName: "compositeAssociationRecord", refField: "alltypesidrecordStringType"}} + }, + keyFields: ["booleanType", "intType", "floatType", "decimalType", "stringType"], + joinMetadata: {compositeAssociationRecord: {entity: CompositeAssociationRecord, fieldName: "compositeAssociationRecord", refTable: "CompositeAssociationRecord", refColumns: ["alltypesidrecordBooleanType", "alltypesidrecordIntType", "alltypesidrecordFloatType", "alltypesidrecordDecimalType", "alltypesidrecordStringType"], joinColumns: ["booleanType", "intType", "floatType", "decimalType", "stringType"], 'type: ONE_TO_ONE}} + } + }; + + public isolated function init() returns persist:Error? { + jdbc:Client|error dbClient = new (url = h2.url, user = h2.user, password = h2.password, options = {...h2.connectionOptions}); + if dbClient is error { + return error(dbClient.message()); + } + self.dbClient = dbClient; + self.persistClients = { + [ALL_TYPES] : check new (dbClient, self.metadata.get(ALL_TYPES), H2_SPECIFICS), + [STRING_ID_RECORD] : check new (dbClient, self.metadata.get(STRING_ID_RECORD), H2_SPECIFICS), + [INT_ID_RECORD] : check new (dbClient, self.metadata.get(INT_ID_RECORD), H2_SPECIFICS), + [FLOAT_ID_RECORD] : check new (dbClient, self.metadata.get(FLOAT_ID_RECORD), H2_SPECIFICS), + [DECIMAL_ID_RECORD] : check new (dbClient, self.metadata.get(DECIMAL_ID_RECORD), H2_SPECIFICS), + [BOOLEAN_ID_RECORD] : check new (dbClient, self.metadata.get(BOOLEAN_ID_RECORD), H2_SPECIFICS), + [COMPOSITE_ASSOCIATION_RECORD] : check new (dbClient, self.metadata.get(COMPOSITE_ASSOCIATION_RECORD), H2_SPECIFICS), + [ALL_TYPES_ID_RECORD] : check new (dbClient, self.metadata.get(ALL_TYPES_ID_RECORD), H2_SPECIFICS) + }; + } + + isolated resource function get alltypes(AllTypesTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "query" + } external; + + isolated resource function get alltypes/[int id](AllTypesTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "queryOne" + } external; + + isolated resource function post alltypes(AllTypesInsert[] data) returns int[]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(ALL_TYPES); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from AllTypesInsert inserted in data + select inserted.id; + } + + isolated resource function put alltypes/[int id](AllTypesUpdate value) returns AllTypes|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(ALL_TYPES); + } + _ = check sqlClient.runUpdateQuery(id, value); + return self->/alltypes/[id].get(); + } + + isolated resource function delete alltypes/[int id]() returns AllTypes|persist:Error { + AllTypes result = check self->/alltypes/[id].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(ALL_TYPES); + } + _ = check sqlClient.runDeleteQuery(id); + return result; + } + + isolated resource function get stringidrecords(StringIdRecordTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "query" + } external; + + isolated resource function get stringidrecords/[string id](StringIdRecordTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "queryOne" + } external; + + isolated resource function post stringidrecords(StringIdRecordInsert[] data) returns string[]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(STRING_ID_RECORD); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from StringIdRecordInsert inserted in data + select inserted.id; + } + + isolated resource function put stringidrecords/[string id](StringIdRecordUpdate value) returns StringIdRecord|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(STRING_ID_RECORD); + } + _ = check sqlClient.runUpdateQuery(id, value); + return self->/stringidrecords/[id].get(); + } + + isolated resource function delete stringidrecords/[string id]() returns StringIdRecord|persist:Error { + StringIdRecord result = check self->/stringidrecords/[id].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(STRING_ID_RECORD); + } + _ = check sqlClient.runDeleteQuery(id); + return result; + } + + isolated resource function get intidrecords(IntIdRecordTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "query" + } external; + + isolated resource function get intidrecords/[int id](IntIdRecordTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "queryOne" + } external; + + isolated resource function post intidrecords(IntIdRecordInsert[] data) returns int[]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(INT_ID_RECORD); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from IntIdRecordInsert inserted in data + select inserted.id; + } + + isolated resource function put intidrecords/[int id](IntIdRecordUpdate value) returns IntIdRecord|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(INT_ID_RECORD); + } + _ = check sqlClient.runUpdateQuery(id, value); + return self->/intidrecords/[id].get(); + } + + isolated resource function delete intidrecords/[int id]() returns IntIdRecord|persist:Error { + IntIdRecord result = check self->/intidrecords/[id].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(INT_ID_RECORD); + } + _ = check sqlClient.runDeleteQuery(id); + return result; + } + + isolated resource function get floatidrecords(FloatIdRecordTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "query" + } external; + + isolated resource function get floatidrecords/[float id](FloatIdRecordTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "queryOne" + } external; + + isolated resource function post floatidrecords(FloatIdRecordInsert[] data) returns float[]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(FLOAT_ID_RECORD); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from FloatIdRecordInsert inserted in data + select inserted.id; + } + + isolated resource function put floatidrecords/[float id](FloatIdRecordUpdate value) returns FloatIdRecord|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(FLOAT_ID_RECORD); + } + _ = check sqlClient.runUpdateQuery(id, value); + return self->/floatidrecords/[id].get(); + } + + isolated resource function delete floatidrecords/[float id]() returns FloatIdRecord|persist:Error { + FloatIdRecord result = check self->/floatidrecords/[id].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(FLOAT_ID_RECORD); + } + _ = check sqlClient.runDeleteQuery(id); + return result; + } + + isolated resource function get decimalidrecords(DecimalIdRecordTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "query" + } external; + + isolated resource function get decimalidrecords/[decimal id](DecimalIdRecordTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "queryOne" + } external; + + isolated resource function post decimalidrecords(DecimalIdRecordInsert[] data) returns decimal[]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(DECIMAL_ID_RECORD); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from DecimalIdRecordInsert inserted in data + select inserted.id; + } + + isolated resource function put decimalidrecords/[decimal id](DecimalIdRecordUpdate value) returns DecimalIdRecord|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(DECIMAL_ID_RECORD); + } + _ = check sqlClient.runUpdateQuery(id, value); + return self->/decimalidrecords/[id].get(); + } + + isolated resource function delete decimalidrecords/[decimal id]() returns DecimalIdRecord|persist:Error { + DecimalIdRecord result = check self->/decimalidrecords/[id].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(DECIMAL_ID_RECORD); + } + _ = check sqlClient.runDeleteQuery(id); + return result; + } + + isolated resource function get booleanidrecords(BooleanIdRecordTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "query" + } external; + + isolated resource function get booleanidrecords/[boolean id](BooleanIdRecordTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "queryOne" + } external; + + isolated resource function post booleanidrecords(BooleanIdRecordInsert[] data) returns boolean[]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(BOOLEAN_ID_RECORD); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from BooleanIdRecordInsert inserted in data + select inserted.id; + } + + isolated resource function put booleanidrecords/[boolean id](BooleanIdRecordUpdate value) returns BooleanIdRecord|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(BOOLEAN_ID_RECORD); + } + _ = check sqlClient.runUpdateQuery(id, value); + return self->/booleanidrecords/[id].get(); + } + + isolated resource function delete booleanidrecords/[boolean id]() returns BooleanIdRecord|persist:Error { + BooleanIdRecord result = check self->/booleanidrecords/[id].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(BOOLEAN_ID_RECORD); + } + _ = check sqlClient.runDeleteQuery(id); + return result; + } + + isolated resource function get compositeassociationrecords(CompositeAssociationRecordTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "query" + } external; + + isolated resource function get compositeassociationrecords/[string id](CompositeAssociationRecordTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "queryOne" + } external; + + isolated resource function post compositeassociationrecords(CompositeAssociationRecordInsert[] data) returns string[]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(COMPOSITE_ASSOCIATION_RECORD); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from CompositeAssociationRecordInsert inserted in data + select inserted.id; + } + + isolated resource function put compositeassociationrecords/[string id](CompositeAssociationRecordUpdate value) returns CompositeAssociationRecord|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(COMPOSITE_ASSOCIATION_RECORD); + } + _ = check sqlClient.runUpdateQuery(id, value); + return self->/compositeassociationrecords/[id].get(); + } + + isolated resource function delete compositeassociationrecords/[string id]() returns CompositeAssociationRecord|persist:Error { + CompositeAssociationRecord result = check self->/compositeassociationrecords/[id].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(COMPOSITE_ASSOCIATION_RECORD); + } + _ = check sqlClient.runDeleteQuery(id); + return result; + } + + isolated resource function get alltypesidrecords(AllTypesIdRecordTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "query" + } external; + + isolated resource function get alltypesidrecords/[boolean booleanType]/[int intType]/[float floatType]/[decimal decimalType]/[string stringType](AllTypesIdRecordTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "queryOne" + } external; + + isolated resource function post alltypesidrecords(AllTypesIdRecordInsert[] data) returns [boolean, int, float, decimal, string][]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(ALL_TYPES_ID_RECORD); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from AllTypesIdRecordInsert inserted in data + select [inserted.booleanType, inserted.intType, inserted.floatType, inserted.decimalType, inserted.stringType]; + } + + isolated resource function put alltypesidrecords/[boolean booleanType]/[int intType]/[float floatType]/[decimal decimalType]/[string stringType](AllTypesIdRecordUpdate value) returns AllTypesIdRecord|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(ALL_TYPES_ID_RECORD); + } + _ = check sqlClient.runUpdateQuery({"booleanType": booleanType, "intType": intType, "floatType": floatType, "decimalType": decimalType, "stringType": stringType}, value); + return self->/alltypesidrecords/[booleanType]/[intType]/[floatType]/[decimalType]/[stringType].get(); + } + + isolated resource function delete alltypesidrecords/[boolean booleanType]/[int intType]/[float floatType]/[decimal decimalType]/[string stringType]() returns AllTypesIdRecord|persist:Error { + AllTypesIdRecord result = check self->/alltypesidrecords/[booleanType]/[intType]/[floatType]/[decimalType]/[stringType].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(ALL_TYPES_ID_RECORD); + } + _ = check sqlClient.runDeleteQuery({"booleanType": booleanType, "intType": intType, "floatType": floatType, "decimalType": decimalType, "stringType": stringType}); + return result; + } + + remote isolated function queryNativeSQL(sql:ParameterizedQuery sqlQuery, typedesc rowType = <>) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor" + } external; + + remote isolated function executeNativeSQL(sql:ParameterizedQuery sqlQuery) returns ExecutionResult|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor" + } external; + + public isolated function close() returns persist:Error? { + error? result = self.dbClient.close(); + if result is error { + return error(result.message()); + } + return result; + } +} + diff --git a/ballerina/tests/init-tests.bal b/ballerina/tests/init-tests.bal index 8c117e9..1ac9e74 100644 --- a/ballerina/tests/init-tests.bal +++ b/ballerina/tests/init-tests.bal @@ -15,13 +15,35 @@ // under the License. import ballerina/test; -import ballerinax/mysql; -import ballerinax/mysql.driver as _; +import ballerina/time; +import ballerinax/h2.driver as _; +import ballerinax/java.jdbc; import ballerinax/mssql; import ballerinax/mssql.driver as _; +import ballerinax/mysql; +import ballerinax/mysql.driver as _; import ballerinax/postgresql; import ballerinax/postgresql.driver as _; -import ballerina/time; + +configurable record {| + string url; + string user; + string password; + Options connectionOptions = {}; +|} & readonly h2 = ?; + +public type Options record {| + string datasourceName?; + map properties?; + Operations requestGeneratedKeys = ALL; +|}; + +public enum Operations { + NONE, + EXECUTE, + BATCH_EXECUTE, + ALL +} configurable record {| int port; @@ -51,15 +73,11 @@ configurable record {| |} postgresql = ?; @test:BeforeSuite -function initTests() returns error? { - // MySQL +function initSuite() returns error? { check initMySqlTests(); - - //MSSQL check initMsSqlTests(); - - // PostgreSQL check initPostgreSqlTests(); + check initH2Tests(); } function initMySqlTests() returns error? { @@ -235,6 +253,7 @@ function initMsSqlTests() returns error? { PRIMARY KEY(id) ); `); + _ = check mssqlDbClient->execute(` CREATE TABLE [Doctor] ( [id] INT NOT NULL, @@ -289,6 +308,206 @@ function initPostgreSqlTests() returns error? { check postgresqlDbClient.close(); } +function initH2Tests() returns error? { + jdbc:Client h2DbClient = check new (url = h2.url, user = h2.user, password = h2.password); + _ = check h2DbClient->execute(`DROP TABLE IF EXISTS "Building"`); + _ = check h2DbClient->execute(` + CREATE TABLE "Building" ( + "buildingCode" VARCHAR(36) PRIMARY KEY, + "city" VARCHAR(50), + "state" VARCHAR(50), + "country" VARCHAR(50), + "postalCode" VARCHAR(50), + "type" VARCHAR(50) + ) + `); + + _ = check h2DbClient->execute(`DROP TABLE IF EXISTS "Workspace"`); + _ = check h2DbClient->execute(` + CREATE TABLE "Workspace" ( + "workspaceId" VARCHAR(36) PRIMARY KEY, + "workspaceType" VARCHAR(10), + "locationBuildingCode" VARCHAR(36), + FOREIGN KEY ("locationBuildingCode") REFERENCES "Building"("buildingCode") + ) + `); + + _ = check h2DbClient->execute(`DROP TABLE IF EXISTS "Department"`); + _ = check h2DbClient->execute(` + CREATE TABLE "Department" ( + "deptNo" VARCHAR(36) PRIMARY KEY, + "deptName" VARCHAR(30) + ) + `); + + _ = check h2DbClient->execute(`DROP TABLE IF EXISTS "Employee"`); + _ = check h2DbClient->execute(` + CREATE TABLE "Employee" ( + "empNo" VARCHAR(36) PRIMARY KEY, + "firstName" VARCHAR(30), + "lastName" VARCHAR(30), + "birthDate" DATE, + "gender" VARCHAR(6) CHECK ("gender" IN ('MALE', 'FEMALE')) NOT NULL, + "hireDate" DATE, + "departmentDeptNo" VARCHAR(36), + "workspaceWorkspaceId" VARCHAR(36), + FOREIGN KEY ("departmentDeptNo") REFERENCES "Department"("deptNo"), + FOREIGN KEY ("workspaceWorkspaceId") REFERENCES "Workspace"("workspaceId") + ) + `); + + _ = check h2DbClient->execute(`DROP TABLE IF EXISTS "OrderItem"`); + _ = check h2DbClient->execute(` + CREATE TABLE "OrderItem" ( + "orderId" VARCHAR(36), + "itemId" VARCHAR(30), + "quantity" INTEGER, + "notes" VARCHAR(255), + PRIMARY KEY("orderId", "itemId") + ) + `); + + _ = check h2DbClient->execute(`DROP TABLE IF EXISTS "AllTypes"`); + _ = check h2DbClient->execute(` + CREATE TABLE "AllTypes" ( + "id" INT NOT NULL, + "booleanType" BOOLEAN NOT NULL, + "intType" INT NOT NULL, + "floatType" FLOAT NOT NULL, + "decimalType" DECIMAL(10, 2) NOT NULL, + "stringType" VARCHAR(191) NOT NULL, + "byteArrayType" VARBINARY NOT NULL, + "dateType" DATE NOT NULL, + "timeOfDayType" TIME NOT NULL, + "civilType" TIMESTAMP NOT NULL, + "booleanTypeOptional" BOOLEAN, + "intTypeOptional" INT, + "floatTypeOptional" FLOAT, + "decimalTypeOptional" DECIMAL(10, 2), + "stringTypeOptional" VARCHAR(191), + "dateTypeOptional" DATE, + "timeOfDayTypeOptional" TIME, + "civilTypeOptional" TIMESTAMP, + "enumType" VARCHAR(6) CHECK ("enumType" IN ('TYPE_1', 'TYPE_2', 'TYPE_3', 'TYPE_4')) NOT NULL, + "enumTypeOptional" VARCHAR(6) CHECK ("enumTypeOptional" IN ('TYPE_1', 'TYPE_2', 'TYPE_3', 'TYPE_4')), + PRIMARY KEY("id") + ); + `); + + _ = check h2DbClient->execute(`DROP TABLE IF EXISTS "FloatIdRecord"`); + _ = check h2DbClient->execute(` + CREATE TABLE "FloatIdRecord" ( + "id" FLOAT NOT NULL, + "randomField" VARCHAR(191) NOT NULL, + PRIMARY KEY("id") + ); + `); + + _ = check h2DbClient->execute(`DROP TABLE IF EXISTS "StringIdRecord"`); + _ = check h2DbClient->execute(` + CREATE TABLE "StringIdRecord" ( + "id" VARCHAR(191) NOT NULL, + "randomField" VARCHAR(191) NOT NULL, + PRIMARY KEY("id") + ); + `); + + _ = check h2DbClient->execute(`DROP TABLE IF EXISTS "DecimalIdRecord"`); + _ = check h2DbClient->execute(` + CREATE TABLE "DecimalIdRecord" ( + "id" DECIMAL(10, 2) NOT NULL, + "randomField" VARCHAR(191) NOT NULL, + PRIMARY KEY("id") + ); + `); + + _ = check h2DbClient->execute(`DROP TABLE IF EXISTS "BooleanIdRecord"`); + _ = check h2DbClient->execute(` + CREATE TABLE "BooleanIdRecord" ( + "id" BOOLEAN NOT NULL, + "randomField" VARCHAR(191) NOT NULL, + PRIMARY KEY("id") + ); + `); + + _ = check h2DbClient->execute(`DROP TABLE IF EXISTS "IntIdRecord"`); + _ = check h2DbClient->execute(` + CREATE TABLE "IntIdRecord" ( + "id" INT NOT NULL, + "randomField" VARCHAR(191) NOT NULL, + PRIMARY KEY("id") + ); + `); + + _ = check h2DbClient->execute(`DROP TABLE IF EXISTS "AllTypesIdRecord"`); + _ = check h2DbClient->execute(` + CREATE TABLE "AllTypesIdRecord" ( + "booleanType" BOOLEAN NOT NULL, + "intType" INT NOT NULL, + "floatType" FLOAT NOT NULL, + "decimalType" DECIMAL(10, 2) NOT NULL, + "stringType" VARCHAR(191) NOT NULL, + "randomField" VARCHAR(191) NOT NULL, + PRIMARY KEY("booleanType","intType","floatType","decimalType","stringType") + ); + `); + + _ = check h2DbClient->execute(`DROP TABLE IF EXISTS "CompositeAssociationRecord"`); + _ = check h2DbClient->execute(` + CREATE TABLE "CompositeAssociationRecord" ( + "id" VARCHAR(191) NOT NULL, + "randomField" VARCHAR(191) NOT NULL, + "alltypesidrecordBooleanType" BOOLEAN NOT NULL, + "alltypesidrecordIntType" INT NOT NULL, + "alltypesidrecordFloatType" FLOAT NOT NULL, + "alltypesidrecordDecimalType" DECIMAL(10, 2) NOT NULL, + "alltypesidrecordStringType" VARCHAR(191) NOT NULL, + CONSTRAINT FK_COMPOSITEASSOCIATIONRECORD_ALLTYPESIDRECORD FOREIGN KEY("alltypesidrecordBooleanType", "alltypesidrecordIntType", "alltypesidrecordFloatType", "alltypesidrecordDecimalType", "alltypesidrecordStringType") REFERENCES "AllTypesIdRecord"("booleanType", "intType", "floatType", "decimalType", "stringType"), + PRIMARY KEY("id") + ); + `); + + _ = check h2DbClient->execute(`DROP TABLE IF EXISTS "Doctor"`); + _ = check h2DbClient->execute(` + CREATE TABLE "Doctor" ( + "id" INT NOT NULL, + "name" VARCHAR(191) NOT NULL, + "specialty" VARCHAR(191) NOT NULL, + "phone_number" VARCHAR(191) NOT NULL, + "salary" DECIMAL(10,2), + PRIMARY KEY("id") + ); + `); + + _ = check h2DbClient->execute(`DROP TABLE IF EXISTS "patients"`); + _ = check h2DbClient->execute(` + CREATE TABLE "patients" ( + "IDP" INT AUTO_INCREMENT, + "name" VARCHAR(191) NOT NULL, + "age" INT NOT NULL, + "ADD_RESS" VARCHAR(191) NOT NULL, + "phoneNumber" CHAR(10) NOT NULL, + "gender" VARCHAR(6) CHECK ("gender" IN ('MALE', 'FEMALE')) NOT NULL, + PRIMARY KEY("IDP") + ); + `); + + _ = check h2DbClient->execute(`DROP TABLE IF EXISTS "appointment"`); + _ = check h2DbClient->execute(`CREATE TABLE "appointment" ( + "id" INT NOT NULL, + "reason" VARCHAR(191) NOT NULL, + "appointmentTime" DATETIME NOT NULL, + "status" VARCHAR(9) CHECK ("status" IN ('SCHEDULED', 'STARTED', 'ENDED')) NOT NULL, + "patient_id" INT NOT NULL, + FOREIGN KEY("patient_id") REFERENCES "patients"("IDP"), + "doctorId" INT NOT NULL, + FOREIGN KEY("doctorId") REFERENCES "Doctor"("id"), + PRIMARY KEY("id") + )`); + + check h2DbClient.close(); +} + AllTypes allTypes1 = { id: 1, booleanType: false, @@ -351,7 +570,7 @@ AllTypes allTypes2 = { floatTypeOptional: 66.0, decimalTypeOptional: 233.44, stringTypeOptional: "test2", - dateTypeOptional: {year: 1293, month: 11, day: 3}, + dateTypeOptional: {year: 1993, month: 11, day: 3}, timeOfDayTypeOptional: {hour: 19, minute: 32, second: 34}, civilTypeOptional: {year: 1989, month: 11, day: 3, hour: 12, minute: 32, second: 34}, enumType: "TYPE_1", @@ -399,7 +618,7 @@ AllTypes allTypes3 = { floatTypeOptional: 66.0, decimalTypeOptional: (), stringTypeOptional: (), - dateTypeOptional: {year: 1293, month: 11, day: 3}, + dateTypeOptional: {year: 1993, month: 11, day: 3}, timeOfDayTypeOptional: {hour: 19, minute: 32, second: 34}, civilTypeOptional: {year: 1989, month: 11, day: 3, hour: 12, minute: 32, second: 34}, enumType: "TYPE_1", diff --git a/ballerina/tests/mssql_hospital_tests.bal b/ballerina/tests/mssql_hospital_tests.bal index a00208f..9281d91 100644 --- a/ballerina/tests/mssql_hospital_tests.bal +++ b/ballerina/tests/mssql_hospital_tests.bal @@ -18,7 +18,9 @@ import ballerina/persist; import ballerina/test; -@test:Config{} +@test:Config{ + groups: ["annotation", "mssql"] +} function testCreatePatientMsSql() returns error? { MsSqlHospitalClient mssqlDbHospital = check new(); PatientInsert patient = { @@ -31,7 +33,9 @@ function testCreatePatientMsSql() returns error? { _ = check mssqlDbHospital->/patients.post([patient]); } -@test:Config{} +@test:Config{ + groups: ["annotation", "mssql"] +} function testCreateDoctorMsSql() returns error? { MsSqlHospitalClient mssqlDbHospital = check new(); DoctorInsert doctor = { @@ -45,6 +49,7 @@ function testCreateDoctorMsSql() returns error? { } @test:Config{ + groups: ["annotation", "mssql"], dependsOn: [testCreateDoctorMsSql] } function testCreateDoctorAlreadyExistsMsSql() returns error? { @@ -63,6 +68,7 @@ function testCreateDoctorAlreadyExistsMsSql() returns error? { } @test:Config{ + groups: ["annotation", "mssql"], dependsOn: [testCreatePatientMsSql, testCreateDoctorMsSql] } function testCreateAppointmentMsSql() returns error? { @@ -79,6 +85,7 @@ function testCreateAppointmentMsSql() returns error? { } @test:Config{ + groups: ["annotation", "mssql"], dependsOn: [testCreatePatientMsSql, testCreateDoctorMsSql, testCreateAppointmentMsSql] } function testCreateAppointmentAlreadyExistsMsSql() returns error? { @@ -98,6 +105,7 @@ function testCreateAppointmentAlreadyExistsMsSql() returns error? { } @test:Config{ + groups: ["annotation", "mssql"], dependsOn: [testCreateDoctorMsSql] } function testGetDoctorsMsSql() returns error? { @@ -111,6 +119,7 @@ function testGetDoctorsMsSql() returns error? { } @test:Config{ + groups: ["annotation", "mssql"], dependsOn: [testCreatePatientMsSql] } function testGetPatientByIdMsSql() returns error? { @@ -120,7 +129,9 @@ function testGetPatientByIdMsSql() returns error? { test:assertEquals(patient, expected, "Patient details should be returned"); } -@test:Config{} +@test:Config{ + groups: ["annotation", "mssql"] +} function testGetPatientNotFoundMsSql() returns error? { MsSqlHospitalClient mssqlDbHospital = check new(); Patient|persist:Error patient = mssqlDbHospital->/patients/[10].get(); @@ -130,6 +141,7 @@ function testGetPatientNotFoundMsSql() returns error? { } @test:Config{ + groups: ["annotation", "mssql"], dependsOn: [testCreateAppointmentMsSql] } function testGetAppointmentByDoctorMsSql() returns error? { @@ -186,6 +198,7 @@ function testGetAppointmentByDoctorMsSql() returns error? { } @test:Config{ + groups: ["annotation", "mssql"], dependsOn: [testCreateAppointmentMsSql] } function testGetAppointmentByPatientMsSql() returns error? { @@ -235,6 +248,7 @@ function testGetAppointmentByPatientMsSql() returns error? { } @test:Config{ + groups: ["annotation", "mssql"], dependsOn: [testCreateAppointmentMsSql, testGetAppointmentByDoctorMsSql, testGetAppointmentByPatientMsSql] } function testPatchAppointmentMsSql() returns error? { @@ -287,6 +301,7 @@ function testPatchAppointmentMsSql() returns error? { } @test:Config{ + groups: ["annotation", "mssql"], dependsOn: [testCreateAppointmentMsSql, testGetAppointmentByDoctorMsSql, testGetAppointmentByPatientMsSql, testPatchAppointmentMsSql] } function testDeleteAppointmentByPatientIdMsSql() returns error? { @@ -318,6 +333,7 @@ function testDeleteAppointmentByPatientIdMsSql() returns error? { } @test:Config{ + groups: ["annotation", "mssql"], dependsOn: [testGetPatientByIdMsSql, testDeleteAppointmentByPatientIdMsSql] } function testDeletePatientMsSql() returns error? { @@ -329,7 +345,9 @@ function testDeletePatientMsSql() returns error? { } @test:Config{ - dependsOn: [testGetDoctorsMsSql, testDeleteAppointmentByPatientIdMsSql] + groups: ["annotation", "mssql"], + dependsOn: [testGetDoctorsMsSql, testDeleteAppointmentByPatientIdMsSql], + enable: true } function testDeleteDoctorMsSql() returns error? { MsSqlHospitalClient mssqlDbHospital = check new(); diff --git a/ballerina/tests/mysql_hospital_tests.bal b/ballerina/tests/mysql_hospital_tests.bal index 97f116f..39583ac 100644 --- a/ballerina/tests/mysql_hospital_tests.bal +++ b/ballerina/tests/mysql_hospital_tests.bal @@ -18,7 +18,9 @@ import ballerina/persist; import ballerina/test; -@test:Config{} +@test:Config{ + groups: ["annotation", "mysql"] +} function testCreatePatientMySql() returns error? { MySqlHospitalClient mysqlDbHospital = check new(); PatientInsert patient = { @@ -32,7 +34,9 @@ function testCreatePatientMySql() returns error? { test:assertEquals(unionResult[0], 1, "Patient should be created"); } -@test:Config{} +@test:Config{ + groups: ["annotation", "mysql"] +} function testCreateDoctorMySql() returns error? { MySqlHospitalClient mysqlDbHospital = check new(); DoctorInsert doctor = { @@ -47,7 +51,8 @@ function testCreateDoctorMySql() returns error? { } @test:Config{ - dependsOn: [testCreateDoctorMySql] + groups: ["annotation", "mysql"], + dependsOn: [testCreateDoctorMySql] } function testCreateDoctorAlreadyExistsMySql() returns error? { MySqlHospitalClient mysqlDbHospital = check new(); @@ -65,7 +70,8 @@ function testCreateDoctorAlreadyExistsMySql() returns error? { } @test:Config{ - dependsOn: [testCreatePatientMySql, testCreateDoctorMySql] + groups: ["annotation", "mysql"], + dependsOn: [testCreatePatientMySql, testCreateDoctorMySql] } function testCreateAppointmentMySql() returns error? { MySqlHospitalClient mysqlDbHospital = check new(); @@ -82,7 +88,8 @@ function testCreateAppointmentMySql() returns error? { } @test:Config{ - dependsOn: [testCreatePatientMySql, testCreateDoctorMySql, testCreateAppointmentMySql] + groups: ["annotation", "mysql"], + dependsOn: [testCreatePatientMySql, testCreateDoctorMySql, testCreateAppointmentMySql] } function testCreateAppointmentAlreadyExistsMySql() returns error? { MySqlHospitalClient mysqlDbHospital = check new(); @@ -101,7 +108,8 @@ function testCreateAppointmentAlreadyExistsMySql() returns error? { } @test:Config{ - dependsOn: [testCreateDoctorMySql] + groups: ["annotation", "mysql"], + dependsOn: [testCreateDoctorMySql] } function testGetDoctorsMySql() returns error? { MySqlHospitalClient mysqlDbHospital = check new(); @@ -114,7 +122,8 @@ function testGetDoctorsMySql() returns error? { } @test:Config{ - dependsOn: [testCreatePatientMySql] + groups: ["annotation", "mysql"], + dependsOn: [testCreatePatientMySql] } function testGetPatientByIdMySql() returns error? { MySqlHospitalClient mysqlDbHospital = check new(); @@ -123,7 +132,9 @@ function testGetPatientByIdMySql() returns error? { test:assertEquals(patient, expected, "Patient details should be returned"); } -@test:Config{} +@test:Config{ + groups: ["annotation", "mysql"] +} function testGetPatientNotFoundMySql() returns error? { MySqlHospitalClient mysqlDbHospital = check new(); Patient|persist:Error patient = mysqlDbHospital->/patients/[10].get(); @@ -133,7 +144,8 @@ function testGetPatientNotFoundMySql() returns error? { } @test:Config{ - dependsOn: [testCreateAppointmentMySql] + groups: ["annotation", "mysql"], + dependsOn: [testCreateAppointmentMySql] } function testGetAppointmentByDoctorMySql() returns error? { MySqlHospitalClient mysqlDbHospital = check new(); @@ -189,7 +201,8 @@ function testGetAppointmentByDoctorMySql() returns error? { } @test:Config{ - dependsOn: [testCreateAppointmentMySql] + groups: ["annotation", "mysql"], + dependsOn: [testCreateAppointmentMySql] } function testGetAppointmentByPatientMySql() returns error? { MySqlHospitalClient mysqlDbHospital = check new(); @@ -238,7 +251,8 @@ function testGetAppointmentByPatientMySql() returns error? { } @test:Config{ - dependsOn: [testCreateAppointmentMySql, testGetAppointmentByDoctorMySql, testGetAppointmentByPatientMySql] + groups: ["annotation", "mysql"], + dependsOn: [testCreateAppointmentMySql, testGetAppointmentByDoctorMySql, testGetAppointmentByPatientMySql] } function testPatchAppointmentMySql() returns error? { MySqlHospitalClient mysqlDbHospital = check new(); @@ -290,7 +304,8 @@ function testPatchAppointmentMySql() returns error? { } @test:Config{ - dependsOn: [testCreateAppointmentMySql, testGetAppointmentByDoctorMySql, testGetAppointmentByPatientMySql, testPatchAppointmentMySql] + groups: ["annotation", "mysql"], + dependsOn: [testCreateAppointmentMySql, testGetAppointmentByDoctorMySql, testGetAppointmentByPatientMySql, testPatchAppointmentMySql] } function testDeleteAppointmentByPatientIdMySql() returns error? { MySqlHospitalClient mysqlDbHospital = check new(); @@ -321,7 +336,8 @@ function testDeleteAppointmentByPatientIdMySql() returns error? { } @test:Config{ - dependsOn: [testGetPatientByIdMySql, testDeleteAppointmentByPatientIdMySql] + groups: ["annotation", "mysql"], + dependsOn: [testGetPatientByIdMySql, testDeleteAppointmentByPatientIdMySql] } function testDeletePatientMySql() returns error? { MySqlHospitalClient mysqlDbHospital = check new(); @@ -332,7 +348,8 @@ function testDeletePatientMySql() returns error? { } @test:Config{ - dependsOn: [testGetDoctorsMySql, testDeleteAppointmentByPatientIdMySql] + groups: ["annotation", "mysql"], + dependsOn: [testGetDoctorsMySql, testDeleteAppointmentByPatientIdMySql] } function testDeleteDoctorMySql() returns error? { MySqlHospitalClient mysqlDbHospital = check new(); diff --git a/ballerina/tests/persist/hospital.bal b/ballerina/tests/persist/hospital.bal new file mode 100644 index 0000000..9d3ed0c --- /dev/null +++ b/ballerina/tests/persist/hospital.bal @@ -0,0 +1,76 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.org). +// +// 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/persist as _; +import ballerina/time; +import ballerinax/persist.sql; + +public enum AppointmentStatus { + SCHEDULED = "SCHEDULED", + STARTED = "STARTED", + ENDED = "ENDED" +} + +public enum PatientGender { + MALE = "MALE", + FEMALE = "FEMALE" +} + +@sql:Name {value: "appointment"} +public type Appointment record {| + readonly int id; + @sql:UniqueIndex {name: "reason_index"} + string reason; + time:Civil appointmentTime; + AppointmentStatus status; + @sql:Name {value: "patient_id"} + @sql:Index {name: "patient_id"} + int patientId; + @sql:Index {name: "doctorId"} + int doctorId; + @sql:Relation {keys: ["patientId"]} + Patient patient; + @sql:Relation {keys: ["doctorId"]} + Doctor doctor; +|}; + +@sql:Name {value: "patients"} +public type Patient record {| + @sql:Name {value: "ID_P"} + @sql:Generated + readonly int idP; + string name; + int age; + @sql:Name {value: "ADDRESS"} + string address; + @sql:Char {length: 10} + string phoneNumber; + PatientGender gender; + Appointment[] appointments; +|}; + +public type Doctor record {| + readonly int id; + string name; + @sql:Varchar {length: 20} + @sql:Index {name: "specialty_index"} + string specialty; + @sql:Name {value: "phone_number"} + string phoneNumber; + @sql:Decimal {precision: [10, 2]} + decimal? salary; + Appointment[] appointments; +|}; diff --git a/ballerina/tests/postgresql_hospital_tests.bal b/ballerina/tests/postgresql_hospital_tests.bal index a7cc584..25a4bd5 100644 --- a/ballerina/tests/postgresql_hospital_tests.bal +++ b/ballerina/tests/postgresql_hospital_tests.bal @@ -18,7 +18,9 @@ import ballerina/persist; import ballerina/test; -@test:Config{} +@test:Config { + groups: ["annotation", "postgresql"] +} function testCreatePatientPostgreSql() returns error? { PostgreSqlHospitalClient postgreSqlDbHospital = check new(); PatientInsert patient = { @@ -31,7 +33,9 @@ function testCreatePatientPostgreSql() returns error? { _ = check postgreSqlDbHospital->/patients.post([patient]); } -@test:Config{} +@test:Config { + groups: ["annotation", "postgresql"] +} function testCreateDoctorPostgreSql() returns error? { PostgreSqlHospitalClient postgresSqlDbHospital = check new(); DoctorInsert doctor = { @@ -44,8 +48,9 @@ function testCreateDoctorPostgreSql() returns error? { _ = check postgresSqlDbHospital->/doctors.post([doctor]); } -@test:Config{ - dependsOn: [testCreateDoctorPostgreSql] +@test:Config { + groups: ["annotation", "postgresql"], + dependsOn: [testCreateDoctorPostgreSql] } function testCreateDoctorAlreadyExistsPostgreSql() returns error? { PostgreSqlHospitalClient postgresSqlDbHospital = check new(); @@ -62,8 +67,9 @@ function testCreateDoctorAlreadyExistsPostgreSql() returns error? { } } -@test:Config{ - dependsOn: [testCreatePatientPostgreSql, testCreateDoctorPostgreSql] +@test:Config { + groups: ["annotation", "postgresql"], + dependsOn: [testCreatePatientPostgreSql, testCreateDoctorPostgreSql] } function testCreateAppointmentPostgreSql() returns error? { PostgreSqlHospitalClient postgresSqlDbHospital = check new(); @@ -78,8 +84,9 @@ function testCreateAppointmentPostgreSql() returns error? { _ = check postgresSqlDbHospital->/appointments.post([appointment]); } -@test:Config{ - dependsOn: [testCreatePatientPostgreSql, testCreateDoctorPostgreSql, testCreateAppointmentPostgreSql] +@test:Config { + groups: ["annotation", "postgresql"], + dependsOn: [testCreatePatientPostgreSql, testCreateDoctorPostgreSql, testCreateAppointmentPostgreSql] } function testCreateAppointmentAlreadyExistsPostgreSql() returns error? { PostgreSqlHospitalClient postgresSqlDbHospital = check new(); @@ -97,8 +104,9 @@ function testCreateAppointmentAlreadyExistsPostgreSql() returns error? { } } -@test:Config{ - dependsOn: [testCreateDoctorPostgreSql] +@test:Config { + groups: ["annotation", "postgresql"], + dependsOn: [testCreateDoctorPostgreSql] } function testGetDoctorsPostgreSql() returns error? { PostgreSqlHospitalClient postgresSqlDbHospital = check new(); @@ -110,8 +118,9 @@ function testGetDoctorsPostgreSql() returns error? { test:assertEquals(doctorsArr, expected, "Doctor details should be returned"); } -@test:Config{ - dependsOn: [testCreatePatientPostgreSql] +@test:Config { + groups: ["annotation", "postgresql"], + dependsOn: [testCreatePatientPostgreSql] } function testGetPatientByIdPostgreSql() returns error? { PostgreSqlHospitalClient postgresSqlDbHospital = check new(); @@ -120,7 +129,9 @@ function testGetPatientByIdPostgreSql() returns error? { test:assertEquals(patient, expected, "Patient details should be returned"); } -@test:Config{} +@test:Config { + groups: ["annotation", "postgresql"] +} function testGetPatientNotFoundPostgreSql() returns error? { PostgreSqlHospitalClient postgresSqlDbHospital = check new(); Patient|persist:Error patient = postgresSqlDbHospital->/patients/[10].get(); @@ -129,8 +140,9 @@ function testGetPatientNotFoundPostgreSql() returns error? { } } -@test:Config{ - dependsOn: [testCreateAppointmentPostgreSql] +@test:Config { + groups: ["annotation", "postgresql"], + dependsOn: [testCreateAppointmentPostgreSql] } function testGetAppointmentByDoctorPostgreSql() returns error? { PostgreSqlHospitalClient postgresSqlDbHospital = check new(); @@ -185,8 +197,9 @@ function testGetAppointmentByDoctorPostgreSql() returns error? { test:assertEquals(filteredAppointments2, [], "Appointment details should be empty"); } -@test:Config{ - dependsOn: [testCreateAppointmentPostgreSql] +@test:Config { + groups: ["annotation", "postgresql"], + dependsOn: [testCreateAppointmentPostgreSql] } function testGetAppointmentByPatientPostgreSql() returns error? { PostgreSqlHospitalClient postgresSqlDbHospital = check new(); @@ -234,8 +247,9 @@ function testGetAppointmentByPatientPostgreSql() returns error? { test:assertEquals(filteredAppointments2, [], "Appointment details should be empty"); } -@test:Config{ - dependsOn: [testCreateAppointmentPostgreSql, testGetAppointmentByDoctorPostgreSql, testGetAppointmentByPatientPostgreSql] +@test:Config { + groups: ["annotation", "postgresql"], + dependsOn: [testCreateAppointmentPostgreSql, testGetAppointmentByDoctorPostgreSql, testGetAppointmentByPatientPostgreSql] } function testPatchAppointmentPostgreSql() returns error? { PostgreSqlHospitalClient postgresSqlDbHospital = check new(); @@ -286,8 +300,9 @@ function testPatchAppointmentPostgreSql() returns error? { } } -@test:Config{ - dependsOn: [testCreateAppointmentPostgreSql, testGetAppointmentByDoctorPostgreSql, testGetAppointmentByPatientPostgreSql, testPatchAppointmentPostgreSql] +@test:Config { + groups: ["annotation", "postgresql"], + dependsOn: [testCreateAppointmentPostgreSql, testGetAppointmentByDoctorPostgreSql, testGetAppointmentByPatientPostgreSql, testPatchAppointmentPostgreSql] } function testDeleteAppointmentByPatientIdPostgreSql() returns error? { PostgreSqlHospitalClient postgresSqlDbHospital = check new(); @@ -317,8 +332,9 @@ function testDeleteAppointmentByPatientIdPostgreSql() returns error? { test:assertEquals(result3, [], "Appointment details should be empty"); } -@test:Config{ - dependsOn: [testGetPatientByIdPostgreSql, testDeleteAppointmentByPatientIdPostgreSql] +@test:Config { + groups: ["annotation", "postgresql"], + dependsOn: [testGetPatientByIdPostgreSql, testDeleteAppointmentByPatientIdPostgreSql] } function testDeletePatientPostgreSql() returns error? { PostgreSqlHospitalClient postgresSqlDbHospital = check new(); @@ -328,8 +344,9 @@ function testDeletePatientPostgreSql() returns error? { } } -@test:Config{ - dependsOn: [testGetDoctorsPostgreSql, testDeleteAppointmentByPatientIdPostgreSql] +@test:Config { + groups: ["annotation", "postgresql"], + dependsOn: [testGetDoctorsPostgreSql, testDeleteAppointmentByPatientIdPostgreSql] } function testDeleteDoctorPostgreSql() returns error? { PostgreSqlHospitalClient postgresSqlDbHospital = check new(); diff --git a/build.gradle b/build.gradle index 91aadff..2d62704 100644 --- a/build.gradle +++ b/build.gradle @@ -93,9 +93,11 @@ subprojects { ballerinaStdLibs "io.ballerina.stdlib:mysql-ballerina:${stdlibMysqlVersion}" ballerinaStdLibs "io.ballerina.stdlib:mssql-ballerina:${stdlibMssqlVersion}" ballerinaStdLibs "io.ballerina.stdlib:postgresql-ballerina:${stdlibPostgresqlVersion}" + ballerinaStdLibs "io.ballerina.stdlib:java.jdbc-ballerina:${stdlibJdbcVersion}" ballerinaStdLibs "io.ballerina.stdlib:mysql.driver-ballerina:${stdlibMysqlDriverVersion}" ballerinaStdLibs "io.ballerina.stdlib:mssql.driver-ballerina:${stdlibMssqlDriverVersion}" ballerinaStdLibs "io.ballerina.stdlib:postgresql.driver-ballerina:${stdlibPostgresqlDriverVersion}" + ballerinaStdLibs "io.ballerina.stdlib:h2.driver-ballerina:${stdlibH2DriverVersion}" ballerinaStdLibs "io.ballerina.stdlib:observe-ballerina:${observeVersion}" ballerinaStdLibs "io.ballerina:observe-ballerina:${observeInternalVersion}" } diff --git a/changelog.md b/changelog.md index d5ca7da..883790f 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- [Added support for H2 DB as a datasource](https://github.com/ballerina-platform/ballerina-library/issues/5715) + +## [1.3.0] - 2024-05-03 + ### Added - [Added support for PostgreSQL as a datasource](https://github.com/ballerina-platform/ballerina-library/issues/5829) - [Added support for advanced SQL annotations](https://github.com/ballerina-platform/ballerina-library/issues/6013) diff --git a/gradle.properties b/gradle.properties index 746ea7c..e8a518f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=io.ballerina.stdlib -version=1.3.1-SNAPSHOT +version=1.4.0-SNAPSHOT puppycrawlCheckstyleVersion=10.12.1 checkstyleToolVersion=10.12.1 @@ -35,6 +35,7 @@ stdlibHttpVersion=2.11.0 stdlibSqlVersion=1.13.0 # Ballerina external dependency +stdlibH2DriverVersion=1.1.0 stdlibMysqlDriverVersion=1.6.0 stdlibMssqlDriverVersion=1.6.0 stdlibPostgresqlDriverVersion=1.5.1 @@ -67,7 +68,7 @@ stdlibTransactionVersion=1.9.0 stdlibMysqlVersion=1.12.0 stdlibMssqlVersion=1.12.0 stdlibPostgresqlVersion=1.12.0 - +stdlibJdbcVersion=1.11.0 # Enabled publishing insecure checksums, due to fail to publish to maven central # Refer https://github.com/gradle/gradle/issues/11308 diff --git a/native/src/main/java/io/ballerina/stdlib/persist/sql/datastore/H2Processor.java b/native/src/main/java/io/ballerina/stdlib/persist/sql/datastore/H2Processor.java new file mode 100644 index 0000000..1c65cfe --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/persist/sql/datastore/H2Processor.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org). + * + * 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. + */ + +package io.ballerina.stdlib.persist.sql.datastore; + +import io.ballerina.runtime.api.Environment; +import io.ballerina.runtime.api.values.BArray; +import io.ballerina.runtime.api.values.BObject; +import io.ballerina.runtime.api.values.BStream; +import io.ballerina.runtime.api.values.BTypedesc; + +public class H2Processor { + + private H2Processor() { + } + + public static BStream query(Environment env, BObject client, BTypedesc targetType, BObject whereClause, + BObject orderClause, BObject limitClause, BObject groupByClause) { + return SQLProcessor.query(env, client, targetType, whereClause, orderClause, limitClause, groupByClause); + } + + public static Object queryOne(Environment env, BObject client, BArray path, BTypedesc targetType) { + return SQLProcessor.queryOne(env, client, path, targetType); + } + + public static Object executeNativeSQL(Environment env, BObject client, BObject paramSQLString) { + return SQLProcessor.executeNativeSQL(env, client, paramSQLString); + } + + public static BStream queryNativeSQL(Environment env, BObject client, BObject paramSQLString, + BTypedesc targetType) { + return SQLProcessor.queryNativeSQL(env, client, paramSQLString, targetType); + } +}