From df344bafaeaa7215eb81b1f1623b9a8c83d40149 Mon Sep 17 00:00:00 2001 From: Haritha Hasathcharu Date: Mon, 18 Mar 2024 17:16:31 +0530 Subject: [PATCH 1/3] Improve validation for foreign keys with annotations --- .../sql/compiler/CompilerPluginTest.java | 34 ++++ .../modelvalidator/persist/relation9.bal | 164 ++++++++++++++++++ .../PersistSqlModelDefinitionValidator.java | 125 +++++++++---- .../sql/compiler/pluginutils/PluginUtils.java | 45 ++--- 4 files changed, 314 insertions(+), 54 deletions(-) create mode 100644 compiler-plugin-test/src/test/resources/modelvalidator/persist/relation9.bal diff --git a/compiler-plugin-test/src/test/java/io/ballerina/stdlib/persist/sql/compiler/CompilerPluginTest.java b/compiler-plugin-test/src/test/java/io/ballerina/stdlib/persist/sql/compiler/CompilerPluginTest.java index 74795b9..ba062fd 100644 --- a/compiler-plugin-test/src/test/java/io/ballerina/stdlib/persist/sql/compiler/CompilerPluginTest.java +++ b/compiler-plugin-test/src/test/java/io/ballerina/stdlib/persist/sql/compiler/CompilerPluginTest.java @@ -358,6 +358,40 @@ public void validateRelationAnnotations8() { ); } + @Test(enabled = true) + public void validateRelationAnnotations9() { + List diagnostics = getErrorDiagnostics("modelvalidator", "relation9.bal", 5); + testDiagnostic( + diagnostics, + new String[]{ + PERSIST_SQL_424.getCode(), + PERSIST_SQL_424.getCode(), + PERSIST_SQL_424.getCode(), + PERSIST_SQL_424.getCode(), + PERSIST_SQL_424.getCode() + }, + new String[]{ + "invalid use of the `Relation` annotation. mismatched key types for the related entity " + + "'Person3'.", + "invalid use of the `Relation` annotation. mismatched key types for the related entity " + + "'Person2'.", + "invalid use of the `Relation` annotation. mismatched key types for the related entity " + + "'Person7'.", + "invalid use of the `Relation` annotation. mismatched key types for the related entity " + + "'Person5'.", + "invalid use of the `Relation` annotation. mismatched key types for the related entity " + + "'Person4'.", + }, + new String[]{ + "(77:4,78:18)", + "(56:4,57:18)", + "(161:4,162:18)", + "(119:4,120:18)", + "(98:4,99:18)" + } + ); + } + @Test(enabled = true) public void validateIndexAnnotation() { List diagnostics = getErrorDiagnostics("modelvalidator", "index.bal", 7); diff --git a/compiler-plugin-test/src/test/resources/modelvalidator/persist/relation9.bal b/compiler-plugin-test/src/test/resources/modelvalidator/persist/relation9.bal new file mode 100644 index 0000000..2e5a78c --- /dev/null +++ b/compiler-plugin-test/src/test/resources/modelvalidator/persist/relation9.bal @@ -0,0 +1,164 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/persist as _; +import ballerinax/persist.sql; + +public type Person record {| + @sql:Char {length:12} + readonly string nic; + string name; + int age; + string city; + Car? car; +|}; + +public type Car record {| + readonly string plateNo; + string make; + string model; + int year; + string color; + @sql:Char {length:12} + string ownerNic; + @sql:Relation {keys: ["ownerNic"]} + Person owner; +|}; + +public type Person2 record {| + readonly string nic; + string name; + int age; + string city; + Car2? car; +|}; + +public type Car2 record {| + readonly string plateNo; + string make; + string model; + int year; + string color; + @sql:Char {length:12} + string ownerNic; + @sql:Relation {keys: ["ownerNic"]} + Person2 owner; +|}; + +public type Person3 record {| + @sql:Varchar {length:12} + readonly string nic; + string name; + int age; + string city; + Car3? car; +|}; + +public type Car3 record {| + readonly string plateNo; + string make; + string model; + int year; + string color; + @sql:Char {length:12} + string ownerNic; + @sql:Relation {keys: ["ownerNic"]} + Person3 owner; +|}; + +public type Person4 record {| + @sql:Char {length:11} + readonly string nic; + string name; + int age; + string city; + Car4? car; +|}; + +public type Car4 record {| + readonly string plateNo; + string make; + string model; + int year; + string color; + @sql:Char {length:12} + string ownerNic; + @sql:Relation {keys: ["ownerNic"]} + Person4 owner; +|}; + +public type Person5 record {| + @sql:Char {length:11} + readonly string nic; + string name; + int age; + string city; + Car5? car; +|}; + +public type Car5 record {| + readonly string plateNo; + string make; + string model; + int year; + string color; + @sql:Varchar {length:12} + string ownerNic; + @sql:Relation {keys: ["ownerNic"]} + Person5 owner; +|}; + +public type Person6 record {| + @sql:Decimal {precision:[10,2]} + readonly decimal nic; + string name; + int age; + string city; + Car6? car; +|}; + +public type Car6 record {| + readonly string plateNo; + string make; + string model; + int year; + string color; + @sql:Decimal {precision:[10,2]} + decimal ownerNic; + @sql:Relation {keys: ["ownerNic"]} + Person6 owner; +|}; + +public type Person7 record {| + @sql:Decimal {precision:[10,2]} + readonly decimal nic; + string name; + int age; + string city; + Car7? car; +|}; + +public type Car7 record {| + readonly string plateNo; + string make; + string model; + int year; + string color; + @sql:Decimal {precision:[10,9]} + decimal ownerNic; + @sql:Relation {keys: ["ownerNic"]} + Person7 owner; +|}; diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/persist/sql/compiler/PersistSqlModelDefinitionValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/persist/sql/compiler/PersistSqlModelDefinitionValidator.java index de56510..a6ce4e0 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/persist/sql/compiler/PersistSqlModelDefinitionValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/persist/sql/compiler/PersistSqlModelDefinitionValidator.java @@ -41,6 +41,7 @@ import io.ballerina.stdlib.persist.compiler.model.IdentityField; import io.ballerina.stdlib.persist.compiler.model.RelationField; import io.ballerina.stdlib.persist.compiler.model.SimpleTypeField; +import io.ballerina.stdlib.persist.sql.compiler.pluginutils.PluginUtils; import java.io.File; import java.nio.file.Path; @@ -170,8 +171,8 @@ public void perform(SyntaxNodeAnalysisContext ctx) { .forEach(field -> validateRelationField(entity, field, refs))); //table mapping annotations if (isAnnotationPresent(entity.getAnnotations(), SQL_DB_NAME_ANNOTATION_NAME)) { - String tableName = readStringValueFromAnnotation - (entity.getAnnotations(), SQL_DB_NAME_ANNOTATION_NAME, ANNOTATION_VALUE_FIELD); + String tableName = readStringValueFromAnnotation(new PluginUtils.AnnotationUtilRecord( + entity.getAnnotations(), SQL_DB_NAME_ANNOTATION_NAME, ANNOTATION_VALUE_FIELD)); if (tableName.isEmpty()) { entity.reportDiagnostic(PERSIST_SQL_600.getCode(), PERSIST_SQL_600.getMessage(), PERSIST_SQL_600.getSeverity(), entity.getEntityNameLocation()); @@ -194,8 +195,8 @@ public void perform(SyntaxNodeAnalysisContext ctx) { List columnMappings = new ArrayList<>(); for (SimpleTypeField field : entity.getNonRelationFields()) { if (isAnnotationPresent(field.getAnnotations(), SQL_DB_NAME_ANNOTATION_NAME)) { - String mappingName = readStringValueFromAnnotation(field.getAnnotations(), - SQL_DB_NAME_ANNOTATION_NAME, ANNOTATION_VALUE_FIELD); + String mappingName = readStringValueFromAnnotation(new PluginUtils.AnnotationUtilRecord( + field.getAnnotations(), SQL_DB_NAME_ANNOTATION_NAME, ANNOTATION_VALUE_FIELD)); if (mappingName.isEmpty()) { entity.reportDiagnostic(PERSIST_SQL_600.getCode(), PERSIST_SQL_600.getMessage(), PERSIST_SQL_600.getSeverity(), field.getNodeLocation()); @@ -227,16 +228,16 @@ public void perform(SyntaxNodeAnalysisContext ctx) { entity.reportDiagnostic(PERSIST_SQL_605.getCode(), PERSIST_SQL_605.getMessage(), PERSIST_SQL_605.getSeverity(), field.getNodeLocation()); } else if (isCharPresent) { - String length = readStringValueFromAnnotation(field.getAnnotations(), - SQL_CHAR_MAPPING_ANNOTATION_NAME, ANNOTATION_LENGTH_FIELD); + String length = readStringValueFromAnnotation(new PluginUtils.AnnotationUtilRecord( + field.getAnnotations(), SQL_CHAR_MAPPING_ANNOTATION_NAME, ANNOTATION_LENGTH_FIELD)); if (length.equals("0")) { entity.reportDiagnostic(PERSIST_SQL_607.getCode(), MessageFormat.format(PERSIST_SQL_607.getMessage(), CHAR_ANNOTATION), PERSIST_SQL_607.getSeverity(), field.getNodeLocation()); } } else if (isVarCharPresent) { - String length = readStringValueFromAnnotation(field.getAnnotations(), - SQL_VARCHAR_MAPPING_ANNOTATION_NAME, ANNOTATION_LENGTH_FIELD); + String length = readStringValueFromAnnotation(new PluginUtils.AnnotationUtilRecord( + field.getAnnotations(), SQL_VARCHAR_MAPPING_ANNOTATION_NAME, ANNOTATION_LENGTH_FIELD)); if (length.equals("0")) { entity.reportDiagnostic(PERSIST_SQL_607.getCode(), MessageFormat.format(PERSIST_SQL_607.getMessage(), VARCHAR_ANNOTATION), @@ -256,8 +257,9 @@ public void perform(SyntaxNodeAnalysisContext ctx) { } if (isDecimalPresent) { if (field.getType().equals(DECIMAL)) { - List decimal = readStringArrayValueFromAnnotation(field.getAnnotations(), - SQL_DECIMAL_MAPPING_ANNOTATION_NAME, ANNOTATION_PRECISION_FIELD) + List decimal = readStringArrayValueFromAnnotation( + new PluginUtils.AnnotationUtilRecord(field.getAnnotations(), + SQL_DECIMAL_MAPPING_ANNOTATION_NAME, ANNOTATION_PRECISION_FIELD)) .stream().map(Integer::parseInt).toList(); if (decimal.get(0) == 0) { entity.reportDiagnostic(PERSIST_SQL_608.getCode(), @@ -275,18 +277,16 @@ public void perform(SyntaxNodeAnalysisContext ctx) { } } if (isAnnotationPresent(field.getAnnotations(), SQL_INDEX_MAPPING_ANNOTATION_NAME)) { - if (isAnnotationFieldStringType(field.getAnnotations(), SQL_INDEX_MAPPING_ANNOTATION_NAME, - ANNOTATION_NAME_FIELD)) { - String indexName = readStringValueFromAnnotation(field.getAnnotations(), - SQL_INDEX_MAPPING_ANNOTATION_NAME, ANNOTATION_NAME_FIELD); + PluginUtils.AnnotationUtilRecord annotationUtilRecord = new PluginUtils.AnnotationUtilRecord( + field.getAnnotations(), SQL_INDEX_MAPPING_ANNOTATION_NAME, ANNOTATION_NAME_FIELD); + if (isAnnotationFieldStringType(annotationUtilRecord)) { + String indexName = readStringValueFromAnnotation(annotationUtilRecord); if (indexName.isEmpty()) { entity.reportDiagnostic(PERSIST_SQL_622.getCode(), PERSIST_SQL_622.getMessage(), PERSIST_SQL_622.getSeverity(), field.getNodeLocation()); } - } else if (isAnnotationFieldArrayType(field.getAnnotations(), SQL_INDEX_MAPPING_ANNOTATION_NAME, - ANNOTATION_NAME_FIELD)) { - List indexNames = readStringArrayValueFromAnnotation(field.getAnnotations(), - SQL_INDEX_MAPPING_ANNOTATION_NAME, ANNOTATION_NAME_FIELD); + } else if (isAnnotationFieldArrayType(annotationUtilRecord)) { + List indexNames = readStringArrayValueFromAnnotation(annotationUtilRecord); if (indexNames.isEmpty()) { entity.reportDiagnostic(PERSIST_SQL_624.getCode(), PERSIST_SQL_624.getMessage(), PERSIST_SQL_624.getSeverity(), field.getNodeLocation()); @@ -304,18 +304,16 @@ public void perform(SyntaxNodeAnalysisContext ctx) { } } if (isAnnotationPresent(field.getAnnotations(), SQL_UNIQUE_INDEX_MAPPING_ANNOTATION_NAME)) { - if (isAnnotationFieldStringType(field.getAnnotations(), SQL_UNIQUE_INDEX_MAPPING_ANNOTATION_NAME, - ANNOTATION_NAME_FIELD)) { - String indexName = readStringValueFromAnnotation(field.getAnnotations(), - SQL_UNIQUE_INDEX_MAPPING_ANNOTATION_NAME, ANNOTATION_NAME_FIELD); + PluginUtils.AnnotationUtilRecord annotationUtilRecord = new PluginUtils.AnnotationUtilRecord( + field.getAnnotations(), SQL_UNIQUE_INDEX_MAPPING_ANNOTATION_NAME, ANNOTATION_NAME_FIELD); + if (isAnnotationFieldStringType(annotationUtilRecord)) { + String indexName = readStringValueFromAnnotation(annotationUtilRecord); if (indexName.isEmpty()) { entity.reportDiagnostic(PERSIST_SQL_623.getCode(), PERSIST_SQL_623.getMessage(), PERSIST_SQL_623.getSeverity(), field.getNodeLocation()); } - } else if (isAnnotationFieldArrayType(field.getAnnotations(), - SQL_UNIQUE_INDEX_MAPPING_ANNOTATION_NAME, ANNOTATION_NAME_FIELD)) { - List indexNames = readStringArrayValueFromAnnotation(field.getAnnotations(), - SQL_UNIQUE_INDEX_MAPPING_ANNOTATION_NAME, ANNOTATION_NAME_FIELD); + } else if (isAnnotationFieldArrayType(annotationUtilRecord)) { + List indexNames = readStringArrayValueFromAnnotation(annotationUtilRecord); if (indexNames.isEmpty()) { entity.reportDiagnostic(PERSIST_SQL_625.getCode(), PERSIST_SQL_625.getMessage(), PERSIST_SQL_625.getSeverity(), field.getNodeLocation()); @@ -489,10 +487,11 @@ private void validateRelationAnnotation(RelationField relationField, Entity owne return; } //annotation present, relationField is the owner - List referenceFields = readStringArrayValueFromAnnotation(relationField.getAnnotations(), - SQL_RELATION_MAPPING_ANNOTATION_NAME, ANNOTATION_KEYS_FIELD); - List referredIdFieldTypes = referredEntity.getIdentityFields().stream() - .map(IdentityField::getType).toList(); + List referenceFields = readStringArrayValueFromAnnotation(new PluginUtils.AnnotationUtilRecord( + relationField.getAnnotations(), SQL_RELATION_MAPPING_ANNOTATION_NAME, ANNOTATION_KEYS_FIELD)); + List referredIdFields = referredEntity.getIdentityFields().stream().map(idField -> + referredEntity.getNonRelationFields().stream().filter(field -> field.getName() + .equals(idField.getName())).findFirst().orElse(null)).toList(); List distinctReferenceFields = referenceFields.stream() .distinct() .toList(); @@ -512,10 +511,10 @@ private void validateRelationAnnotation(RelationField relationField, Entity owne return; } } - if (referenceFields.size() != referredIdFieldTypes.size()) { + if (referenceFields.size() != referredIdFields.size()) { ownerEntity.reportDiagnostic(PERSIST_SQL_423.getCode(), MessageFormat.format(PERSIST_SQL_423.getMessage(), relationField.getType(), - ownerEntity.getEntityName(), referredIdFieldTypes.size(), referenceFields.size()), + ownerEntity.getEntityName(), referredIdFields.size(), referenceFields.size()), PERSIST_SQL_423.getSeverity(), relationField.getLocation()); return; @@ -525,12 +524,55 @@ private void validateRelationAnnotation(RelationField relationField, Entity owne SimpleTypeField ownerField = ownerEntity.getNonRelationFields().stream() .filter(f -> f.getName().equals(referenceFields.get(finalI))).findFirst().orElse(null); if (ownerField != null) { - if (!ownerField.getType().equals(referredIdFieldTypes.get(finalI))) { + if (!ownerField.getType().equals(referredIdFields.get(finalI).getType())) { ownerEntity.reportDiagnostic(PERSIST_SQL_424.getCode(), MessageFormat.format(PERSIST_SQL_424.getMessage(), referredEntity.getEntityName()), PERSIST_SQL_424.getSeverity(), relationField.getLocation()); return; } + boolean ownerCharPresent = isAnnotationPresent(ownerField.getAnnotations(), + SQL_CHAR_MAPPING_ANNOTATION_NAME); + boolean ownerVarcharPresent = isAnnotationPresent(ownerField.getAnnotations(), + SQL_VARCHAR_MAPPING_ANNOTATION_NAME); + boolean ownerDecimalPresent = isAnnotationPresent(ownerField.getAnnotations(), + SQL_DECIMAL_MAPPING_ANNOTATION_NAME); + boolean referredCharPresent = isAnnotationPresent(referredIdFields.get(finalI).getAnnotations(), + SQL_CHAR_MAPPING_ANNOTATION_NAME); + boolean referredVarcharPresent = isAnnotationPresent(referredIdFields.get(finalI).getAnnotations(), + SQL_VARCHAR_MAPPING_ANNOTATION_NAME); + boolean referredDecimalPresent = isAnnotationPresent(referredIdFields.get(finalI).getAnnotations(), + SQL_DECIMAL_MAPPING_ANNOTATION_NAME); + if (ownerCharPresent != referredCharPresent || ownerVarcharPresent != referredVarcharPresent + || ownerDecimalPresent != referredDecimalPresent) { + ownerEntity.reportDiagnostic(PERSIST_SQL_424.getCode(), + MessageFormat.format(PERSIST_SQL_424.getMessage(), referredEntity.getEntityName()), + PERSIST_SQL_424.getSeverity(), relationField.getLocation()); + return; + } + if (ownerCharPresent) { + if (validateTypeLengthParam(relationField, ownerEntity, referredEntity, ownerField, + referredIdFields, finalI, SQL_CHAR_MAPPING_ANNOTATION_NAME)) { + return; + } + } else if (ownerVarcharPresent) { + if (validateTypeLengthParam(relationField, ownerEntity, referredEntity, ownerField, + referredIdFields, finalI, SQL_VARCHAR_MAPPING_ANNOTATION_NAME)) { + return; + } + } else if (ownerDecimalPresent) { + List ownerDecimal = readStringArrayValueFromAnnotation( + new PluginUtils.AnnotationUtilRecord(ownerField.getAnnotations(), + SQL_DECIMAL_MAPPING_ANNOTATION_NAME, ANNOTATION_PRECISION_FIELD)); + List referredDecimal = readStringArrayValueFromAnnotation( + new PluginUtils.AnnotationUtilRecord(referredIdFields.get(finalI).getAnnotations(), + SQL_DECIMAL_MAPPING_ANNOTATION_NAME, ANNOTATION_PRECISION_FIELD)); + if (!ownerDecimal.equals(referredDecimal)) { + ownerEntity.reportDiagnostic(PERSIST_SQL_424.getCode(), + MessageFormat.format(PERSIST_SQL_424.getMessage(), referredEntity.getEntityName()), + PERSIST_SQL_424.getSeverity(), relationField.getLocation()); + return; + } + } } } if (new HashSet<>(refs).containsAll(referenceFields)) { @@ -541,6 +583,23 @@ private void validateRelationAnnotation(RelationField relationField, Entity owne refs.addAll(referenceFields); } + private static boolean validateTypeLengthParam(RelationField relationField, Entity ownerEntity, + Entity referredEntity, SimpleTypeField ownerField, + List referredIdFields, int finalI, + String annotation) { + String ownerLength = readStringValueFromAnnotation(new PluginUtils.AnnotationUtilRecord( + ownerField.getAnnotations(), annotation, ANNOTATION_LENGTH_FIELD)); + String referredLength = readStringValueFromAnnotation(new PluginUtils.AnnotationUtilRecord( + referredIdFields.get(finalI).getAnnotations(), annotation, ANNOTATION_LENGTH_FIELD)); + if (!ownerLength.equals(referredLength)) { + ownerEntity.reportDiagnostic(PERSIST_SQL_424.getCode(), + MessageFormat.format(PERSIST_SQL_424.getMessage(), referredEntity.getEntityName()), + PERSIST_SQL_424.getSeverity(), relationField.getLocation()); + return true; + } + return false; + } + private boolean isPersistModelDefinitionDocument(SyntaxNodeAnalysisContext ctx) { try { if (ctx.currentPackage().project().kind().equals(ProjectKind.SINGLE_FILE_PROJECT)) { diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/persist/sql/compiler/pluginutils/PluginUtils.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/persist/sql/compiler/pluginutils/PluginUtils.java index 34ef503..1523152 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/persist/sql/compiler/pluginutils/PluginUtils.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/persist/sql/compiler/pluginutils/PluginUtils.java @@ -40,6 +40,14 @@ public final class PluginUtils { private PluginUtils() { } + public record AnnotationUtilRecord(List annotationNodes, String annotation, String field) { + public AnnotationUtilRecord (List annotationNodes, String annotation, String field) { + this.annotationNodes = Collections.unmodifiableList(annotationNodes); + this.annotation = annotation; + this.field = field; + } + } + public static boolean hasCompilationErrors(SyntaxNodeAnalysisContext context) { for (Diagnostic diagnostic : context.compilation().diagnosticResult().diagnostics()) { if (diagnostic.diagnosticInfo().severity() == DiagnosticSeverity.ERROR) { @@ -53,18 +61,16 @@ public static String stripEscapeCharacter(String name) { return name.startsWith("'") ? name.substring(1) : name; } - public static String readStringValueFromAnnotation - (List annotationNodes, String annotation, - String field) { - for (AnnotationNode annotationNode : annotationNodes) { + public static String readStringValueFromAnnotation(AnnotationUtilRecord annotationMethodRecord) { + for (AnnotationNode annotationNode : annotationMethodRecord.annotationNodes) { String annotationName = annotationNode.annotReference().toSourceCode().trim(); - if (annotationName.equals(annotation)) { + if (annotationName.equals(annotationMethodRecord.annotation)) { Optional annotationFieldNode = annotationNode.annotValue(); if (annotationFieldNode.isPresent()) { for (MappingFieldNode mappingFieldNode : annotationFieldNode.get().fields()) { SpecificFieldNode specificFieldNode = (SpecificFieldNode) mappingFieldNode; String fieldName = specificFieldNode.fieldName().toSourceCode().trim(); - if (!fieldName.equals(field)) { + if (!fieldName.equals(annotationMethodRecord.field)) { return ""; } Optional valueExpr = specificFieldNode.valueExpr(); @@ -88,17 +94,16 @@ public static String stripEscapeCharacter(String name) { return false; } - public static boolean isAnnotationFieldStringType - (List annotationNodes, String annotation, String field) { - for (AnnotationNode annotationNode : annotationNodes) { + public static boolean isAnnotationFieldStringType(AnnotationUtilRecord annotationMethodRecord) { + for (AnnotationNode annotationNode : annotationMethodRecord.annotationNodes) { String annotationName = annotationNode.annotReference().toSourceCode().trim(); - if (annotationName.equals(annotation)) { + if (annotationName.equals(annotationMethodRecord.annotation)) { Optional annotationFieldNode = annotationNode.annotValue(); if (annotationFieldNode.isPresent()) { for (MappingFieldNode mappingFieldNode : annotationFieldNode.get().fields()) { SpecificFieldNode specificFieldNode = (SpecificFieldNode) mappingFieldNode; String fieldName = specificFieldNode.fieldName().toSourceCode().trim(); - if (!fieldName.equals(field)) { + if (!fieldName.equals(annotationMethodRecord.field)) { return false; } Optional valueExpr = specificFieldNode.valueExpr(); @@ -113,17 +118,16 @@ public static String stripEscapeCharacter(String name) { return false; } - public static boolean isAnnotationFieldArrayType - (List annotationNodes, String annotation, String field) { - for (AnnotationNode annotationNode : annotationNodes) { + public static boolean isAnnotationFieldArrayType(AnnotationUtilRecord annotationMethodRecord) { + for (AnnotationNode annotationNode : annotationMethodRecord.annotationNodes) { String annotationName = annotationNode.annotReference().toSourceCode().trim(); - if (annotationName.equals(annotation)) { + if (annotationName.equals(annotationMethodRecord.annotation)) { Optional annotationFieldNode = annotationNode.annotValue(); if (annotationFieldNode.isPresent()) { for (MappingFieldNode mappingFieldNode : annotationFieldNode.get().fields()) { SpecificFieldNode specificFieldNode = (SpecificFieldNode) mappingFieldNode; String fieldName = specificFieldNode.fieldName().toSourceCode().trim(); - if (!fieldName.equals(field)) { + if (!fieldName.equals(annotationMethodRecord.field)) { return false; } Optional valueExpr = specificFieldNode.valueExpr(); @@ -138,17 +142,16 @@ public static String stripEscapeCharacter(String name) { return false; } - public static List readStringArrayValueFromAnnotation(List annotationNodes, - String annotation, String field) { - for (AnnotationNode annotationNode : annotationNodes) { + public static List readStringArrayValueFromAnnotation(AnnotationUtilRecord annotationUtilRecord) { + for (AnnotationNode annotationNode : annotationUtilRecord.annotationNodes) { String annotationName = annotationNode.annotReference().toSourceCode().trim(); - if (annotationName.equals(annotation)) { + if (annotationName.equals(annotationUtilRecord.annotation)) { Optional annotationFieldNode = annotationNode.annotValue(); if (annotationFieldNode.isPresent()) { for (MappingFieldNode mappingFieldNode : annotationFieldNode.get().fields()) { SpecificFieldNode specificFieldNode = (SpecificFieldNode) mappingFieldNode; String fieldName = specificFieldNode.fieldName().toSourceCode().trim(); - if (!fieldName.equals(field)) { + if (!fieldName.equals(annotationUtilRecord.field)) { return Collections.emptyList(); } Optional valueExpr = specificFieldNode.valueExpr(); From f8a6aacdf34c48197cfa0303f6c4001d45c33a08 Mon Sep 17 00:00:00 2001 From: Haritha Hasathcharu Date: Tue, 19 Mar 2024 16:03:25 +0530 Subject: [PATCH 2/3] Improve support for Name and Generated annotations --- ballerina/metadata_types.bal | 6 +- ballerina/sql_client.bal | 8 +- ballerina/tests/hospital_persist_types.bal | 143 ++++++++ ballerina/tests/init-tests.bal | 37 ++ .../tests/mssql_hospital_persist_client.bal | 254 +++++++++++++ ballerina/tests/mssql_hospital_tests.bal | 340 +++++++++++++++++ .../tests/mysql_hospital_persist_client.bal | 254 +++++++++++++ ballerina/tests/mysql_hospital_tests.bal | 343 ++++++++++++++++++ .../postgres_hospital_persist_client.bal | 254 +++++++++++++ ballerina/tests/postgresql_hospital_tests.bal | 340 +++++++++++++++++ ballerina/tests/resources/mysql/init.sql | 31 ++ ballerina/tests/resources/postgresql/init.sql | 31 ++ .../sql/compiler/CompilerPluginTest.java | 8 +- .../modelvalidator/persist/relation9.bal | 21 ++ .../PersistSqlModelDefinitionValidator.java | 8 +- 15 files changed, 2068 insertions(+), 10 deletions(-) create mode 100644 ballerina/tests/hospital_persist_types.bal create mode 100644 ballerina/tests/mssql_hospital_persist_client.bal create mode 100644 ballerina/tests/mssql_hospital_tests.bal create mode 100644 ballerina/tests/mysql_hospital_persist_client.bal create mode 100644 ballerina/tests/mysql_hospital_tests.bal create mode 100644 ballerina/tests/postgres_hospital_persist_client.bal create mode 100644 ballerina/tests/postgresql_hospital_tests.bal diff --git a/ballerina/metadata_types.bal b/ballerina/metadata_types.bal index dd2a34a..f664ab3 100644 --- a/ballerina/metadata_types.bal +++ b/ballerina/metadata_types.bal @@ -39,8 +39,10 @@ public type EntityFieldMetadata record {| # Represents the metadata associated with a simple field in the entity record. # # + columnName - The name of the SQL table column to which the field is mapped +# + dbGenerated - Whether the field is generated by the database public type SimpleFieldMetadata record {| string columnName; + boolean? dbGenerated = false; |}; # Represents the metadata associated with a field of an entity. @@ -52,11 +54,13 @@ public type FieldMetadata SimpleFieldMetadata|EntityFieldMetadata; # Only used by the generated persist clients and `persist:SQLClient`. # # + entityName - The name of the entity represented in the relation -# + refField - The name of the referenced column in the SQL table +# + refField - The name of the field in the entity that is being referenced +# + refColumn - The name of the column in the SQL table that is being referenced. If this is not provided, the `refField` is used as the `refColumn` public type RelationMetadata record {| string entityName; string refField; + string? refColumn = (); |}; # Represents the metadata associated with performing an SQL `JOIN` operation. diff --git a/ballerina/sql_client.bal b/ballerina/sql_client.bal index fadeae8..8e47181 100644 --- a/ballerina/sql_client.bal +++ b/ballerina/sql_client.bal @@ -294,7 +294,8 @@ public isolated client class SQLClient { columnNames.push(self.escape(self.entityName) + "." + self.escape(fieldMetadata.columnName) + " AS " + self.escape(key)); } else { // column is in another entity's table - columnNames.push(self.escape(fieldName) + "." + self.escape(fieldMetadata.relation.refField) + " AS " + self.escape(fieldName + "." + fieldMetadata.relation.refField)); + columnNames.push(self.escape(fieldName) + "." + self.escape(fieldMetadata.relation.refColumn ?: fieldMetadata.relation.refField) + " AS " + self.escape(fieldName + "." + fieldMetadata.relation.refField)); + } } @@ -313,7 +314,7 @@ public isolated client class SQLClient { continue; } - string columnName = fieldMetadata.relation.refField; + string columnName = fieldMetadata.relation.refColumn ?: fieldMetadata.relation.refField; columnNames.push(self.escape(columnName)); } return arrayToParameterizedQuery(columnNames); @@ -478,7 +479,8 @@ public isolated client class SQLClient { private isolated function getInsertableFields() returns string[] { return from string key in self.fieldMetadata.keys() - where self.fieldMetadata.get(key) is SimpleFieldMetadata + let FieldMetadata metadataField = self.fieldMetadata.get(key) + where metadataField is SimpleFieldMetadata && metadataField.dbGenerated != true select key; } diff --git a/ballerina/tests/hospital_persist_types.bal b/ballerina/tests/hospital_persist_types.bal new file mode 100644 index 0000000..00d81eb --- /dev/null +++ b/ballerina/tests/hospital_persist_types.bal @@ -0,0 +1,143 @@ +// 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/time; + +public enum AppointmentStatus { + SCHEDULED = "SCHEDULED", + STARTED = "STARTED", + ENDED = "ENDED" +} + +public enum PatientGender { + MALE = "MALE", + FEMALE = "FEMALE" +} + +public type Appointment record {| + readonly int id; + string reason; + time:Civil appointmentTime; + AppointmentStatus status; + int patientId; + int doctorId; +|}; + +public type AppointmentOptionalized record {| + int id?; + string reason?; + time:Civil appointmentTime?; + AppointmentStatus status?; + int patientId?; + int doctorId?; +|}; + +public type AppointmentWithRelations record {| + *AppointmentOptionalized; + PatientOptionalized patient?; + DoctorOptionalized doctor?; +|}; + +public type AppointmentTargetType typedesc; + +public type AppointmentInsert Appointment; + +public type AppointmentUpdate record {| + string reason?; + time:Civil appointmentTime?; + AppointmentStatus status?; + int patientId?; + int doctorId?; +|}; + +public type Patient record {| + readonly int id; + string name; + int age; + string address; + string phoneNumber; + PatientGender gender; + +|}; + +public type PatientOptionalized record {| + int id?; + string name?; + int age?; + string address?; + string phoneNumber?; + PatientGender gender?; +|}; + +public type PatientWithRelations record {| + *PatientOptionalized; + AppointmentOptionalized[] appointments?; +|}; + +public type PatientTargetType typedesc; + +public type PatientInsert record {| + string name; + int age; + string address; + string phoneNumber; + PatientGender gender; +|}; + +public type PatientUpdate record {| + string name?; + int age?; + string address?; + string phoneNumber?; + PatientGender gender?; +|}; + +public type Doctor record {| + readonly int id; + string name; + string specialty; + string phoneNumber; + decimal? salary; + +|}; + +public type DoctorOptionalized record {| + int id?; + string name?; + string specialty?; + string phoneNumber?; + decimal? salary?; +|}; + +public type DoctorWithRelations record {| + *DoctorOptionalized; + AppointmentOptionalized[] appointments?; +|}; + +public type DoctorTargetType typedesc; + +public type DoctorInsert Doctor; + +public type DoctorUpdate record {| + string name?; + string specialty?; + string phoneNumber?; + decimal? salary?; +|}; + diff --git a/ballerina/tests/init-tests.bal b/ballerina/tests/init-tests.bal index edd18a3..5489326 100644 --- a/ballerina/tests/init-tests.bal +++ b/ballerina/tests/init-tests.bal @@ -78,6 +78,9 @@ function initMySqlTests() returns error? { _ = check mysqlDbClient->execute(`TRUNCATE IntIdRecord`); _ = check mysqlDbClient->execute(`TRUNCATE AllTypesIdRecord`); _ = check mysqlDbClient->execute(`TRUNCATE CompositeAssociationRecord`); + _ = check mysqlDbClient->execute(`TRUNCATE Doctor`); + _ = check mysqlDbClient->execute(`TRUNCATE appointment`); + _ = check mysqlDbClient->execute(`TRUNCATE patients`); _ = check mysqlDbClient->execute(`SET FOREIGN_KEY_CHECKS = 1`); check mysqlDbClient.close(); } @@ -232,6 +235,40 @@ function initMsSqlTests() returns error? { PRIMARY KEY(id) ); `); + _ = check mssqlDbClient->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 mssqlDbClient->execute(` + CREATE TABLE [patients] ( + [IDP] INT IDENTITY(1,1), + [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 mssqlDbClient->execute(` + CREATE TABLE [appointment] ( + [id] INT NOT NULL, + [reason] VARCHAR(191) NOT NULL, + [appointmentTime] DATETIME2 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]) + ); + `); } function initPostgreSqlTests() returns error? { diff --git a/ballerina/tests/mssql_hospital_persist_client.bal b/ballerina/tests/mssql_hospital_persist_client.bal new file mode 100644 index 0000000..3477429 --- /dev/null +++ b/ballerina/tests/mssql_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/mssql; +import ballerinax/mssql.driver as _; + +const APPOINTMENT = "appointments"; +const PATIENT = "patients"; +const DOCTOR = "doctors"; + +public isolated client class MsSqlHospitalClient { + *persist:AbstractPersistClient; + + private final mssql: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? { + 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(dbClient.message()); + } + self.dbClient = dbClient; + self.persistClients = { + [APPOINTMENT]: check new (dbClient, self.metadata.get(APPOINTMENT), MSSQL_SPECIFICS), + [PATIENT]: check new (dbClient, self.metadata.get(PATIENT), MSSQL_SPECIFICS), + [DOCTOR]: check new (dbClient, self.metadata.get(DOCTOR), MSSQL_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.MSSQLProcessor", + 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.MSSQLProcessor", + 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.MSSQLProcessor", + 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.MSSQLProcessor", + 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.MSSQLProcessor", + 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.MSSQLProcessor", + 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.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/mssql_hospital_tests.bal b/ballerina/tests/mssql_hospital_tests.bal new file mode 100644 index 0000000..a00208f --- /dev/null +++ b/ballerina/tests/mssql_hospital_tests.bal @@ -0,0 +1,340 @@ +// 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/persist; +import ballerina/test; + + +@test:Config{} +function testCreatePatientMsSql() returns error? { + MsSqlHospitalClient mssqlDbHospital = check new(); + PatientInsert patient = { + name: "John Doe", + age: 30, + phoneNumber: "0771690000", + gender: "MALE", + address: "123, Main Street, Colombo 05" + }; + _ = check mssqlDbHospital->/patients.post([patient]); +} + +@test:Config{} +function testCreateDoctorMsSql() returns error? { + MsSqlHospitalClient mssqlDbHospital = check new(); + DoctorInsert doctor = { + id: 1, + name: "Doctor Mouse", + specialty: "Physician", + phoneNumber: "077100100", + salary: 20000 + }; + _ = check mssqlDbHospital->/doctors.post([doctor]); +} + +@test:Config{ + dependsOn: [testCreateDoctorMsSql] +} +function testCreateDoctorAlreadyExistsMsSql() returns error? { + MsSqlHospitalClient mssqlDbHospital = check new(); + DoctorInsert doctor = { + id: 1, + name: "Doctor Mouse", + specialty: "Physician", + phoneNumber: "077100100", + salary: 20000.00 + }; + int[]|persist:Error res = mssqlDbHospital->/doctors.post([doctor]); + if !(res is persist:AlreadyExistsError) { + test:assertFail("Doctor should not be created"); + } +} + +@test:Config{ + dependsOn: [testCreatePatientMsSql, testCreateDoctorMsSql] +} +function testCreateAppointmentMsSql() returns error? { + MsSqlHospitalClient mssqlDbHospital = 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" + }; + _ = check mssqlDbHospital->/appointments.post([appointment]); +} + +@test:Config{ + dependsOn: [testCreatePatientMsSql, testCreateDoctorMsSql, testCreateAppointmentMsSql] +} +function testCreateAppointmentAlreadyExistsMsSql() returns error? { + MsSqlHospitalClient mssqlDbHospital = 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 = mssqlDbHospital->/appointments.post([appointment]); + if !(res is persist:AlreadyExistsError) { + test:assertFail("Appointment should not be created"); + } +} + +@test:Config{ + dependsOn: [testCreateDoctorMsSql] +} +function testGetDoctorsMsSql() returns error? { + MsSqlHospitalClient mssqlDbHospital = check new(); + stream doctors = mssqlDbHospital->/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{ + dependsOn: [testCreatePatientMsSql] +} +function testGetPatientByIdMsSql() returns error? { + MsSqlHospitalClient mssqlDbHospital = check new(); + Patient|persist:Error patient = mssqlDbHospital->/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{} +function testGetPatientNotFoundMsSql() returns error? { + MsSqlHospitalClient mssqlDbHospital = check new(); + Patient|persist:Error patient = mssqlDbHospital->/patients/[10].get(); + if !(patient is persist:NotFoundError) { + test:assertFail("Patient should be not found"); + } +} + +@test:Config{ + dependsOn: [testCreateAppointmentMsSql] +} +function testGetAppointmentByDoctorMsSql() returns error? { + MsSqlHospitalClient mssqlDbHospital = check new(); + stream appointments = mssqlDbHospital->/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 = mssqlDbHospital->/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{ + dependsOn: [testCreateAppointmentMsSql] +} +function testGetAppointmentByPatientMsSql() returns error? { + MsSqlHospitalClient mssqlDbHospital = check new(); + stream appointments = mssqlDbHospital->/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 = mssqlDbHospital->/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{ + dependsOn: [testCreateAppointmentMsSql, testGetAppointmentByDoctorMsSql, testGetAppointmentByPatientMsSql] +} +function testPatchAppointmentMsSql() returns error? { + MsSqlHospitalClient mssqlDbHospital = check new(); + Appointment|persist:Error result = mssqlDbHospital->/appointments/[1].put({status: "STARTED"}); + if result is persist:Error { + test:assertFail("Appointment should be updated"); + } + stream appointments = mssqlDbHospital->/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 = mssqlDbHospital->/appointments/[0].put({status: "STARTED"}); + if !(result2 is persist:NotFoundError) { + test:assertFail("Appointment should not be found"); + } +} + +@test:Config{ + dependsOn: [testCreateAppointmentMsSql, testGetAppointmentByDoctorMsSql, testGetAppointmentByPatientMsSql, testPatchAppointmentMsSql] +} +function testDeleteAppointmentByPatientIdMsSql() returns error? { + MsSqlHospitalClient mssqlDbHospital = check new(); + stream appointments = mssqlDbHospital->/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 = mssqlDbHospital->/appointments/[appointment.id].delete(); + if result2 is persist:Error { + test:assertFail("Appointment should be deleted"); + } + } + stream appointments2 = mssqlDbHospital->/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{ + dependsOn: [testGetPatientByIdMsSql, testDeleteAppointmentByPatientIdMsSql] +} +function testDeletePatientMsSql() returns error? { + MsSqlHospitalClient mssqlDbHospital = check new(); + Patient|persist:Error result = mssqlDbHospital->/patients/[1].delete(); + if result is persist:Error { + test:assertFail("Patient should be deleted"); + } +} + +@test:Config{ + dependsOn: [testGetDoctorsMsSql, testDeleteAppointmentByPatientIdMsSql] +} +function testDeleteDoctorMsSql() returns error? { + MsSqlHospitalClient mssqlDbHospital = check new(); + Doctor|persist:Error result = mssqlDbHospital->/doctors/[1].delete(); + if result is persist:Error { + test:assertFail("Patient should be deleted"); + } +} diff --git a/ballerina/tests/mysql_hospital_persist_client.bal b/ballerina/tests/mysql_hospital_persist_client.bal new file mode 100644 index 0000000..9e0f80c --- /dev/null +++ b/ballerina/tests/mysql_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/mysql; +import ballerinax/mysql.driver as _; + +const APPOINTMENT = "appointments"; +const PATIENT = "patients"; +const DOCTOR = "doctors"; + +public isolated client class MySqlHospitalClient { + *persist:AbstractPersistClient; + + private final mysql: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? { + mysql:Client|error dbClient = new (host = "localhost", user = "root", password = "Test123#", database = "test", port = 3305); + if dbClient is error { + return error(dbClient.message()); + } + self.dbClient = dbClient; + self.persistClients = { + [APPOINTMENT]: check new (dbClient, self.metadata.get(APPOINTMENT), MYSQL_SPECIFICS), + [PATIENT]: check new (dbClient, self.metadata.get(PATIENT), MYSQL_SPECIFICS), + [DOCTOR]: check new (dbClient, self.metadata.get(DOCTOR), MYSQL_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.MySQLProcessor", + 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.MySQLProcessor", + 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.MySQLProcessor", + 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.MySQLProcessor", + 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.MySQLProcessor", + 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.MySQLProcessor", + 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.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_tests.bal b/ballerina/tests/mysql_hospital_tests.bal new file mode 100644 index 0000000..97f116f --- /dev/null +++ b/ballerina/tests/mysql_hospital_tests.bal @@ -0,0 +1,343 @@ +// 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/persist; +import ballerina/test; + + +@test:Config{} +function testCreatePatientMySql() returns error? { + MySqlHospitalClient mysqlDbHospital = check new(); + PatientInsert patient = { + name: "John Doe", + age: 30, + phoneNumber: "0771690000", + gender: "MALE", + address: "123, Main Street, Colombo 05" + }; + int[] unionResult = check mysqlDbHospital->/patients.post([patient]); + test:assertEquals(unionResult[0], 1, "Patient should be created"); +} + +@test:Config{} +function testCreateDoctorMySql() returns error? { + MySqlHospitalClient mysqlDbHospital = check new(); + DoctorInsert doctor = { + id: 1, + name: "Doctor Mouse", + specialty: "Physician", + phoneNumber: "077100100", + salary: 20000 + }; + int[] res = check mysqlDbHospital->/doctors.post([doctor]); + test:assertEquals(res[0], 1, "Doctor should be created"); +} + +@test:Config{ + dependsOn: [testCreateDoctorMySql] +} +function testCreateDoctorAlreadyExistsMySql() returns error? { + MySqlHospitalClient mysqlDbHospital = check new(); + DoctorInsert doctor = { + id: 1, + name: "Doctor Mouse", + specialty: "Physician", + phoneNumber: "077100100", + salary: 20000.00 + }; + int[]|persist:Error res = mysqlDbHospital->/doctors.post([doctor]); + if !(res is persist:AlreadyExistsError) { + test:assertFail("Doctor should not be created"); + } +} + +@test:Config{ + dependsOn: [testCreatePatientMySql, testCreateDoctorMySql] +} +function testCreateAppointmentMySql() returns error? { + MySqlHospitalClient mysqlDbHospital = 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 mysqlDbHospital->/appointments.post([appointment]); + test:assertEquals(res[0], 1, "Appointment should be created"); +} + +@test:Config{ + dependsOn: [testCreatePatientMySql, testCreateDoctorMySql, testCreateAppointmentMySql] +} +function testCreateAppointmentAlreadyExistsMySql() returns error? { + MySqlHospitalClient mysqlDbHospital = 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 = mysqlDbHospital->/appointments.post([appointment]); + if !(res is persist:AlreadyExistsError) { + test:assertFail("Appointment should not be created"); + } +} + +@test:Config{ + dependsOn: [testCreateDoctorMySql] +} +function testGetDoctorsMySql() returns error? { + MySqlHospitalClient mysqlDbHospital = check new(); + stream doctors = mysqlDbHospital->/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{ + dependsOn: [testCreatePatientMySql] +} +function testGetPatientByIdMySql() returns error? { + MySqlHospitalClient mysqlDbHospital = check new(); + Patient|persist:Error patient = mysqlDbHospital->/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{} +function testGetPatientNotFoundMySql() returns error? { + MySqlHospitalClient mysqlDbHospital = check new(); + Patient|persist:Error patient = mysqlDbHospital->/patients/[10].get(); + if !(patient is persist:NotFoundError) { + test:assertFail("Patient should be not found"); + } +} + +@test:Config{ + dependsOn: [testCreateAppointmentMySql] +} +function testGetAppointmentByDoctorMySql() returns error? { + MySqlHospitalClient mysqlDbHospital = check new(); + stream appointments = mysqlDbHospital->/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 = mysqlDbHospital->/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{ + dependsOn: [testCreateAppointmentMySql] +} +function testGetAppointmentByPatientMySql() returns error? { + MySqlHospitalClient mysqlDbHospital = check new(); + stream appointments = mysqlDbHospital->/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 = mysqlDbHospital->/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{ + dependsOn: [testCreateAppointmentMySql, testGetAppointmentByDoctorMySql, testGetAppointmentByPatientMySql] +} +function testPatchAppointmentMySql() returns error? { + MySqlHospitalClient mysqlDbHospital = check new(); + Appointment|persist:Error result = mysqlDbHospital->/appointments/[1].put({status: "STARTED"}); + if result is persist:Error { + test:assertFail("Appointment should be updated"); + } + stream appointments = mysqlDbHospital->/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 = mysqlDbHospital->/appointments/[0].put({status: "STARTED"}); + if !(result2 is persist:NotFoundError) { + test:assertFail("Appointment should not be found"); + } +} + +@test:Config{ + dependsOn: [testCreateAppointmentMySql, testGetAppointmentByDoctorMySql, testGetAppointmentByPatientMySql, testPatchAppointmentMySql] +} +function testDeleteAppointmentByPatientIdMySql() returns error? { + MySqlHospitalClient mysqlDbHospital = check new(); + stream appointments = mysqlDbHospital->/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 = mysqlDbHospital->/appointments/[appointment.id].delete(); + if result2 is persist:Error { + test:assertFail("Appointment should be deleted"); + } + } + stream appointments2 = mysqlDbHospital->/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{ + dependsOn: [testGetPatientByIdMySql, testDeleteAppointmentByPatientIdMySql] +} +function testDeletePatientMySql() returns error? { + MySqlHospitalClient mysqlDbHospital = check new(); + Patient|persist:Error result = mysqlDbHospital->/patients/[1].delete(); + if result is persist:Error { + test:assertFail("Patient should be deleted"); + } +} + +@test:Config{ + dependsOn: [testGetDoctorsMySql, testDeleteAppointmentByPatientIdMySql] +} +function testDeleteDoctorMySql() returns error? { + MySqlHospitalClient mysqlDbHospital = check new(); + Doctor|persist:Error result = mysqlDbHospital->/doctors/[1].delete(); + if result is persist:Error { + test:assertFail("Patient should be deleted"); + } +} diff --git a/ballerina/tests/postgres_hospital_persist_client.bal b/ballerina/tests/postgres_hospital_persist_client.bal new file mode 100644 index 0000000..612f6e1 --- /dev/null +++ b/ballerina/tests/postgres_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/postgresql; +import ballerinax/postgresql.driver as _; + +const APPOINTMENT = "appointments"; +const PATIENT = "patients"; +const DOCTOR = "doctors"; + +public isolated client class PostgreSqlHospitalClient { + *persist:AbstractPersistClient; + + private final postgresql: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? { + 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 = { + [APPOINTMENT]: check new (dbClient, self.metadata.get(APPOINTMENT), POSTGRESQL_SPECIFICS), + [PATIENT]: check new (dbClient, self.metadata.get(PATIENT), POSTGRESQL_SPECIFICS), + [DOCTOR]: check new (dbClient, self.metadata.get(DOCTOR), POSTGRESQL_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.PostgreSQLProcessor", + 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.PostgreSQLProcessor", + 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.PostgreSQLProcessor", + 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.PostgreSQLProcessor", + 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.PostgreSQLProcessor", + 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.PostgreSQLProcessor", + 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.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/postgresql_hospital_tests.bal b/ballerina/tests/postgresql_hospital_tests.bal new file mode 100644 index 0000000..a7cc584 --- /dev/null +++ b/ballerina/tests/postgresql_hospital_tests.bal @@ -0,0 +1,340 @@ +// 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/persist; +import ballerina/test; + + +@test:Config{} +function testCreatePatientPostgreSql() returns error? { + PostgreSqlHospitalClient postgreSqlDbHospital = check new(); + PatientInsert patient = { + name: "John Doe", + age: 30, + phoneNumber: "0771690000", + gender: "MALE", + address: "123, Main Street, Colombo 05" + }; + _ = check postgreSqlDbHospital->/patients.post([patient]); +} + +@test:Config{} +function testCreateDoctorPostgreSql() returns error? { + PostgreSqlHospitalClient postgresSqlDbHospital = check new(); + DoctorInsert doctor = { + id: 1, + name: "Doctor Mouse", + specialty: "Physician", + phoneNumber: "077100100", + salary: 20000 + }; + _ = check postgresSqlDbHospital->/doctors.post([doctor]); +} + +@test:Config{ + dependsOn: [testCreateDoctorPostgreSql] +} +function testCreateDoctorAlreadyExistsPostgreSql() returns error? { + PostgreSqlHospitalClient postgresSqlDbHospital = check new(); + DoctorInsert doctor = { + id: 1, + name: "Doctor Mouse", + specialty: "Physician", + phoneNumber: "077100100", + salary: 20000.00 + }; + int[]|persist:Error res = postgresSqlDbHospital->/doctors.post([doctor]); + if !(res is persist:AlreadyExistsError) { + test:assertFail("Doctor should not be created"); + } +} + +@test:Config{ + dependsOn: [testCreatePatientPostgreSql, testCreateDoctorPostgreSql] +} +function testCreateAppointmentPostgreSql() returns error? { + PostgreSqlHospitalClient postgresSqlDbHospital = 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" + }; + _ = check postgresSqlDbHospital->/appointments.post([appointment]); +} + +@test:Config{ + dependsOn: [testCreatePatientPostgreSql, testCreateDoctorPostgreSql, testCreateAppointmentPostgreSql] +} +function testCreateAppointmentAlreadyExistsPostgreSql() returns error? { + PostgreSqlHospitalClient postgresSqlDbHospital = 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 = postgresSqlDbHospital->/appointments.post([appointment]); + if !(res is persist:AlreadyExistsError) { + test:assertFail("Appointment should not be created"); + } +} + +@test:Config{ + dependsOn: [testCreateDoctorPostgreSql] +} +function testGetDoctorsPostgreSql() returns error? { + PostgreSqlHospitalClient postgresSqlDbHospital = check new(); + stream doctors = postgresSqlDbHospital->/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{ + dependsOn: [testCreatePatientPostgreSql] +} +function testGetPatientByIdPostgreSql() returns error? { + PostgreSqlHospitalClient postgresSqlDbHospital = check new(); + Patient|persist:Error patient = postgresSqlDbHospital->/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{} +function testGetPatientNotFoundPostgreSql() returns error? { + PostgreSqlHospitalClient postgresSqlDbHospital = check new(); + Patient|persist:Error patient = postgresSqlDbHospital->/patients/[10].get(); + if !(patient is persist:NotFoundError) { + test:assertFail("Patient should be not found"); + } +} + +@test:Config{ + dependsOn: [testCreateAppointmentPostgreSql] +} +function testGetAppointmentByDoctorPostgreSql() returns error? { + PostgreSqlHospitalClient postgresSqlDbHospital = check new(); + stream appointments = postgresSqlDbHospital->/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 = postgresSqlDbHospital->/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{ + dependsOn: [testCreateAppointmentPostgreSql] +} +function testGetAppointmentByPatientPostgreSql() returns error? { + PostgreSqlHospitalClient postgresSqlDbHospital = check new(); + stream appointments = postgresSqlDbHospital->/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 = postgresSqlDbHospital->/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{ + dependsOn: [testCreateAppointmentPostgreSql, testGetAppointmentByDoctorPostgreSql, testGetAppointmentByPatientPostgreSql] +} +function testPatchAppointmentPostgreSql() returns error? { + PostgreSqlHospitalClient postgresSqlDbHospital = check new(); + Appointment|persist:Error result = postgresSqlDbHospital->/appointments/[1].put({status: "STARTED"}); + if result is persist:Error { + test:assertFail("Appointment should be updated"); + } + stream appointments = postgresSqlDbHospital->/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 = postgresSqlDbHospital->/appointments/[0].put({status: "STARTED"}); + if !(result2 is persist:NotFoundError) { + test:assertFail("Appointment should not be found"); + } +} + +@test:Config{ + dependsOn: [testCreateAppointmentPostgreSql, testGetAppointmentByDoctorPostgreSql, testGetAppointmentByPatientPostgreSql, testPatchAppointmentPostgreSql] +} +function testDeleteAppointmentByPatientIdPostgreSql() returns error? { + PostgreSqlHospitalClient postgresSqlDbHospital = check new(); + stream appointments = postgresSqlDbHospital->/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 = postgresSqlDbHospital->/appointments/[appointment.id].delete(); + if result2 is persist:Error { + test:assertFail("Appointment should be deleted"); + } + } + stream appointments2 = postgresSqlDbHospital->/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{ + dependsOn: [testGetPatientByIdPostgreSql, testDeleteAppointmentByPatientIdPostgreSql] +} +function testDeletePatientPostgreSql() returns error? { + PostgreSqlHospitalClient postgresSqlDbHospital = check new(); + Patient|persist:Error result = postgresSqlDbHospital->/patients/[1].delete(); + if result is persist:Error { + test:assertFail("Patient should be deleted"); + } +} + +@test:Config{ + dependsOn: [testGetDoctorsPostgreSql, testDeleteAppointmentByPatientIdPostgreSql] +} +function testDeleteDoctorPostgreSql() returns error? { + PostgreSqlHospitalClient postgresSqlDbHospital = check new(); + Doctor|persist:Error result = postgresSqlDbHospital->/doctors/[1].delete(); + if result is persist:Error { + test:assertFail("Patient should be deleted"); + } +} diff --git a/ballerina/tests/resources/mysql/init.sql b/ballerina/tests/resources/mysql/init.sql index 605ea4a..da9af0d 100644 --- a/ballerina/tests/resources/mysql/init.sql +++ b/ballerina/tests/resources/mysql/init.sql @@ -133,3 +133,34 @@ CREATE TABLE test.CompositeAssociationRecord ( CONSTRAINT FK_COMPOSITEASSOCIATIONRECORD_ALLTYPESIDRECORD FOREIGN KEY(alltypesidrecordBooleanType, alltypesidrecordIntType, alltypesidrecordFloatType, alltypesidrecordDecimalType, alltypesidrecordStringType) REFERENCES AllTypesIdRecord(booleanType, intType, floatType, decimalType, stringType), PRIMARY KEY(id) ); + +CREATE TABLE test.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) +); + +CREATE TABLE test.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 ENUM('MALE', 'FEMALE') NOT NULL, + PRIMARY KEY(IDP) +); + +CREATE TABLE test.appointment ( + id INT NOT NULL, + reason VARCHAR(191) NOT NULL, + appointmentTime DATETIME NOT NULL, + status ENUM('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) +); diff --git a/ballerina/tests/resources/postgresql/init.sql b/ballerina/tests/resources/postgresql/init.sql index 5cbe203..17a7c34 100644 --- a/ballerina/tests/resources/postgresql/init.sql +++ b/ballerina/tests/resources/postgresql/init.sql @@ -120,3 +120,34 @@ CREATE TABLE "CompositeAssociationRecord" ( FOREIGN KEY ("alltypesidrecordBooleanType", "alltypesidrecordIntType", "alltypesidrecordFloatType", "alltypesidrecordDecimalType", "alltypesidrecordStringType") REFERENCES "AllTypesIdRecord"("booleanType", "intType", "floatType", "decimalType", "stringType"), PRIMARY KEY("id") ); + +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") +); + +CREATE TABLE "patients" ( + "IDP" SERIAL, + "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") +); + +CREATE TABLE "appointment" ( + "id" INT NOT NULL, + "reason" VARCHAR(191) NOT NULL, + "appointmentTime" TIMESTAMP 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") +); diff --git a/compiler-plugin-test/src/test/java/io/ballerina/stdlib/persist/sql/compiler/CompilerPluginTest.java b/compiler-plugin-test/src/test/java/io/ballerina/stdlib/persist/sql/compiler/CompilerPluginTest.java index ba062fd..49fb087 100644 --- a/compiler-plugin-test/src/test/java/io/ballerina/stdlib/persist/sql/compiler/CompilerPluginTest.java +++ b/compiler-plugin-test/src/test/java/io/ballerina/stdlib/persist/sql/compiler/CompilerPluginTest.java @@ -360,7 +360,7 @@ public void validateRelationAnnotations8() { @Test(enabled = true) public void validateRelationAnnotations9() { - List diagnostics = getErrorDiagnostics("modelvalidator", "relation9.bal", 5); + List diagnostics = getErrorDiagnostics("modelvalidator", "relation9.bal", 6); testDiagnostic( diagnostics, new String[]{ @@ -368,13 +368,16 @@ public void validateRelationAnnotations9() { PERSIST_SQL_424.getCode(), PERSIST_SQL_424.getCode(), PERSIST_SQL_424.getCode(), - PERSIST_SQL_424.getCode() + PERSIST_SQL_424.getCode(), + PERSIST_SQL_424.getCode(), }, new String[]{ "invalid use of the `Relation` annotation. mismatched key types for the related entity " + "'Person3'.", "invalid use of the `Relation` annotation. mismatched key types for the related entity " + "'Person2'.", + "invalid use of the `Relation` annotation. mismatched key types for the related entity " + + "'Person8'.", "invalid use of the `Relation` annotation. mismatched key types for the related entity " + "'Person7'.", "invalid use of the `Relation` annotation. mismatched key types for the related entity " + @@ -385,6 +388,7 @@ public void validateRelationAnnotations9() { new String[]{ "(77:4,78:18)", "(56:4,57:18)", + "(182:4,183:18)", "(161:4,162:18)", "(119:4,120:18)", "(98:4,99:18)" diff --git a/compiler-plugin-test/src/test/resources/modelvalidator/persist/relation9.bal b/compiler-plugin-test/src/test/resources/modelvalidator/persist/relation9.bal index 2e5a78c..7cd07af 100644 --- a/compiler-plugin-test/src/test/resources/modelvalidator/persist/relation9.bal +++ b/compiler-plugin-test/src/test/resources/modelvalidator/persist/relation9.bal @@ -162,3 +162,24 @@ public type Car7 record {| @sql:Relation {keys: ["ownerNic"]} Person7 owner; |}; + +public type Person8 record {| + @sql:Varchar {length: 10} + readonly string nic; + string name; + int age; + string city; + Car8? car; +|}; + +public type Car8 record {| + readonly string plateNo; + string make; + string model; + int year; + string color; + @sql:Varchar {length:5} + string ownerNic; + @sql:Relation {keys: ["ownerNic"]} + Person8 owner; +|}; diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/persist/sql/compiler/PersistSqlModelDefinitionValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/persist/sql/compiler/PersistSqlModelDefinitionValidator.java index a6ce4e0..05f35b1 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/persist/sql/compiler/PersistSqlModelDefinitionValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/persist/sql/compiler/PersistSqlModelDefinitionValidator.java @@ -550,12 +550,12 @@ private void validateRelationAnnotation(RelationField relationField, Entity owne return; } if (ownerCharPresent) { - if (validateTypeLengthParam(relationField, ownerEntity, referredEntity, ownerField, + if (!validateTypeLengthParam(relationField, ownerEntity, referredEntity, ownerField, referredIdFields, finalI, SQL_CHAR_MAPPING_ANNOTATION_NAME)) { return; } } else if (ownerVarcharPresent) { - if (validateTypeLengthParam(relationField, ownerEntity, referredEntity, ownerField, + if (!validateTypeLengthParam(relationField, ownerEntity, referredEntity, ownerField, referredIdFields, finalI, SQL_VARCHAR_MAPPING_ANNOTATION_NAME)) { return; } @@ -595,9 +595,9 @@ private static boolean validateTypeLengthParam(RelationField relationField, Enti ownerEntity.reportDiagnostic(PERSIST_SQL_424.getCode(), MessageFormat.format(PERSIST_SQL_424.getMessage(), referredEntity.getEntityName()), PERSIST_SQL_424.getSeverity(), relationField.getLocation()); - return true; + return false; } - return false; + return true; } private boolean isPersistModelDefinitionDocument(SyntaxNodeAnalysisContext ctx) { From 2baa56b6f762f2e0793d84dea4cdce9caf2db2a2 Mon Sep 17 00:00:00 2001 From: Haritha Hasathcharu Date: Wed, 20 Mar 2024 13:36:41 +0530 Subject: [PATCH 3/3] Make suggested changes --- ballerina/metadata_types.bal | 2 +- ballerina/sql_client.bal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ballerina/metadata_types.bal b/ballerina/metadata_types.bal index f664ab3..b7646fc 100644 --- a/ballerina/metadata_types.bal +++ b/ballerina/metadata_types.bal @@ -42,7 +42,7 @@ public type EntityFieldMetadata record {| # + dbGenerated - Whether the field is generated by the database public type SimpleFieldMetadata record {| string columnName; - boolean? dbGenerated = false; + boolean dbGenerated = false; |}; # Represents the metadata associated with a field of an entity. diff --git a/ballerina/sql_client.bal b/ballerina/sql_client.bal index 8e47181..8ab5940 100644 --- a/ballerina/sql_client.bal +++ b/ballerina/sql_client.bal @@ -480,7 +480,7 @@ public isolated client class SQLClient { private isolated function getInsertableFields() returns string[] { return from string key in self.fieldMetadata.keys() let FieldMetadata metadataField = self.fieldMetadata.get(key) - where metadataField is SimpleFieldMetadata && metadataField.dbGenerated != true + where metadataField is SimpleFieldMetadata && !metadataField.dbGenerated select key; }