diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index aefa18f..8a7de28 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerinax" name = "persist.sql" -version = "1.4.0" +version = "1.4.1" 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.4.0" -path = "../native/build/libs/persist.sql-native-1.4.0.jar" +version = "1.4.1" +path = "../native/build/libs/persist.sql-native-1.4.1-SNAPSHOT.jar" [[platform.java17.dependency]] groupId = "io.ballerina.stdlib" diff --git a/ballerina/CompilerPlugin.toml b/ballerina/CompilerPlugin.toml index b44f1c9..6bd6fbc 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.4.0.jar" +path = "../compiler-plugin/build/libs/persist.sql-compiler-plugin-1.4.1-SNAPSHOT.jar" [[dependency]] path = "./lib/persist-native-1.4.0.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index a237821..0969bec 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -66,7 +66,7 @@ dependencies = [ [[package]] org = "ballerina" name = "http" -version = "2.12.0" +version = "2.12.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "auth"}, @@ -100,6 +100,9 @@ dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "lang.value"} ] +modules = [ + {org = "ballerina", packageName = "io", moduleName = "io"} +] [[package]] org = "ballerina" @@ -241,7 +244,7 @@ modules = [ [[package]] org = "ballerina" name = "mime" -version = "2.10.0" +version = "2.10.1" scope = "testOnly" dependencies = [ {org = "ballerina", name = "io"}, @@ -296,7 +299,7 @@ modules = [ [[package]] org = "ballerina" name = "sql" -version = "1.14.0" +version = "1.14.1" dependencies = [ {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, @@ -437,7 +440,7 @@ modules = [ [[package]] org = "ballerinax" name = "mysql" -version = "1.13.0" +version = "1.13.1" scope = "testOnly" dependencies = [ {org = "ballerina", name = "crypto"}, @@ -462,8 +465,9 @@ modules = [ [[package]] org = "ballerinax" name = "persist.sql" -version = "1.4.0" +version = "1.4.1" dependencies = [ + {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "log"}, {org = "ballerina", name = "persist"}, diff --git a/ballerina/sql_client.bal b/ballerina/sql_client.bal index 9fc736c..57474f4 100644 --- a/ballerina/sql_client.bal +++ b/ballerina/sql_client.bal @@ -229,6 +229,62 @@ public isolated client class SQLClient { } } + # Check whether associated entries exist for the given record. + # + # + 'object - The record to which the retrieved records should be appended + # + fields - The fields to be retrieved + # + include - The relations to be retrieved (SQL `JOINs` to be performed) + # + return - `()` if the operation is performed successfully or a `persist:Error` if the operation fails + public isolated function verifyEntityAssociation(anydata 'object, string[] fields, string[] include) returns persist:Error? { + if 'object !is record {} { + return error persist:Error("The 'object' parameter should be a record"); + } + + do { + // check the values of included entities are () + foreach string joinKey in self.getJoinFields(include) { + JoinMetadata joinMetadata = self.joinMetadata.get(joinKey); + anydata associatedEntity = 'object.get(joinMetadata.fieldName); + if associatedEntity is record {} { + // check if the fields are empty in the associated record. + map nonEmptyAssocEntity = associatedEntity.filter(value => value != ()); + // If the associated entity has non-empty fields, then the association is already verified. + if nonEmptyAssocEntity.length() > 0 { + continue; + } + + // check if the associated record values contain the foreign fields, if so, we can skip the query. + boolean hasKeys = true; + foreach string refColumn in joinMetadata.refColumns { + if !associatedEntity.hasKey(refColumn) { + hasKeys = false; + break; + } + } + if hasKeys { + 'object[joinMetadata.fieldName] = (); + continue; + } + // construct the query to check whether the associated entries are exists + sql:ParameterizedQuery query = ``; + map whereFilter = check self.getManyRelationWhereFilter('object, joinMetadata); + query = sql:queryConcat( + ` SELECT COUNT(*) AS count`, + ` FROM `, stringToParameterizedQuery(self.escape(joinMetadata.refTable)), + ` WHERE`, check self.getWhereClauses(whereFilter, true) + ); + // execute the query and check the count of the associated entries + int count = check self.dbClient->queryRow(query); + if count == 0 { + 'object[joinMetadata.fieldName] = (); + } + } + } + } on fail error err { + return error persist:Error(err.message(), err); + } + } + public isolated function getKeyFields() returns string[] { return self.keyFields; } diff --git a/ballerina/stream_types.bal b/ballerina/stream_types.bal index bd09899..cfdd900 100644 --- a/ballerina/stream_types.bal +++ b/ballerina/stream_types.bal @@ -60,6 +60,8 @@ public class PersistSQLStream { return error(value.message()); } check (self.persistClient).getManyRelations(value, self.fields, self.include, self.typeDescriptions); + // verifies the entity association whether associated entity is available in the database. + check (self.persistClient).verifyEntityAssociation(value, self.fields, self.include); string[] keyFields = (self.persistClient).getKeyFields(); foreach string keyField in keyFields { diff --git a/ballerina/tests/api_subscription_types.bal b/ballerina/tests/api_subscription_types.bal new file mode 100644 index 0000000..1ba8862 --- /dev/null +++ b/ballerina/tests/api_subscription_types.bal @@ -0,0 +1,73 @@ +// 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. + +public type Subscription record {| + readonly string subscriptionId; + string userName; + string apimetadataApiId; + string apimetadataOrgId; +|}; + +public type SubscriptionOptionalized record {| + string subscriptionId?; + string userName?; + string apimetadataApiId?; + string apimetadataOrgId?; +|}; + +public type SubscriptionWithRelations record {| + *SubscriptionOptionalized; + ApiMetadataOptionalized apimetadata?; +|}; + +public type SubscriptionTargetType typedesc; + +public type SubscriptionInsert Subscription; + +public type SubscriptionUpdate record {| + string userName?; + string apimetadataApiId?; + string apimetadataOrgId?; +|}; + +public type ApiMetadata record {| + readonly string apiId; + readonly string orgId; + string apiName; + string metadata; + +|}; + +public type ApiMetadataOptionalized record {| + string apiId?; + string orgId?; + string apiName?; + string metadata?; +|}; + +public type ApiMetadataWithRelations record {| + *ApiMetadataOptionalized; + SubscriptionOptionalized subscription?; +|}; + +public type ApiMetadataTargetType typedesc; + +public type ApiMetadataInsert ApiMetadata; + +public type ApiMetadataUpdate record {| + string apiName?; + string metadata?; +|}; diff --git a/ballerina/tests/h2-optional-assoc-tests.bal b/ballerina/tests/h2-optional-assoc-tests.bal new file mode 100644 index 0000000..4b44c8b --- /dev/null +++ b/ballerina/tests/h2-optional-assoc-tests.bal @@ -0,0 +1,118 @@ +// 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: ["assoc", "h2"] +} +function h2APMNoRelationsTest() returns error? { + H2ApimClient apimClient = check new (); + + [string, string][] metaId = check apimClient->/apimetadata.post([{ + apiId: "123457", + orgId: "wso2", + apiName: "abc", + metadata: "metadata" + }]); + test:assertEquals(metaId, [["123457", "wso2"]]); + + stream streamResult = apimClient->/apimetadata(); + H2ApimWithSubscriptions[] apimResults = check from H2ApimWithSubscriptions apiMetadata in streamResult + select apiMetadata; + + test:assertEquals(apimResults.length(), 1); + test:assertEquals(apimResults[0].subscription, ()); + check apimClient.close(); +} + +@test:Config { + groups: ["assoc", "h2"], + dependsOn: [mssqlAPMNoRelationsTest, mssqlAPMWithRelationsTest] +} +function h2APMWithoutRelationsTest() returns error? { + H2ApimClient apimClient = check new (); + + stream streamResult = apimClient->/apimetadata(); + H2ApimWithoutSubscriptions[] apimResults = check from H2ApimWithoutSubscriptions apiMetadata in streamResult + select apiMetadata; + + test:assertEquals(apimResults.length(), 2); + test:assertEquals(apimResults[0], { + apiId: "123457", + orgId: "wso2", + apiName: "abc", + metadata: "metadata" +}); + check apimClient.close(); +} + +@test:Config { + groups: ["assoc", "h2"], + dependsOn: [h2APMNoRelationsTest] +} +function h2APMWithRelationsTest() returns error? { + H2ApimClient apimClient = check new (); + + [string, string][] metaId = check apimClient->/apimetadata.post([{ + apiId: "123458", + orgId: "wso2", + apiName: "abc", + metadata: "metadata" + }]); + test:assertEquals(metaId, [["123458", "wso2"]]); + + string[] subId = check apimClient->/subscriptions.post([{ + subscriptionId: "123", + userName: "ballerina", + apimetadataApiId: "123458", + apimetadataOrgId: "wso2" + }]); + test:assertEquals(subId, ["123"]); + + stream streamResult = apimClient->/apimetadata(); + H2ApimWithSubscriptions[] apimResults = check from H2ApimWithSubscriptions apiMetadata in streamResult + select apiMetadata; + + test:assertEquals(apimResults.length(), 2); + + test:assertEquals(apimResults[0].subscription, ()); + test:assertEquals(apimResults[1].subscription, { + subscriptionId: "123", + apimetadataApiId: "123458" + }); + check apimClient.close(); +} + +type H2ApimWithSubscriptions record {| + string apiId; + string orgId; + string apiName; + string metadata; + record {| + string subscriptionId; + string apimetadataApiId; + |} subscription?; +|}; + + +type H2ApimWithoutSubscriptions record {| + string apiId; + string orgId; + string apiName; + string metadata; +|}; diff --git a/ballerina/tests/h2_api_subscription_client.bal b/ballerina/tests/h2_api_subscription_client.bal new file mode 100644 index 0000000..c707aa5 --- /dev/null +++ b/ballerina/tests/h2_api_subscription_client.bal @@ -0,0 +1,173 @@ +// 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; + +const SUBSCRIPTION = "subscriptions"; +const API_METADATA = "apimetadata"; + +public isolated client class H2ApimClient { + *persist:AbstractPersistClient; + + private final jdbc:Client dbClient; + + private final map persistClients; + + private final record {|SQLMetadata...;|} & readonly metadata = { + [SUBSCRIPTION]: { + entityName: "Subscription", + tableName: "Subscription", + fieldMetadata: { + subscriptionId: {columnName: "subscriptionId"}, + userName: {columnName: "userName"}, + apimetadataApiId: {columnName: "apimetadataApiId"}, + apimetadataOrgId: {columnName: "apimetadataOrgId"}, + "apimetadata.apiId": {relation: {entityName: "apimetadata", refField: "apiId"}}, + "apimetadata.orgId": {relation: {entityName: "apimetadata", refField: "orgId"}}, + "apimetadata.apiName": {relation: {entityName: "apimetadata", refField: "apiName"}}, + "apimetadata.metadata": {relation: {entityName: "apimetadata", refField: "metadata"}} + }, + keyFields: ["subscriptionId"], + joinMetadata: {apimetadata: {entity: ApiMetadata, fieldName: "apimetadata", refTable: "ApiMetadata", refColumns: ["apiId", "orgId"], joinColumns: ["apimetadataApiId", "apimetadataOrgId"], 'type: ONE_TO_ONE}} + }, + [API_METADATA]: { + entityName: "ApiMetadata", + tableName: "ApiMetadata", + fieldMetadata: { + apiId: {columnName: "apiId"}, + orgId: {columnName: "orgId"}, + apiName: {columnName: "apiName"}, + metadata: {columnName: "metadata"}, + "subscription.subscriptionId": {relation: {entityName: "subscription", refField: "subscriptionId"}}, + "subscription.userName": {relation: {entityName: "subscription", refField: "userName"}}, + "subscription.apimetadataApiId": {relation: {entityName: "subscription", refField: "apimetadataApiId"}}, + "subscription.apimetadataOrgId": {relation: {entityName: "subscription", refField: "apimetadataOrgId"}} + }, + keyFields: ["apiId", "orgId"], + joinMetadata: {subscription: {entity: Subscription, fieldName: "subscription", refTable: "Subscription", refColumns: ["apimetadataApiId", "apimetadataOrgId"], joinColumns: ["apiId", "orgId"], '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 = { + [SUBSCRIPTION]: check new (dbClient, self.metadata.get(SUBSCRIPTION), H2_SPECIFICS), + [API_METADATA]: check new (dbClient, self.metadata.get(API_METADATA), H2_SPECIFICS) + }; + } + + isolated resource function get subscriptions(SubscriptionTargetType 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 subscriptions/[string subscriptionId](SubscriptionTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "queryOne" + } external; + + isolated resource function post subscriptions(SubscriptionInsert[] data) returns string[]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(SUBSCRIPTION); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from SubscriptionInsert inserted in data + select inserted.subscriptionId; + } + + isolated resource function put subscriptions/[string subscriptionId](SubscriptionUpdate value) returns Subscription|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(SUBSCRIPTION); + } + _ = check sqlClient.runUpdateQuery(subscriptionId, value); + return self->/subscriptions/[subscriptionId].get(); + } + + isolated resource function delete subscriptions/[string subscriptionId]() returns Subscription|persist:Error { + Subscription result = check self->/subscriptions/[subscriptionId].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(SUBSCRIPTION); + } + _ = check sqlClient.runDeleteQuery(subscriptionId); + return result; + } + + isolated resource function get apimetadata(ApiMetadataTargetType 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 apimetadata/[string apiId]/[string orgId](ApiMetadataTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.H2Processor", + name: "queryOne" + } external; + + isolated resource function post apimetadata(ApiMetadataInsert[] data) returns [string, string][]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(API_METADATA); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from ApiMetadataInsert inserted in data + select [inserted.apiId, inserted.orgId]; + } + + isolated resource function put apimetadata/[string apiId]/[string orgId](ApiMetadataUpdate value) returns ApiMetadata|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(API_METADATA); + } + _ = check sqlClient.runUpdateQuery({"apiId": apiId, "orgId": orgId}, value); + return self->/apimetadata/[apiId]/[orgId].get(); + } + + isolated resource function delete apimetadata/[string apiId]/[string orgId]() returns ApiMetadata|persist:Error { + ApiMetadata result = check self->/apimetadata/[apiId]/[orgId].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(API_METADATA); + } + _ = check sqlClient.runDeleteQuery({"apiId": apiId, "orgId": orgId}); + 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 1c93395..cc769c7 100644 --- a/ballerina/tests/init-tests.bal +++ b/ballerina/tests/init-tests.bal @@ -86,6 +86,8 @@ function initMySqlTests() returns error? { _ = check mysqlDbClient->execute(`TRUNCATE Doctor`); _ = check mysqlDbClient->execute(`TRUNCATE appointment`); _ = check mysqlDbClient->execute(`TRUNCATE patients`); + _ = check mysqlDbClient->execute(`TRUNCATE Subscription`); + _ = check mysqlDbClient->execute(`TRUNCATE ApiMetadata`); _ = check mysqlDbClient->execute(`SET FOREIGN_KEY_CHECKS = 1`); check mysqlDbClient.close(); } @@ -275,6 +277,26 @@ function initMsSqlTests() returns error? { PRIMARY KEY([id]) ); `); + _ = check mssqlDbClient->execute(` + CREATE TABLE [ApiMetadata] ( + [apiId] VARCHAR(191) NOT NULL, + [orgId] VARCHAR(191) NOT NULL, + [apiName] VARCHAR(191) NOT NULL, + [metadata] VARCHAR(191) NOT NULL, + PRIMARY KEY([apiId],[orgId]) + ); + `); + _ = check mssqlDbClient->execute(` + CREATE TABLE [Subscription] ( + [subscriptionId] VARCHAR(191) NOT NULL, + [userName] VARCHAR(191) NOT NULL, + [apimetadataApiId] VARCHAR(191) NOT NULL, + [apimetadataOrgId] VARCHAR(191) NOT NULL, + UNIQUE ([apimetadataApiId], [apimetadataOrgId]), + FOREIGN KEY([apimetadataApiId], [apimetadataOrgId]) REFERENCES [ApiMetadata]([apiId], [orgId]), + PRIMARY KEY([subscriptionId]) + ); + `); } function initPostgreSqlTests() returns error? { @@ -292,6 +314,8 @@ function initPostgreSqlTests() returns error? { _ = check postgresqlDbClient->execute(`TRUNCATE "IntIdRecord" CASCADE`); _ = check postgresqlDbClient->execute(`TRUNCATE "AllTypesIdRecord" CASCADE`); _ = check postgresqlDbClient->execute(`TRUNCATE "CompositeAssociationRecord" CASCADE`); + _ = check postgresqlDbClient->execute(`TRUNCATE "Subscription" CASCADE`); + _ = check postgresqlDbClient->execute(`TRUNCATE "ApiMetadata" CASCADE`); check postgresqlDbClient.close(); } @@ -492,6 +516,26 @@ function initH2Tests() returns error? { PRIMARY KEY("id") )`); + _ = check h2DbClient->execute(`DROP TABLE IF EXISTS "Subscription"`); + _ = check h2DbClient->execute(`DROP TABLE IF EXISTS "ApiMetadata"`); + _ = check h2DbClient->execute(`CREATE TABLE "ApiMetadata" ( + "apiId" VARCHAR(191) NOT NULL, + "orgId" VARCHAR(191) NOT NULL, + "apiName" VARCHAR(191) NOT NULL, + "metadata" VARCHAR(191) NOT NULL, + PRIMARY KEY("apiId","orgId") + )`); + + _ = check h2DbClient->execute(`CREATE TABLE "Subscription" ( + "subscriptionId" VARCHAR(191) NOT NULL, + "userName" VARCHAR(191) NOT NULL, + "apimetadataApiId" VARCHAR(191) NOT NULL, + "apimetadataOrgId" VARCHAR(191) NOT NULL, + UNIQUE ("apimetadataApiId", "apimetadataOrgId"), + FOREIGN KEY("apimetadataApiId", "apimetadataOrgId") REFERENCES "ApiMetadata"("apiId", "orgId"), + PRIMARY KEY("subscriptionId") + )`); + check h2DbClient.close(); } diff --git a/ballerina/tests/mssql-optional-assoc-tests.bal b/ballerina/tests/mssql-optional-assoc-tests.bal new file mode 100644 index 0000000..584f7db --- /dev/null +++ b/ballerina/tests/mssql-optional-assoc-tests.bal @@ -0,0 +1,117 @@ +// 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: ["assoc", "mssql"] +} +function mssqlAPMNoRelationsTest() returns error? { + MsSqlApimClient apimClient = check new (); + + [string, string][] metaId = check apimClient->/apimetadata.post([{ + apiId: "123457", + orgId: "wso2", + apiName: "abc", + metadata: "metadata" + }]); + test:assertEquals(metaId, [["123457", "wso2"]]); + + stream streamResult = apimClient->/apimetadata(); + MsSqlAPIMWithSubscriptions[] apimResults = check from MsSqlAPIMWithSubscriptions apiMetadata in streamResult + select apiMetadata; + + test:assertEquals(apimResults.length(), 1); + test:assertEquals(apimResults[0].subscription, ()); + check apimClient.close(); +} + +@test:Config { + groups: ["assoc", "mssql"], + dependsOn: [mssqlAPMNoRelationsTest, mssqlAPMWithRelationsTest] +} +function mssqlAPMWithoutRelationsTest() returns error? { + MsSqlApimClient apimClient = check new (); + + stream streamResult = apimClient->/apimetadata(); + MsSqlApimWithoutSubscriptions[] apimResults = check from MsSqlApimWithoutSubscriptions apiMetadata in streamResult + select apiMetadata; + + test:assertEquals(apimResults.length(), 2); + test:assertEquals(apimResults[0], { + apiId: "123457", + orgId: "wso2", + apiName: "abc", + metadata: "metadata" +}); + check apimClient.close(); +} + +@test:Config { + groups: ["assoc", "mssql"], + dependsOn: [mssqlAPMNoRelationsTest] +} +function mssqlAPMWithRelationsTest() returns error? { + MsSqlApimClient apimClient = check new (); + + [string, string][] metaId = check apimClient->/apimetadata.post([{ + apiId: "123458", + orgId: "wso2", + apiName: "abc", + metadata: "metadata" + }]); + test:assertEquals(metaId, [["123458", "wso2"]]); + + string[] subId = check apimClient->/subscriptions.post([{ + subscriptionId: "123", + userName: "ballerina", + apimetadataApiId: "123458", + apimetadataOrgId: "wso2" + }]); + test:assertEquals(subId, ["123"]); + + stream streamResult = apimClient->/apimetadata(); + MsSqlAPIMWithSubscriptions[] apimResults = check from MsSqlAPIMWithSubscriptions apiMetadata in streamResult + select apiMetadata; + + test:assertEquals(apimResults.length(), 2); + + test:assertEquals(apimResults[0].subscription, ()); + test:assertEquals(apimResults[1].subscription, { + subscriptionId: "123", + apimetadataApiId: "123458" + }); + check apimClient.close(); +} + +type MsSqlAPIMWithSubscriptions record {| + string apiId; + string orgId; + string apiName; + string metadata; + record {| + string subscriptionId; + string apimetadataApiId; + |} subscription?; +|}; + +type MsSqlApimWithoutSubscriptions record {| + string apiId; + string orgId; + string apiName; + string metadata; +|}; diff --git a/ballerina/tests/mssql_api_subscription_client.bal b/ballerina/tests/mssql_api_subscription_client.bal new file mode 100644 index 0000000..b16d2ab --- /dev/null +++ b/ballerina/tests/mssql_api_subscription_client.bal @@ -0,0 +1,174 @@ +// 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/mssql; +import ballerinax/mssql.driver as _; + +const SUBSCRIPTION = "subscriptions"; +const API_METADATA = "apimetadata"; + +public isolated client class MsSqlApimClient { + *persist:AbstractPersistClient; + + private final mssql:Client dbClient; + + private final map persistClients; + + private final record {|SQLMetadata...;|} & readonly metadata = { + [SUBSCRIPTION]: { + entityName: "Subscription", + tableName: "Subscription", + fieldMetadata: { + subscriptionId: {columnName: "subscriptionId"}, + userName: {columnName: "userName"}, + apimetadataApiId: {columnName: "apimetadataApiId"}, + apimetadataOrgId: {columnName: "apimetadataOrgId"}, + "apimetadata.apiId": {relation: {entityName: "apimetadata", refField: "apiId"}}, + "apimetadata.orgId": {relation: {entityName: "apimetadata", refField: "orgId"}}, + "apimetadata.apiName": {relation: {entityName: "apimetadata", refField: "apiName"}}, + "apimetadata.metadata": {relation: {entityName: "apimetadata", refField: "metadata"}} + }, + keyFields: ["subscriptionId"], + joinMetadata: {apimetadata: {entity: ApiMetadata, fieldName: "apimetadata", refTable: "ApiMetadata", refColumns: ["apiId", "orgId"], joinColumns: ["apimetadataApiId", "apimetadataOrgId"], 'type: ONE_TO_ONE}} + }, + [API_METADATA]: { + entityName: "ApiMetadata", + tableName: "ApiMetadata", + fieldMetadata: { + apiId: {columnName: "apiId"}, + orgId: {columnName: "orgId"}, + apiName: {columnName: "apiName"}, + metadata: {columnName: "metadata"}, + "subscription.subscriptionId": {relation: {entityName: "subscription", refField: "subscriptionId"}}, + "subscription.userName": {relation: {entityName: "subscription", refField: "userName"}}, + "subscription.apimetadataApiId": {relation: {entityName: "subscription", refField: "apimetadataApiId"}}, + "subscription.apimetadataOrgId": {relation: {entityName: "subscription", refField: "apimetadataOrgId"}} + }, + keyFields: ["apiId", "orgId"], + joinMetadata: {subscription: {entity: Subscription, fieldName: "subscription", refTable: "Subscription", refColumns: ["apimetadataApiId", "apimetadataOrgId"], joinColumns: ["apiId", "orgId"], 'type: ONE_TO_ONE}} + } + }; + + public isolated function init() returns persist:Error? { + mssql:Client|error dbClient = new (host = mssql.host, user = mssql.user, password = mssql.password, database = mssql.database, port = mssql.port); + if dbClient is error { + return error persist:Error(dbClient.message()); + } + self.dbClient = dbClient; + self.persistClients = { + [SUBSCRIPTION]: check new (dbClient, self.metadata.get(SUBSCRIPTION), MSSQL_SPECIFICS), + [API_METADATA]: check new (dbClient, self.metadata.get(API_METADATA), MSSQL_SPECIFICS) + }; + } + + isolated resource function get subscriptions(SubscriptionTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.MSSQLProcessor", + name: "query" + } external; + + isolated resource function get subscriptions/[string subscriptionId](SubscriptionTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.MSSQLProcessor", + name: "queryOne" + } external; + + isolated resource function post subscriptions(SubscriptionInsert[] data) returns string[]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(SUBSCRIPTION); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from SubscriptionInsert inserted in data + select inserted.subscriptionId; + } + + isolated resource function put subscriptions/[string subscriptionId](SubscriptionUpdate value) returns Subscription|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(SUBSCRIPTION); + } + _ = check sqlClient.runUpdateQuery(subscriptionId, value); + return self->/subscriptions/[subscriptionId].get(); + } + + isolated resource function delete subscriptions/[string subscriptionId]() returns Subscription|persist:Error { + Subscription result = check self->/subscriptions/[subscriptionId].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(SUBSCRIPTION); + } + _ = check sqlClient.runDeleteQuery(subscriptionId); + return result; + } + + isolated resource function get apimetadata(ApiMetadataTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.MSSQLProcessor", + name: "query" + } external; + + isolated resource function get apimetadata/[string apiId]/[string orgId](ApiMetadataTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.MSSQLProcessor", + name: "queryOne" + } external; + + isolated resource function post apimetadata(ApiMetadataInsert[] data) returns [string, string][]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(API_METADATA); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from ApiMetadataInsert inserted in data + select [inserted.apiId, inserted.orgId]; + } + + isolated resource function put apimetadata/[string apiId]/[string orgId](ApiMetadataUpdate value) returns ApiMetadata|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(API_METADATA); + } + _ = check sqlClient.runUpdateQuery({"apiId": apiId, "orgId": orgId}, value); + return self->/apimetadata/[apiId]/[orgId].get(); + } + + isolated resource function delete apimetadata/[string apiId]/[string orgId]() returns ApiMetadata|persist:Error { + ApiMetadata result = check self->/apimetadata/[apiId]/[orgId].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(API_METADATA); + } + _ = check sqlClient.runDeleteQuery({"apiId": apiId, "orgId": orgId}); + return result; + } + + remote isolated function queryNativeSQL(sql:ParameterizedQuery sqlQuery, typedesc rowType = <>) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.MSSQLProcessor" + } external; + + remote isolated function executeNativeSQL(sql:ParameterizedQuery sqlQuery) returns ExecutionResult|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.MSSQLProcessor" + } 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/mysql-optional-assoc-tests.bal b/ballerina/tests/mysql-optional-assoc-tests.bal new file mode 100644 index 0000000..5f89885 --- /dev/null +++ b/ballerina/tests/mysql-optional-assoc-tests.bal @@ -0,0 +1,117 @@ +// 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: ["assoc", "mysql"] +} +function MySqlAPMNoRelationsTest() returns error? { + MySqlApimClient apimClient = check new (); + + [string, string][] metaId = check apimClient->/apimetadata.post([{ + apiId: "123457", + orgId: "wso2", + apiName: "abc", + metadata: "metadata" + }]); + test:assertEquals(metaId, [["123457", "wso2"]]); + + stream streamResult = apimClient->/apimetadata(); + MySqlAPIMWithSubscriptions[] apimResults = check from MySqlAPIMWithSubscriptions apiMetadata in streamResult + select apiMetadata; + + test:assertEquals(apimResults.length(), 1); + test:assertEquals(apimResults[0].subscription, ()); + check apimClient.close(); +} + +@test:Config { + groups: ["assoc", "mysql"], + dependsOn: [MySqlAPMNoRelationsTest, MySqlAPMWithRelationsTest] +} +function MySqlAPMWithoutRelationsTest() returns error? { + MySqlApimClient apimClient = check new (); + + stream streamResult = apimClient->/apimetadata(); + MySqlApimWithoutSubscriptions[] apimResults = check from MySqlApimWithoutSubscriptions apiMetadata in streamResult + select apiMetadata; + + test:assertEquals(apimResults.length(), 2); + test:assertEquals(apimResults[0], { + apiId: "123457", + orgId: "wso2", + apiName: "abc", + metadata: "metadata" +}); + check apimClient.close(); +} + +@test:Config { + groups: ["assoc", "mysql"], + dependsOn: [MySqlAPMNoRelationsTest] +} +function MySqlAPMWithRelationsTest() returns error? { + MySqlApimClient apimClient = check new (); + + [string, string][] metaId = check apimClient->/apimetadata.post([{ + apiId: "123458", + orgId: "wso2", + apiName: "abc", + metadata: "metadata" + }]); + test:assertEquals(metaId, [["123458", "wso2"]]); + + string[] subId = check apimClient->/subscriptions.post([{ + subscriptionId: "123", + userName: "ballerina", + apimetadataApiId: "123458", + apimetadataOrgId: "wso2" + }]); + test:assertEquals(subId, ["123"]); + + stream streamResult = apimClient->/apimetadata(); + MySqlAPIMWithSubscriptions[] apimResults = check from MySqlAPIMWithSubscriptions apiMetadata in streamResult + select apiMetadata; + + test:assertEquals(apimResults.length(), 2); + + + test:assertEquals(apimResults[1].subscription, { + subscriptionId: "123", + apimetadataApiId: "123458" + }); + check apimClient.close(); +} + +type MySqlAPIMWithSubscriptions record {| + string apiId; + string orgId; + string apiName; + string metadata; + record {| + string subscriptionId; + string apimetadataApiId; + |} subscription?; +|}; + +type MySqlApimWithoutSubscriptions record {| + string apiId; + string orgId; + string apiName; + string metadata; +|}; diff --git a/ballerina/tests/mysql_api_subscription_client.bal b/ballerina/tests/mysql_api_subscription_client.bal new file mode 100644 index 0000000..0793ecc --- /dev/null +++ b/ballerina/tests/mysql_api_subscription_client.bal @@ -0,0 +1,173 @@ +// 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/mysql; +import ballerinax/mysql.driver as _; + +const SUBSCRIPTION = "subscriptions"; +const API_METADATA = "apimetadata"; + +public isolated client class MySqlApimClient { + *persist:AbstractPersistClient; + + private final mysql:Client dbClient; + + private final map persistClients; + + private final record {|SQLMetadata...;|} & readonly metadata = { + [SUBSCRIPTION]: { + entityName: "Subscription", + tableName: "Subscription", + fieldMetadata: { + subscriptionId: {columnName: "subscriptionId"}, + userName: {columnName: "userName"}, + apimetadataApiId: {columnName: "apimetadataApiId"}, + apimetadataOrgId: {columnName: "apimetadataOrgId"}, + "apimetadata.apiId": {relation: {entityName: "apimetadata", refField: "apiId"}}, + "apimetadata.orgId": {relation: {entityName: "apimetadata", refField: "orgId"}}, + "apimetadata.apiName": {relation: {entityName: "apimetadata", refField: "apiName"}}, + "apimetadata.metadata": {relation: {entityName: "apimetadata", refField: "metadata"}} + }, + keyFields: ["subscriptionId"], + joinMetadata: {apimetadata: {entity: ApiMetadata, fieldName: "apimetadata", refTable: "ApiMetadata", refColumns: ["apiId", "orgId"], joinColumns: ["apimetadataApiId", "apimetadataOrgId"], 'type: ONE_TO_ONE}} + }, + [API_METADATA]: { + entityName: "ApiMetadata", + tableName: "ApiMetadata", + fieldMetadata: { + apiId: {columnName: "apiId"}, + orgId: {columnName: "orgId"}, + apiName: {columnName: "apiName"}, + metadata: {columnName: "metadata"}, + "subscription.subscriptionId": {relation: {entityName: "subscription", refField: "subscriptionId"}}, + "subscription.userName": {relation: {entityName: "subscription", refField: "userName"}}, + "subscription.apimetadataApiId": {relation: {entityName: "subscription", refField: "apimetadataApiId"}}, + "subscription.apimetadataOrgId": {relation: {entityName: "subscription", refField: "apimetadataOrgId"}} + }, + keyFields: ["apiId", "orgId"], + joinMetadata: {subscription: {entity: Subscription, fieldName: "subscription", refTable: "Subscription", refColumns: ["apimetadataApiId", "apimetadataOrgId"], joinColumns: ["apiId", "orgId"], 'type: ONE_TO_ONE}} + } + }; + + public isolated function init() returns persist:Error? { + mysql:Client|error dbClient = new (host = mysql.host, user = mysql.user, password = mysql.password, database = mysql.database, port = mysql.port); + if dbClient is error { + return error persist:Error(dbClient.message()); + } + self.dbClient = dbClient; + self.persistClients = { + [SUBSCRIPTION]: check new (dbClient, self.metadata.get(SUBSCRIPTION), MYSQL_SPECIFICS), + [API_METADATA]: check new (dbClient, self.metadata.get(API_METADATA), MYSQL_SPECIFICS) + }; + } + + isolated resource function get subscriptions(SubscriptionTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.MySQLProcessor", + name: "query" + } external; + + isolated resource function get subscriptions/[string subscriptionId](SubscriptionTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.MySQLProcessor", + name: "queryOne" + } external; + + isolated resource function post subscriptions(SubscriptionInsert[] data) returns string[]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(SUBSCRIPTION); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from SubscriptionInsert inserted in data + select inserted.subscriptionId; + } + + isolated resource function put subscriptions/[string subscriptionId](SubscriptionUpdate value) returns Subscription|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(SUBSCRIPTION); + } + _ = check sqlClient.runUpdateQuery(subscriptionId, value); + return self->/subscriptions/[subscriptionId].get(); + } + + isolated resource function delete subscriptions/[string subscriptionId]() returns Subscription|persist:Error { + Subscription result = check self->/subscriptions/[subscriptionId].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(SUBSCRIPTION); + } + _ = check sqlClient.runDeleteQuery(subscriptionId); + return result; + } + + isolated resource function get apimetadata(ApiMetadataTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.MySQLProcessor", + name: "query" + } external; + + isolated resource function get apimetadata/[string apiId]/[string orgId](ApiMetadataTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.MySQLProcessor", + name: "queryOne" + } external; + + isolated resource function post apimetadata(ApiMetadataInsert[] data) returns [string, string][]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(API_METADATA); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from ApiMetadataInsert inserted in data + select [inserted.apiId, inserted.orgId]; + } + + isolated resource function put apimetadata/[string apiId]/[string orgId](ApiMetadataUpdate value) returns ApiMetadata|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(API_METADATA); + } + _ = check sqlClient.runUpdateQuery({"apiId": apiId, "orgId": orgId}, value); + return self->/apimetadata/[apiId]/[orgId].get(); + } + + isolated resource function delete apimetadata/[string apiId]/[string orgId]() returns ApiMetadata|persist:Error { + ApiMetadata result = check self->/apimetadata/[apiId]/[orgId].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(API_METADATA); + } + _ = check sqlClient.runDeleteQuery({"apiId": apiId, "orgId": orgId}); + return result; + } + + remote isolated function queryNativeSQL(sql:ParameterizedQuery sqlQuery, typedesc rowType = <>) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.MySQLProcessor" + } external; + + remote isolated function executeNativeSQL(sql:ParameterizedQuery sqlQuery) returns ExecutionResult|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.MySQLProcessor" + } 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/mysql_hospital_persist_client.bal b/ballerina/tests/mysql_hospital_persist_client.bal index 9e0f80c..45f2bcb 100644 --- a/ballerina/tests/mysql_hospital_persist_client.bal +++ b/ballerina/tests/mysql_hospital_persist_client.bal @@ -105,7 +105,7 @@ public isolated client class MySqlHospitalClient { }; public isolated function init() returns persist:Error? { - mysql:Client|error dbClient = new (host = "localhost", user = "root", password = "Test123#", database = "test", port = 3305); + mysql:Client|error dbClient = new (host = mysql.host, user = mysql.user, password = mysql.password, database = mysql.database, port = mysql.port); if dbClient is error { return error(dbClient.message()); } diff --git a/ballerina/tests/persist/api_subs.bal b/ballerina/tests/persist/api_subs.bal new file mode 100644 index 0000000..98978e4 --- /dev/null +++ b/ballerina/tests/persist/api_subs.bal @@ -0,0 +1,31 @@ +// 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 _; + +public type Subscription record {| + readonly string subscriptionId; + string userName; + ApiMetadata apimetadata; +|}; + +public type ApiMetadata record {| + readonly string apiId; + readonly string orgId; + string apiName; + string metadata; + Subscription? subscription; +|}; diff --git a/ballerina/tests/postgresql-optional-assoc-tests.bal b/ballerina/tests/postgresql-optional-assoc-tests.bal new file mode 100644 index 0000000..5411f6d --- /dev/null +++ b/ballerina/tests/postgresql-optional-assoc-tests.bal @@ -0,0 +1,117 @@ +// 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: ["assoc", "postgresql"] +} +function PostgreSqlAPMNoRelationsTest() returns error? { + PostgreSqlApimClient apimClient = check new (); + + [string, string][] metaId = check apimClient->/apimetadata.post([{ + apiId: "123457", + orgId: "wso2", + apiName: "abc", + metadata: "metadata" + }]); + test:assertEquals(metaId, [["123457", "wso2"]]); + + stream streamResult = apimClient->/apimetadata(); + PostgreSqlAPIMWithSubscriptions[] apimResults = check from PostgreSqlAPIMWithSubscriptions apiMetadata in streamResult + select apiMetadata; + + test:assertEquals(apimResults.length(), 1); + test:assertEquals(apimResults[0].subscription, ()); + check apimClient.close(); +} + +@test:Config { + groups: ["assoc", "postgresql"], + dependsOn: [PostgreSqlAPMNoRelationsTest, PostgreSqlAPMWithRelationsTest] +} +function PostgreSqlAPMWithoutRelationsTest() returns error? { + PostgreSqlApimClient apimClient = check new (); + + stream streamResult = apimClient->/apimetadata(); + PostgreSqlApimWithoutSubscriptions[] apimResults = check from PostgreSqlApimWithoutSubscriptions apiMetadata in streamResult + select apiMetadata; + + test:assertEquals(apimResults.length(), 2); + test:assertEquals(apimResults[0], { + apiId: "123457", + orgId: "wso2", + apiName: "abc", + metadata: "metadata" +}); + check apimClient.close(); +} + +@test:Config { + groups: ["assoc", "postgresql"], + dependsOn: [PostgreSqlAPMNoRelationsTest] +} +function PostgreSqlAPMWithRelationsTest() returns error? { + PostgreSqlApimClient apimClient = check new (); + + [string, string][] metaId = check apimClient->/apimetadata.post([{ + apiId: "123458", + orgId: "wso2", + apiName: "abc", + metadata: "metadata" + }]); + test:assertEquals(metaId, [["123458", "wso2"]]); + + string[] subId = check apimClient->/subscriptions.post([{ + subscriptionId: "123", + userName: "ballerina", + apimetadataApiId: "123458", + apimetadataOrgId: "wso2" + }]); + test:assertEquals(subId, ["123"]); + + stream streamResult = apimClient->/apimetadata(); + PostgreSqlAPIMWithSubscriptions[] apimResults = check from PostgreSqlAPIMWithSubscriptions apiMetadata in streamResult + select apiMetadata; + + test:assertEquals(apimResults.length(), 2); + + + test:assertEquals(apimResults[1].subscription, { + subscriptionId: "123", + apimetadataApiId: "123458" + }); + check apimClient.close(); +} + +type PostgreSqlAPIMWithSubscriptions record {| + string apiId; + string orgId; + string apiName; + string metadata; + record {| + string subscriptionId; + string apimetadataApiId; + |} subscription?; +|}; + +type PostgreSqlApimWithoutSubscriptions record {| + string apiId; + string orgId; + string apiName; + string metadata; +|}; diff --git a/ballerina/tests/postgresql_api_subscription_client.bal b/ballerina/tests/postgresql_api_subscription_client.bal new file mode 100644 index 0000000..0e1ba02 --- /dev/null +++ b/ballerina/tests/postgresql_api_subscription_client.bal @@ -0,0 +1,174 @@ +// 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/postgresql; +import ballerinax/postgresql.driver as _; + +const SUBSCRIPTION = "subscriptions"; +const API_METADATA = "apimetadata"; + +public isolated client class PostgreSqlApimClient { + *persist:AbstractPersistClient; + + private final postgresql:Client dbClient; + + private final map persistClients; + + private final record {|SQLMetadata...;|} & readonly metadata = { + [SUBSCRIPTION]: { + entityName: "Subscription", + tableName: "Subscription", + fieldMetadata: { + subscriptionId: {columnName: "subscriptionId"}, + userName: {columnName: "userName"}, + apimetadataApiId: {columnName: "apimetadataApiId"}, + apimetadataOrgId: {columnName: "apimetadataOrgId"}, + "apimetadata.apiId": {relation: {entityName: "apimetadata", refField: "apiId"}}, + "apimetadata.orgId": {relation: {entityName: "apimetadata", refField: "orgId"}}, + "apimetadata.apiName": {relation: {entityName: "apimetadata", refField: "apiName"}}, + "apimetadata.metadata": {relation: {entityName: "apimetadata", refField: "metadata"}} + }, + keyFields: ["subscriptionId"], + joinMetadata: {apimetadata: {entity: ApiMetadata, fieldName: "apimetadata", refTable: "ApiMetadata", refColumns: ["apiId", "orgId"], joinColumns: ["apimetadataApiId", "apimetadataOrgId"], 'type: ONE_TO_ONE}} + }, + [API_METADATA]: { + entityName: "ApiMetadata", + tableName: "ApiMetadata", + fieldMetadata: { + apiId: {columnName: "apiId"}, + orgId: {columnName: "orgId"}, + apiName: {columnName: "apiName"}, + metadata: {columnName: "metadata"}, + "subscription.subscriptionId": {relation: {entityName: "subscription", refField: "subscriptionId"}}, + "subscription.userName": {relation: {entityName: "subscription", refField: "userName"}}, + "subscription.apimetadataApiId": {relation: {entityName: "subscription", refField: "apimetadataApiId"}}, + "subscription.apimetadataOrgId": {relation: {entityName: "subscription", refField: "apimetadataOrgId"}} + }, + keyFields: ["apiId", "orgId"], + joinMetadata: {subscription: {entity: Subscription, fieldName: "subscription", refTable: "Subscription", refColumns: ["apimetadataApiId", "apimetadataOrgId"], joinColumns: ["apiId", "orgId"], 'type: ONE_TO_ONE}} + } + }; + + public isolated function init() returns persist:Error? { + postgresql:Client|error dbClient = new (host = postgresql.host, username = postgresql.user, password = postgresql.password, database = postgresql.database, port = postgresql.port); + if dbClient is error { + return error(dbClient.message()); + } + self.dbClient = dbClient; + self.persistClients = { + [SUBSCRIPTION]: check new (dbClient, self.metadata.get(SUBSCRIPTION), POSTGRESQL_SPECIFICS), + [API_METADATA]: check new (dbClient, self.metadata.get(API_METADATA), POSTGRESQL_SPECIFICS) + }; + } + + isolated resource function get subscriptions(SubscriptionTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.PostgreSQLProcessor", + name: "query" + } external; + + isolated resource function get subscriptions/[string subscriptionId](SubscriptionTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.PostgreSQLProcessor", + name: "queryOne" + } external; + + isolated resource function post subscriptions(SubscriptionInsert[] data) returns string[]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(SUBSCRIPTION); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from SubscriptionInsert inserted in data + select inserted.subscriptionId; + } + + isolated resource function put subscriptions/[string subscriptionId](SubscriptionUpdate value) returns Subscription|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(SUBSCRIPTION); + } + _ = check sqlClient.runUpdateQuery(subscriptionId, value); + return self->/subscriptions/[subscriptionId].get(); + } + + isolated resource function delete subscriptions/[string subscriptionId]() returns Subscription|persist:Error { + Subscription result = check self->/subscriptions/[subscriptionId].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(SUBSCRIPTION); + } + _ = check sqlClient.runDeleteQuery(subscriptionId); + return result; + } + + isolated resource function get apimetadata(ApiMetadataTargetType targetType = <>, sql:ParameterizedQuery whereClause = ``, sql:ParameterizedQuery orderByClause = ``, sql:ParameterizedQuery limitClause = ``, sql:ParameterizedQuery groupByClause = ``) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.PostgreSQLProcessor", + name: "query" + } external; + + isolated resource function get apimetadata/[string apiId]/[string orgId](ApiMetadataTargetType targetType = <>) returns targetType|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.PostgreSQLProcessor", + name: "queryOne" + } external; + + isolated resource function post apimetadata(ApiMetadataInsert[] data) returns [string, string][]|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(API_METADATA); + } + _ = check sqlClient.runBatchInsertQuery(data); + return from ApiMetadataInsert inserted in data + select [inserted.apiId, inserted.orgId]; + } + + isolated resource function put apimetadata/[string apiId]/[string orgId](ApiMetadataUpdate value) returns ApiMetadata|persist:Error { + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(API_METADATA); + } + _ = check sqlClient.runUpdateQuery({"apiId": apiId, "orgId": orgId}, value); + return self->/apimetadata/[apiId]/[orgId].get(); + } + + isolated resource function delete apimetadata/[string apiId]/[string orgId]() returns ApiMetadata|persist:Error { + ApiMetadata result = check self->/apimetadata/[apiId]/[orgId].get(); + SQLClient sqlClient; + lock { + sqlClient = self.persistClients.get(API_METADATA); + } + _ = check sqlClient.runDeleteQuery({"apiId": apiId, "orgId": orgId}); + return result; + } + + remote isolated function queryNativeSQL(sql:ParameterizedQuery sqlQuery, typedesc rowType = <>) returns stream = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.PostgreSQLProcessor" + } external; + + remote isolated function executeNativeSQL(sql:ParameterizedQuery sqlQuery) returns ExecutionResult|persist:Error = @java:Method { + 'class: "io.ballerina.stdlib.persist.sql.datastore.PostgreSQLProcessor" + } 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/resources/mysql/init.sql b/ballerina/tests/resources/mysql/init.sql index da9af0d..3783822 100644 --- a/ballerina/tests/resources/mysql/init.sql +++ b/ballerina/tests/resources/mysql/init.sql @@ -164,3 +164,21 @@ CREATE TABLE test.appointment ( FOREIGN KEY(doctorId) REFERENCES Doctor(id), PRIMARY KEY(id) ); + +CREATE TABLE test.ApiMetadata ( + apiId VARCHAR(191) NOT NULL, + orgId VARCHAR(191) NOT NULL, + apiName VARCHAR(191) NOT NULL, + metadata VARCHAR(191) NOT NULL, + PRIMARY KEY(apiId,orgId) +); + +CREATE TABLE test.Subscription ( + subscriptionId VARCHAR(191) NOT NULL, + userName VARCHAR(191) NOT NULL, + apimetadataApiId VARCHAR(191) NOT NULL, + apimetadataOrgId VARCHAR(191) NOT NULL, + UNIQUE (apimetadataApiId, apimetadataOrgId), + FOREIGN KEY(apimetadataApiId, apimetadataOrgId) REFERENCES ApiMetadata(apiId, orgId), + PRIMARY KEY(subscriptionId) +); diff --git a/ballerina/tests/resources/postgresql/init.sql b/ballerina/tests/resources/postgresql/init.sql index 17a7c34..3b78041 100644 --- a/ballerina/tests/resources/postgresql/init.sql +++ b/ballerina/tests/resources/postgresql/init.sql @@ -151,3 +151,21 @@ CREATE TABLE "appointment" ( FOREIGN KEY("doctorId") REFERENCES "Doctor"("id"), PRIMARY KEY("id") ); + +CREATE TABLE "ApiMetadata" ( + "apiId" VARCHAR(191) NOT NULL, + "orgId" VARCHAR(191) NOT NULL, + "apiName" VARCHAR(191) NOT NULL, + "metadata" VARCHAR(191) NOT NULL, + PRIMARY KEY("apiId","orgId") +); + +CREATE TABLE "Subscription" ( + "subscriptionId" VARCHAR(191) NOT NULL, + "userName" VARCHAR(191) NOT NULL, + "apimetadataApiId" VARCHAR(191) NOT NULL, + "apimetadataOrgId" VARCHAR(191) NOT NULL, + UNIQUE ("apimetadataApiId", "apimetadataOrgId"), + FOREIGN KEY("apimetadataApiId", "apimetadataOrgId") REFERENCES "ApiMetadata"("apiId", "orgId"), + PRIMARY KEY("subscriptionId") +); diff --git a/changelog.md b/changelog.md index 883790f..384d179 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] +### Fixed +- [[Bug] Bal Persist fails when tries to retrieve record with a nonexistent associations](https://github.com/ballerina-platform/ballerina-library/issues/7304) + +## [1.4.0] - 2024-08-20 + ### Added - [Added support for H2 DB as a datasource](https://github.com/ballerina-platform/ballerina-library/issues/5715)