This provider generates SQL syntax compatible with MySQL 8.0+. + * It is automatically activated when the "mysql" Spring profile is active.
+ * + * @author LCC Team + * @since 1.0 + */ +@Component +@Profile("mysql") +public class MySQLDialectProvider implements SqlDialectProvider { + + @Override + public String getDialectName() { + return "MySQL"; + } + + @Override + public String getDriverClassName() { + return "com.mysql.cj.jdbc.Driver"; + } + + // ========== Pagination ========== + + @Override + public String buildPaginationClause(int limit, int offset) { + return "LIMIT ? OFFSET ?"; + } + + @Override + public Object[] getPaginationParameters(int limit, int offset) { + return new Object[]{limit, offset}; + } + + // ========== Upsert Operations ========== + + @Override + public String buildUpsertStatement( + String tableName, + ListThis interface abstracts database-specific SQL patterns to enable multi-database support + * in the LCC application. Each dialect provider implements the SQL syntax specific to + * its target database while maintaining consistent semantics across all implementations.
+ * + * @author LCC Team + * @since 1.0 + */ +public interface SqlDialectProvider { + + // ========== Metadata ========== + + /** + * Returns the dialect name (e.g., "MySQL", "MSSQL"). + * + * @return the name of the database dialect + */ + String getDialectName(); + + /** + * Returns the JDBC driver class name for this dialect. + * + * @return the fully qualified JDBC driver class name + */ + String getDriverClassName(); + + // ========== Pagination ========== + + /** + * Generates the pagination clause for limiting and offsetting query results. + * + *Examples:
+ *Note: MSSQL requires an ORDER BY clause before OFFSET/FETCH.
+ * + * @param limit maximum number of rows to return + * @param offset number of rows to skip + * @return SQL clause for pagination (without parameter values) + */ + String buildPaginationClause(int limit, int offset); + + /** + * Returns parameter values in the correct order for the pagination clause. + * + *Parameter order varies by database:
+ *Database-specific implementations:
+ *Database-specific implementations:
+ *This is critical for {@code CalculationJobRepository} concurrent job processing.
+ * + *Database-specific implementations:
+ *Database-specific implementations:
+ *Database-specific implementations:
+ *Database-specific implementations:
+ *Database-specific implementations:
+ *Database-specific implementations:
+ *Database-specific implementations:
+ *Used in {@code NodeRepository} for finding nearby nodes based on geographic coordinates. + * Calculates the great-circle distance between two points on Earth's surface.
+ * + *Both MySQL and MSSQL support trigonometric functions (SIN, COS, ACOS, RADIANS), + * so the implementation is similar across databases.
+ * + * @param lat1 first latitude column or expression + * @param lng1 first longitude column or expression + * @param lat2 second latitude column or expression + * @param lng2 second longitude column or expression + * @return expression calculating distance in meters + */ + String buildHaversineDistance(String lat1, String lng1, String lat2, String lng2); + + // ========== String/Type Functions ========== + + /** + * Builds a string concatenation expression. + * + *Database-specific implementations:
+ *Database-specific implementations:
+ *Used for workarounds in queries that need to skip LIMIT but still use OFFSET.
+ *MSSQL example: {@code OUTPUT INSERTED.id}
+ * + * @param columns columns to return + * @return RETURNING clause + * @throws UnsupportedOperationException if dialect does not support RETURNING + */ + String buildReturningClause(String... columns); + + // ========== Schema/DDL ========== + + /** + * Returns the auto-increment column definition for schema creation. + * + *Database-specific implementations:
+ *Database-specific implementations:
+ *Note: For MSSQL, triggers must be created separately to handle ON UPDATE behavior.
+ * + * @return timestamp column definition + */ + String getTimestampDefinition(); + + // ========== Helper Enums ========== + + /** + * Time units for date arithmetic operations. + */ + enum DateUnit { + /** Year unit */ + YEAR, + /** Month unit */ + MONTH, + /** Day unit */ + DAY, + /** Hour unit */ + HOUR, + /** Minute unit */ + MINUTE, + /** Second unit */ + SECOND + } +} \ No newline at end of file diff --git a/src/main/resources/application-mssql.properties b/src/main/resources/application-mssql.properties new file mode 100644 index 0000000..2106316 --- /dev/null +++ b/src/main/resources/application-mssql.properties @@ -0,0 +1,50 @@ +# MSSQL Profile Configuration +# Activate with: -Dspring.profiles.active=mssql or SPRING_PROFILES_ACTIVE=mssql + +# Application Name +spring.application.name=lcc + +# Database Configuration - MSSQL +spring.datasource.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriver +spring.datasource.url=jdbc:sqlserver://${DB_HOST:localhost}:1433;databaseName=${DB_DATABASE:lcc};encrypt=true;trustServerCertificate=true +spring.datasource.username=${DB_USER:sa} +spring.datasource.password=${DB_PASSWORD} + +# File Upload Limits +spring.servlet.multipart.max-file-size=30MB +spring.servlet.multipart.max-request-size=50MB + +# Azure AD Configuration +spring.cloud.azure.active-directory.enabled=true +spring.cloud.azure.active-directory.authorization-clients.graph.scopes=openid,profile,email,https://graph.microsoft.com/User.Read + +# Management Endpoints +management.endpoints.web.exposure.include=health,info,metrics +management.endpoint.health.show-details=when-authorized + +# Flyway Migration - MSSQL +spring.flyway.enabled=true +spring.flyway.locations=classpath:db/migration/mssql +spring.flyway.baseline-on-migrate=true +spring.sql.init.mode=never + +# LCC Configuration +lcc.allowed_cors= +lcc.allowed_oauth_token_cors=* + +lcc.auth.identify.by=workday +lcc.auth.claim.workday=employeeid +lcc.auth.claim.email=preferred_username +lcc.auth.claim.firstname=given_name +lcc.auth.claim.lastname=family_name + +lcc.auth.claim.ignore.workday=false + +# Bulk Import +lcc.bulk.sheet_password=secretSheet?! + +# Calculation Job Processor Configuration +calculation.job.processor.enabled=true +calculation.job.processor.pool-size=1 +calculation.job.processor.delay=5000 +calculation.job.processor.thread-name-prefix=calc-job- \ No newline at end of file diff --git a/src/main/resources/application-mysql.properties b/src/main/resources/application-mysql.properties new file mode 100644 index 0000000..f33a554 --- /dev/null +++ b/src/main/resources/application-mysql.properties @@ -0,0 +1,50 @@ +# MySQL Profile Configuration +# Activate with: -Dspring.profiles.active=mysql or SPRING_PROFILES_ACTIVE=mysql + +# Application Name +spring.application.name=lcc + +# Database Configuration - MySQL +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.datasource.url=jdbc:mysql://${DB_HOST:localhost}:3306/${DB_DATABASE:lcc} +spring.datasource.username=${DB_USER:root} +spring.datasource.password=${DB_PASSWORD} + +# File Upload Limits +spring.servlet.multipart.max-file-size=30MB +spring.servlet.multipart.max-request-size=50MB + +# Azure AD Configuration +spring.cloud.azure.active-directory.enabled=true +spring.cloud.azure.active-directory.authorization-clients.graph.scopes=openid,profile,email,https://graph.microsoft.com/User.Read + +# Management Endpoints +management.endpoints.web.exposure.include=health,info,metrics +management.endpoint.health.show-details=when-authorized + +# Flyway Migration - MySQL +spring.flyway.enabled=true +spring.flyway.locations=classpath:db/migration/mysql +spring.flyway.baseline-on-migrate=true +spring.sql.init.mode=never + +# LCC Configuration +lcc.allowed_cors= +lcc.allowed_oauth_token_cors=* + +lcc.auth.identify.by=workday +lcc.auth.claim.workday=employeeid +lcc.auth.claim.email=preferred_username +lcc.auth.claim.firstname=given_name +lcc.auth.claim.lastname=family_name + +lcc.auth.claim.ignore.workday=false + +# Bulk Import +lcc.bulk.sheet_password=secretSheet?! + +# Calculation Job Processor Configuration +calculation.job.processor.enabled=true +calculation.job.processor.pool-size=1 +calculation.job.processor.delay=5000 +calculation.job.processor.thread-name-prefix=calc-job- \ No newline at end of file diff --git a/src/test/java/de/avatic/lcc/database/dialect/MySQLDialectProviderTest.java b/src/test/java/de/avatic/lcc/database/dialect/MySQLDialectProviderTest.java new file mode 100644 index 0000000..9830609 --- /dev/null +++ b/src/test/java/de/avatic/lcc/database/dialect/MySQLDialectProviderTest.java @@ -0,0 +1,279 @@ +package de.avatic.lcc.database.dialect; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link MySQLDialectProvider}. + */ +@DisplayName("MySQLDialectProvider Tests") +class MySQLDialectProviderTest { + + private MySQLDialectProvider provider; + + @BeforeEach + void setUp() { + provider = new MySQLDialectProvider(); + } + + @Nested + @DisplayName("Metadata Tests") + class MetadataTests { + + @Test + @DisplayName("Should return correct dialect name") + void shouldReturnCorrectDialectName() { + assertEquals("MySQL", provider.getDialectName()); + } + + @Test + @DisplayName("Should return correct driver class name") + void shouldReturnCorrectDriverClassName() { + assertEquals("com.mysql.cj.jdbc.Driver", provider.getDriverClassName()); + } + } + + @Nested + @DisplayName("Pagination Tests") + class PaginationTests { + + @Test + @DisplayName("Should build correct pagination clause") + void shouldBuildCorrectPaginationClause() { + String result = provider.buildPaginationClause(10, 20); + assertEquals("LIMIT ? OFFSET ?", result); + } + + @Test + @DisplayName("Should return pagination parameters in correct order") + void shouldReturnPaginationParametersInCorrectOrder() { + Object[] params = provider.getPaginationParameters(10, 20); + assertArrayEquals(new Object[]{10, 20}, params); + } + } + + @Nested + @DisplayName("Upsert Operation Tests") + class UpsertOperationTests { + + @Test + @DisplayName("Should build correct upsert statement") + void shouldBuildCorrectUpsertStatement() { + List