Step 1 - Foundation & Infrastruktur (SqlDialectProvider Interface, Maven Dependencies, Konfiguration)

This commit is contained in:
Jan 2026-01-25 18:30:51 +01:00
parent 417221eca8
commit 10a8cfa72b
7 changed files with 1048 additions and 0 deletions

98
CLAUDE.md Normal file
View file

@ -0,0 +1,98 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
LCC (Logistic Cost Calculator) is a Spring Boot 3.5.9 backend API for calculating complex logistics costs across supply chain networks. It handles materials, packaging, transportation rates, route planning, and multi-component cost calculations including customs duties, handling, inventory, and risk assessment.
## Build & Run Commands
```bash
# Build the project
mvn clean install
# Run the application
mvn spring-boot:run
# Run all tests
mvn test
# Run a specific test class
mvn test -Dtest=NodeControllerIntegrationTest
# Run a specific test method
mvn test -Dtest=NodeControllerIntegrationTest#shouldReturnListOfNodesWithDefaultPagination
# Skip tests during build
mvn clean install -DskipTests
# Generate JAXB classes from WSDL (EU taxation service)
mvn jaxb:generate
```
## Architecture
### Layered Architecture
```
Controllers → DTOs → Services → Transformers → Repositories → MySQL
```
### Package Structure (`de.avatic.lcc`)
- **controller/** - REST endpoints organized by domain (calculation, configuration, bulk, users, report)
- **service/access/** - Business logic for domain entities (PremisesService, MaterialService, NodeService, etc.)
- **service/calculation/** - Logistics cost calculation orchestration and step services
- **service/calculation/execution/steps/** - Individual calculation components (airfreight, handling, inventory, customs, etc.)
- **service/bulk/** - Excel-based bulk import/export operations
- **service/api/** - External API integrations (Azure Maps geocoding, EU taxation)
- **service/transformer/** - Entity-to-DTO mapping
- **repositories/** - JDBC-based data access (not JPA) with custom RowMappers
- **model/db/** - Database entity classes
- **dto/** - Data transfer objects for API contracts
### Key Design Decisions
- **JDBC over JPA**: Uses `JdbcTemplate` and `NamedParameterJdbcTemplate` for complex queries
- **Transformer layer**: Explicit DTO mapping keeps entities separate from API contracts
- **Calculation chain**: Cost calculations broken into fine-grained services in `execution/steps/`
### Core Calculation Flow
```
CalculationExecutionService.launchJobCalculation()
→ ContainerCalculationService (container type selection: FEU/TEU/HC/TRUCK)
→ RouteSectionCostCalculationService (per-section costs)
→ AirfreightCalculationService
→ HandlingCostCalculationService
→ InventoryCostCalculationService
→ CustomCostCalculationService (tariff/duties)
```
### Authorization Model
Role-based access control via `@PreAuthorize` annotations:
- SUPER, CALCULATION, MATERIAL, FREIGHT, PACKAGING, BASIC
## Testing
Integration tests use:
- `@SpringBootTest` + `@AutoConfigureMockMvc`
- `@Transactional` for test isolation
- `@Sql` annotations for test data setup/cleanup from `src/test/resources/master_data/`
- MySQL database (requires env.properties with DB_DATABASE, DB_USER, DB_PASSWORD)
## Database
- **MySQL** with Flyway migrations in `src/main/resources/db/migration/`
- Migration naming: `V{N}__{Description}.sql`
- Key tables: `premiss`, `premiss_sink`, `premiss_route`, `calculation_job`, `node`, `material`, `packaging`, `container_rate`, `country_matrix_rate`
## External Integrations
- **Azure AD**: OAuth2/OIDC authentication
- **Azure Maps**: Geocoding and route distance calculations (GeoApiService, DistanceApiService)
- **EU Taxation API**: TARIC nomenclature lookup for customs duties (EUTaxationApiService)
## Configuration
Key properties in `application.properties`:
- `lcc.auth.identify.by` - User identification method (workday)
- `calculation.job.processor.*` - Async calculation job settings
- Flyway enabled by default; migrations run on startup

30
pom.xml
View file

@ -90,6 +90,12 @@
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>12.6.1.jre11</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
@ -178,6 +184,10 @@
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-sqlserver</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
@ -195,6 +205,26 @@
<version>3.2.3</version>
</dependency>
<!-- TestContainers for multi-database integration testing -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mssqlserver</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>

View file

@ -0,0 +1,184 @@
package de.avatic.lcc.database.dialect;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* MySQL-specific implementation of {@link SqlDialectProvider}.
*
* <p>This provider generates SQL syntax compatible with MySQL 8.0+.
* It is automatically activated when the "mysql" Spring profile is active.</p>
*
* @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,
List<String> uniqueColumns,
List<String> insertColumns,
List<String> updateColumns
) {
// INSERT INTO table (col1, col2, ...) VALUES (?, ?, ...)
String insertPart = String.format(
"INSERT INTO %s (%s) VALUES (%s)",
tableName,
String.join(", ", insertColumns),
insertColumns.stream().map(c -> "?").collect(Collectors.joining(", "))
);
// ON DUPLICATE KEY UPDATE col1 = VALUES(col1), col2 = VALUES(col2), ...
String updatePart = updateColumns.stream()
.map(col -> col + " = VALUES(" + col + ")")
.collect(Collectors.joining(", "));
return insertPart + " ON DUPLICATE KEY UPDATE " + updatePart;
}
@Override
public String buildInsertIgnoreStatement(
String tableName,
List<String> columns,
List<String> uniqueColumns
) {
return String.format(
"INSERT IGNORE INTO %s (%s) VALUES (%s)",
tableName,
String.join(", ", columns),
columns.stream().map(c -> "?").collect(Collectors.joining(", "))
);
}
// ========== Locking Strategies ==========
@Override
public String buildSelectForUpdateSkipLocked(String selectStatement) {
return selectStatement + " FOR UPDATE SKIP LOCKED";
}
@Override
public String buildSelectForUpdate(String selectStatement) {
return selectStatement + " FOR UPDATE";
}
// ========== Date/Time Functions ==========
@Override
public String getCurrentTimestamp() {
return "NOW()";
}
@Override
public String buildDateSubtraction(String baseDate, String value, DateUnit unit) {
String base = baseDate != null ? baseDate : "NOW()";
return String.format("DATE_SUB(%s, INTERVAL %s %s)", base, value, unit.name());
}
@Override
public String buildDateAddition(String baseDate, String value, DateUnit unit) {
String base = baseDate != null ? baseDate : "NOW()";
return String.format("DATE_ADD(%s, INTERVAL %s %s)", base, value, unit.name());
}
@Override
public String extractDate(String columnOrExpression) {
return "DATE(" + columnOrExpression + ")";
}
// ========== Auto-increment Reset ==========
@Override
public String buildAutoIncrementReset(String tableName) {
return String.format("ALTER TABLE %s AUTO_INCREMENT = 1", tableName);
}
// ========== Geospatial Distance Calculation ==========
@Override
public String buildHaversineDistance(String lat1, String lng1, String lat2, String lng2) {
// Haversine formula: 6371000 meters (Earth radius) * acos(...)
// Formula: d = 2R * arcsin(sqrt(sin²((lat2-lat1)/2) + cos(lat1)*cos(lat2)*sin²((lon2-lon1)/2)))
// Simplified: R * acos(cos(lat1)*cos(lat2)*cos(lng2-lng1) + sin(lat1)*sin(lat2))
return String.format(
"6371000 * ACOS(COS(RADIANS(%s)) * COS(RADIANS(%s)) * " +
"COS(RADIANS(%s) - RADIANS(%s)) + SIN(RADIANS(%s)) * SIN(RADIANS(%s)))",
lat1, lat2, lng2, lng1, lat1, lat2
);
}
// ========== String/Type Functions ==========
@Override
public String buildConcat(String... expressions) {
return "CONCAT(" + String.join(", ", expressions) + ")";
}
@Override
public String castToString(String expression) {
return "CAST(" + expression + " AS CHAR)";
}
// ========== Bulk Operations ==========
@Override
public String getMaxLimitValue() {
// MySQL BIGINT UNSIGNED max value
return "18446744073709551615";
}
@Override
public boolean supportsReturningClause() {
return false;
}
@Override
public String buildReturningClause(String... columns) {
throw new UnsupportedOperationException(
"MySQL does not support RETURNING clause. Use LAST_INSERT_ID() or GeneratedKeyHolder instead."
);
}
// ========== Schema/DDL ==========
@Override
public String getAutoIncrementDefinition() {
return "INT NOT NULL AUTO_INCREMENT";
}
@Override
public String getTimestampDefinition() {
return "TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP";
}
}

View file

@ -0,0 +1,357 @@
package de.avatic.lcc.database.dialect;
import java.util.List;
/**
* Provides database-specific SQL syntax for different RDBMS implementations.
* Supports MySQL and MSSQL Server with identical semantic behavior.
*
* <p>This 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.</p>
*
* @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.
*
* <p>Examples:</p>
* <ul>
* <li>MySQL: {@code LIMIT ? OFFSET ?}</li>
* <li>MSSQL: {@code OFFSET ? ROWS FETCH NEXT ? ROWS ONLY}</li>
* </ul>
*
* <p><b>Note:</b> MSSQL requires an ORDER BY clause before OFFSET/FETCH.</p>
*
* @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.
*
* <p>Parameter order varies by database:</p>
* <ul>
* <li>MySQL: {@code [limit, offset]}</li>
* <li>MSSQL: {@code [offset, limit]}</li>
* </ul>
*
* @param limit maximum number of rows to return
* @param offset number of rows to skip
* @return array of parameters in database-specific order
*/
Object[] getPaginationParameters(int limit, int offset);
// ========== Upsert Operations ==========
/**
* Builds an UPSERT (INSERT or UPDATE) statement.
*
* <p>Database-specific implementations:</p>
* <ul>
* <li>MySQL: {@code INSERT ... ON DUPLICATE KEY UPDATE ...}</li>
* <li>MSSQL: {@code MERGE ... WHEN MATCHED THEN UPDATE WHEN NOT MATCHED THEN INSERT ...}</li>
* </ul>
*
* @param tableName target table name
* @param uniqueColumns columns that define uniqueness (for matching existing rows)
* @param insertColumns all columns to insert in a new row
* @param updateColumns columns to update if row exists
* @return complete UPSERT SQL statement with placeholders
*/
String buildUpsertStatement(
String tableName,
List<String> uniqueColumns,
List<String> insertColumns,
List<String> updateColumns
);
/**
* Builds an INSERT IGNORE statement that inserts only if the row does not exist.
*
* <p>Database-specific implementations:</p>
* <ul>
* <li>MySQL: {@code INSERT IGNORE INTO ...}</li>
* <li>MSSQL: {@code IF NOT EXISTS (...) INSERT INTO ...}</li>
* </ul>
*
* @param tableName target table name
* @param columns columns to insert
* @param uniqueColumns columns that define uniqueness (for existence check)
* @return INSERT IGNORE statement with placeholders
*/
String buildInsertIgnoreStatement(
String tableName,
List<String> columns,
List<String> uniqueColumns
);
// ========== Locking Strategies ==========
/**
* Builds SELECT FOR UPDATE with skip locked capability for pessimistic locking.
*
* <p>This is critical for {@code CalculationJobRepository} concurrent job processing.</p>
*
* <p>Database-specific implementations:</p>
* <ul>
* <li>MySQL: {@code SELECT ... FOR UPDATE SKIP LOCKED}</li>
* <li>MSSQL: {@code SELECT ... WITH (UPDLOCK, READPAST)}</li>
* </ul>
*
* @param selectStatement base SELECT statement (without locking clause)
* @return complete statement with pessimistic locking that skips locked rows
*/
String buildSelectForUpdateSkipLocked(String selectStatement);
/**
* Builds standard SELECT FOR UPDATE for pessimistic locking (waits for locks).
*
* <p>Database-specific implementations:</p>
* <ul>
* <li>MySQL: {@code SELECT ... FOR UPDATE}</li>
* <li>MSSQL: {@code SELECT ... WITH (UPDLOCK, ROWLOCK)}</li>
* </ul>
*
* @param selectStatement base SELECT statement (without locking clause)
* @return complete statement with pessimistic locking
*/
String buildSelectForUpdate(String selectStatement);
// ========== Date/Time Functions ==========
/**
* Returns the SQL function for getting the current timestamp.
*
* <p>Database-specific implementations:</p>
* <ul>
* <li>MySQL: {@code NOW()}</li>
* <li>MSSQL: {@code GETDATE()}</li>
* </ul>
*
* @return SQL function for current timestamp
*/
String getCurrentTimestamp();
/**
* Builds a date subtraction expression.
*
* <p>Database-specific implementations:</p>
* <ul>
* <li>MySQL: {@code DATE_SUB(NOW(), INTERVAL ? DAY)}</li>
* <li>MSSQL: {@code DATEADD(DAY, -?, GETDATE())}</li>
* </ul>
*
* @param baseDate base date expression (or null to use current timestamp)
* @param value placeholder for number of time units to subtract (e.g., "?")
* @param unit time unit (DAY, HOUR, MINUTE, etc.)
* @return date subtraction expression
*/
String buildDateSubtraction(String baseDate, String value, DateUnit unit);
/**
* Builds a date addition expression.
*
* <p>Database-specific implementations:</p>
* <ul>
* <li>MySQL: {@code DATE_ADD(NOW(), INTERVAL ? DAY)}</li>
* <li>MSSQL: {@code DATEADD(DAY, ?, GETDATE())}</li>
* </ul>
*
* @param baseDate base date expression (or null to use current timestamp)
* @param value placeholder for number of time units to add (e.g., "?")
* @param unit time unit (DAY, HOUR, MINUTE, etc.)
* @return date addition expression
*/
String buildDateAddition(String baseDate, String value, DateUnit unit);
/**
* Extracts the date part from a datetime expression (ignoring time component).
*
* <p>Database-specific implementations:</p>
* <ul>
* <li>MySQL: {@code DATE(column)}</li>
* <li>MSSQL: {@code CAST(column AS DATE)}</li>
* </ul>
*
* @param columnOrExpression column name or expression to extract date from
* @return expression that extracts date component
*/
String extractDate(String columnOrExpression);
// ========== Auto-increment Reset ==========
/**
* Resets the auto-increment counter for a table (primarily used in tests).
*
* <p>Database-specific implementations:</p>
* <ul>
* <li>MySQL: {@code ALTER TABLE table AUTO_INCREMENT = 1}</li>
* <li>MSSQL: {@code DBCC CHECKIDENT ('table', RESEED, 0)}</li>
* </ul>
*
* @param tableName table to reset auto-increment counter
* @return SQL statement to reset auto-increment
*/
String buildAutoIncrementReset(String tableName);
// ========== Geospatial Distance Calculation ==========
/**
* Builds a Haversine distance calculation expression.
*
* <p>Used in {@code NodeRepository} for finding nearby nodes based on geographic coordinates.
* Calculates the great-circle distance between two points on Earth's surface.</p>
*
* <p>Both MySQL and MSSQL support trigonometric functions (SIN, COS, ACOS, RADIANS),
* so the implementation is similar across databases.</p>
*
* @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.
*
* <p>Database-specific implementations:</p>
* <ul>
* <li>MySQL: {@code CONCAT(a, b, c)}</li>
* <li>MSSQL: {@code CONCAT(a, b, c)} (SQL Server 2012+) or {@code a + b + c}</li>
* </ul>
*
* @param expressions expressions to concatenate
* @return concatenation expression
*/
String buildConcat(String... expressions);
/**
* Converts an expression to string type.
*
* <p>Database-specific implementations:</p>
* <ul>
* <li>MySQL: {@code CAST(x AS CHAR)}</li>
* <li>MSSQL: {@code CAST(x AS VARCHAR(MAX))}</li>
* </ul>
*
* @param expression expression to convert to string
* @return cast-to-string expression
*/
String castToString(String expression);
// ========== Bulk Operations ==========
/**
* Returns the maximum safe value for LIMIT clause.
*
* <p>Used for workarounds in queries that need to skip LIMIT but still use OFFSET.</p>
* <ul>
* <li>MySQL: {@code 18446744073709551615} (BIGINT UNSIGNED max)</li>
* <li>MSSQL: {@code 2147483647} (INT max)</li>
* </ul>
*
* @return maximum limit value as string
*/
String getMaxLimitValue();
/**
* Checks if the dialect supports RETURNING clause for INSERT statements.
*
* <ul>
* <li>MySQL: {@code false} (use LAST_INSERT_ID())</li>
* <li>MSSQL: {@code true} (supports OUTPUT INSERTED.id)</li>
* </ul>
*
* @return true if RETURNING clause is supported
*/
boolean supportsReturningClause();
/**
* Builds a RETURNING clause for INSERT statement.
*
* <p>MSSQL example: {@code OUTPUT INSERTED.id}</p>
*
* @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.
*
* <p>Database-specific implementations:</p>
* <ul>
* <li>MySQL: {@code INT NOT NULL AUTO_INCREMENT}</li>
* <li>MSSQL: {@code INT NOT NULL IDENTITY(1,1)}</li>
* </ul>
*
* @return auto-increment column definition
*/
String getAutoIncrementDefinition();
/**
* Returns the timestamp column definition with automatic update capability.
*
* <p>Database-specific implementations:</p>
* <ul>
* <li>MySQL: {@code TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP}</li>
* <li>MSSQL: {@code DATETIME2 NOT NULL DEFAULT GETDATE()} (requires trigger for ON UPDATE)</li>
* </ul>
*
* <p><b>Note:</b> For MSSQL, triggers must be created separately to handle ON UPDATE behavior.</p>
*
* @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
}
}

View file

@ -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-

View file

@ -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-

View file

@ -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<String> uniqueCols = Arrays.asList("id", "user_id");
List<String> insertCols = Arrays.asList("id", "user_id", "name", "value");
List<String> updateCols = Arrays.asList("name", "value");
String result = provider.buildUpsertStatement("test_table", uniqueCols, insertCols, updateCols);
assertTrue(result.contains("INSERT INTO test_table"));
assertTrue(result.contains("(id, user_id, name, value)"));
assertTrue(result.contains("VALUES (?, ?, ?, ?)"));
assertTrue(result.contains("ON DUPLICATE KEY UPDATE"));
assertTrue(result.contains("name = VALUES(name)"));
assertTrue(result.contains("value = VALUES(value)"));
}
@Test
@DisplayName("Should build correct insert ignore statement")
void shouldBuildCorrectInsertIgnoreStatement() {
List<String> columns = Arrays.asList("user_id", "group_id");
List<String> uniqueCols = Arrays.asList("user_id", "group_id");
String result = provider.buildInsertIgnoreStatement("mapping_table", columns, uniqueCols);
assertEquals("INSERT IGNORE INTO mapping_table (user_id, group_id) VALUES (?, ?)", result);
}
}
@Nested
@DisplayName("Locking Strategy Tests")
class LockingStrategyTests {
@Test
@DisplayName("Should build SELECT FOR UPDATE SKIP LOCKED")
void shouldBuildSelectForUpdateSkipLocked() {
String baseQuery = "SELECT * FROM calculation_job WHERE state = 'CREATED'";
String result = provider.buildSelectForUpdateSkipLocked(baseQuery);
assertTrue(result.endsWith("FOR UPDATE SKIP LOCKED"));
assertTrue(result.startsWith("SELECT * FROM calculation_job"));
}
@Test
@DisplayName("Should build SELECT FOR UPDATE")
void shouldBuildSelectForUpdate() {
String baseQuery = "SELECT * FROM calculation_job WHERE id = ?";
String result = provider.buildSelectForUpdate(baseQuery);
assertTrue(result.endsWith("FOR UPDATE"));
assertFalse(result.contains("SKIP LOCKED"));
}
}
@Nested
@DisplayName("Date/Time Function Tests")
class DateTimeFunctionTests {
@Test
@DisplayName("Should return NOW() for current timestamp")
void shouldReturnNowForCurrentTimestamp() {
assertEquals("NOW()", provider.getCurrentTimestamp());
}
@Test
@DisplayName("Should build date subtraction with NOW()")
void shouldBuildDateSubtractionWithNow() {
String result = provider.buildDateSubtraction(null, "3", SqlDialectProvider.DateUnit.DAY);
assertEquals("DATE_SUB(NOW(), INTERVAL 3 DAY)", result);
}
@Test
@DisplayName("Should build date subtraction with custom base date")
void shouldBuildDateSubtractionWithCustomBaseDate() {
String result = provider.buildDateSubtraction("calculation_date", "60", SqlDialectProvider.DateUnit.MINUTE);
assertEquals("DATE_SUB(calculation_date, INTERVAL 60 MINUTE)", result);
}
@Test
@DisplayName("Should build date addition with NOW()")
void shouldBuildDateAdditionWithNow() {
String result = provider.buildDateAddition(null, "7", SqlDialectProvider.DateUnit.DAY);
assertEquals("DATE_ADD(NOW(), INTERVAL 7 DAY)", result);
}
@Test
@DisplayName("Should build date addition with custom base date")
void shouldBuildDateAdditionWithCustomBaseDate() {
String result = provider.buildDateAddition("start_date", "1", SqlDialectProvider.DateUnit.MONTH);
assertEquals("DATE_ADD(start_date, INTERVAL 1 MONTH)", result);
}
@Test
@DisplayName("Should extract date from column")
void shouldExtractDateFromColumn() {
String result = provider.extractDate("created_at");
assertEquals("DATE(created_at)", result);
}
@Test
@DisplayName("Should extract date from expression")
void shouldExtractDateFromExpression() {
String result = provider.extractDate("NOW()");
assertEquals("DATE(NOW())", result);
}
}
@Nested
@DisplayName("Auto-increment Reset Tests")
class AutoIncrementResetTests {
@Test
@DisplayName("Should build auto-increment reset statement")
void shouldBuildAutoIncrementResetStatement() {
String result = provider.buildAutoIncrementReset("test_table");
assertEquals("ALTER TABLE test_table AUTO_INCREMENT = 1", result);
}
}
@Nested
@DisplayName("Geospatial Distance Tests")
class GeospatialDistanceTests {
@Test
@DisplayName("Should build Haversine distance calculation")
void shouldBuildHaversineDistanceCalculation() {
String result = provider.buildHaversineDistance("50.1", "8.6", "node.geo_lat", "node.geo_lng");
assertTrue(result.contains("6371000"));
assertTrue(result.contains("ACOS"));
assertTrue(result.contains("COS"));
assertTrue(result.contains("SIN"));
assertTrue(result.contains("RADIANS"));
assertTrue(result.contains("50.1"));
assertTrue(result.contains("8.6"));
assertTrue(result.contains("node.geo_lat"));
assertTrue(result.contains("node.geo_lng"));
}
}
@Nested
@DisplayName("String/Type Function Tests")
class StringTypeFunctionTests {
@Test
@DisplayName("Should build CONCAT with multiple expressions")
void shouldBuildConcatWithMultipleExpressions() {
String result = provider.buildConcat("first_name", "' '", "last_name");
assertEquals("CONCAT(first_name, ' ', last_name)", result);
}
@Test
@DisplayName("Should build CONCAT with single expression")
void shouldBuildConcatWithSingleExpression() {
String result = provider.buildConcat("column_name");
assertEquals("CONCAT(column_name)", result);
}
@Test
@DisplayName("Should cast to string")
void shouldCastToString() {
String result = provider.castToString("user_id");
assertEquals("CAST(user_id AS CHAR)", result);
}
}
@Nested
@DisplayName("Bulk Operation Tests")
class BulkOperationTests {
@Test
@DisplayName("Should return MySQL BIGINT UNSIGNED max value")
void shouldReturnMySQLBigIntUnsignedMaxValue() {
assertEquals("18446744073709551615", provider.getMaxLimitValue());
}
@Test
@DisplayName("Should not support RETURNING clause")
void shouldNotSupportReturningClause() {
assertFalse(provider.supportsReturningClause());
}
@Test
@DisplayName("Should throw exception when building RETURNING clause")
void shouldThrowExceptionWhenBuildingReturningClause() {
UnsupportedOperationException exception = assertThrows(
UnsupportedOperationException.class,
() -> provider.buildReturningClause("id", "name")
);
assertTrue(exception.getMessage().contains("MySQL does not support RETURNING"));
assertTrue(exception.getMessage().contains("LAST_INSERT_ID"));
}
}
@Nested
@DisplayName("Schema/DDL Tests")
class SchemaDDLTests {
@Test
@DisplayName("Should return AUTO_INCREMENT definition")
void shouldReturnAutoIncrementDefinition() {
String result = provider.getAutoIncrementDefinition();
assertEquals("INT NOT NULL AUTO_INCREMENT", result);
}
@Test
@DisplayName("Should return TIMESTAMP with ON UPDATE definition")
void shouldReturnTimestampWithOnUpdateDefinition() {
String result = provider.getTimestampDefinition();
assertEquals("TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP", result);
}
}
}