package at.mocode.plugins import at.mocode.tables.ArtikelTable import at.mocode.tables.PlaetzeTable import at.mocode.tables.TurniereTable import at.mocode.tables.VeranstaltungenTable import at.mocode.tables.domaene.DomQualifikationTable import at.mocode.tables.stammdaten.LizenzenTable import at.mocode.tables.stammdaten.PersonenTable import at.mocode.tables.stammdaten.PferdeTable import at.mocode.tables.stammdaten.VereineTable import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import io.ktor.server.application.* import io.ktor.server.config.* import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.transactions.transaction import org.slf4j.Logger import org.slf4j.LoggerFactory import java.util.concurrent.TimeUnit /** * Configures the database connection based on the environment. * Supports three environments: * 1. Test environment - Uses in-memory H2 database * 2. Development environment - Uses in-memory H2 database * 3. Production environment - Uses PostgreSQL database * * @param application The Ktor application instance to read configuration from */ fun Application.configureDatabase() { val log = LoggerFactory.getLogger("DatabaseInitialization") var connectionSuccessful: Boolean // Environment detection val isTestEnvironment = System.getProperty("isTestEnvironment")?.toBoolean() ?: false val dbHostFromEnv = System.getenv("DB_HOST") val isIdeaEnvironment = (dbHostFromEnv == null) // Get database configuration from application.yaml if available val dbConfig = try { environment.config.config("database") } catch (_: ApplicationConfigurationException) { log.warn("No database configuration found in application.yaml, using environment variables") null } when { isTestEnvironment -> { configureTestDatabase(log) connectionSuccessful = true } isIdeaEnvironment -> { configureDevelopmentDatabase(log) connectionSuccessful = true } else -> { connectionSuccessful = configureProductionDatabase(log, dbConfig) } } // Initialize schema if the connection was successful if (connectionSuccessful) { initializeSchema(log, isTestEnvironment, isIdeaEnvironment) } else { log.error("No database connection established. Schema initialization skipped.") } } /** * Configures an in-memory H2 database for testing */ private fun configureTestDatabase(log: Logger): Boolean { log.info("Test environment detected, using in-memory H2 database (test)...") return try { Database.connect( url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL", driver = "org.h2.Driver", user = "sa", password = "" ) log.info("Connected to H2 (test) successfully.") true } catch (e: Exception) { log.error("Failed to connect to H2 (test)!", e) throw e // Rethrow to fail the test } } /** * Configures an in-memory H2 database for development */ private fun configureDevelopmentDatabase(log: Logger): Boolean { log.info("Development environment detected, using in-memory H2 database (dev)...") return try { Database.connect( url = "jdbc:h2:mem:dev;DB_CLOSE_DELAY=-1;MODE=PostgreSQL", driver = "org.h2.Driver", user = "sa", password = "" ) log.info("Connected to H2 (dev) successfully.") true } catch (e: Exception) { log.error("Failed to connect to H2 (dev)!", e) throw e } } /** * Configures a PostgreSQL database for production */ private fun configureProductionDatabase(log: Logger, dbConfig: ApplicationConfig?): Boolean { log.info("Production environment detected, connecting to PostgreSQL...") // Get database configuration from application.yaml or environment variables val dbHost = dbConfig?.propertyOrNull("host")?.getString() ?: System.getenv("DB_HOST") ?: error("Database host not configured") val dbPort = dbConfig?.propertyOrNull("port")?.getString() ?: System.getenv("DB_PORT") ?: "5432" val dbName = dbConfig?.propertyOrNull("name")?.getString() ?: System.getenv("DB_NAME") ?: error("Database name not configured") val dbUser = dbConfig?.propertyOrNull("user")?.getString() ?: System.getenv("DB_USER") ?: error("Database user not configured") val dbPassword = dbConfig?.propertyOrNull("password")?.getString() ?: System.getenv("DB_PASSWORD") ?: error("Database password not configured") // Connection pool configuration val maxPoolSize = dbConfig?.propertyOrNull("pool.maxSize")?.getString()?.toIntOrNull() ?: System.getenv("DB_POOL_SIZE")?.toIntOrNull() ?: 10 val minIdle = dbConfig?.propertyOrNull("pool.minIdle")?.getString()?.toIntOrNull() ?: 2 val idleTimeout = dbConfig?.propertyOrNull("pool.idleTimeout")?.getString()?.toLongOrNull() ?: 10000L val connectionTimeout = dbConfig?.propertyOrNull("pool.connectionTimeout")?.getString()?.toLongOrNull() ?: 5000L val maxLifetime = dbConfig?.propertyOrNull("pool.maxLifetime")?.getString()?.toLongOrNull() ?: 1800000L val jdbcURL = "jdbc:postgresql://$dbHost:$dbPort/$dbName" log.info("Attempting to connect to PostgreSQL at URL: {}", jdbcURL) return try { val hikariConfig = HikariConfig().apply { driverClassName = "org.postgresql.Driver" jdbcUrl = jdbcURL username = dbUser password = dbPassword maximumPoolSize = maxPoolSize minimumIdle = minIdle this.idleTimeout = idleTimeout this.connectionTimeout = connectionTimeout this.maxLifetime = maxLifetime // Additional security and performance settings addDataSourceProperty("cachePrepStmts", "true") addDataSourceProperty("prepStmtCacheSize", "250") addDataSourceProperty("prepStmtCacheSqlLimit", "2048") addDataSourceProperty("useServerPrepStmts", "true") // Connection validation connectionTestQuery = "SELECT 1" validationTimeout = TimeUnit.SECONDS.toMillis(5) // Leak detection leakDetectionThreshold = TimeUnit.SECONDS.toMillis(60) validate() } val dataSource = HikariDataSource(hikariConfig) Database.connect(dataSource) log.info("PostgreSQL connection pool initialized successfully!") true } catch (e: Exception) { log.error("Failed to initialize PostgreSQL connection pool!", e) throw e // Rethrow in production } } /** * Initializes the database schema */ private fun initializeSchema(log: Logger, isTestEnvironment: Boolean, isIdeaEnvironment: Boolean) { transaction { log.info("Initializing/Verifying database schema...") try { // Create all tables if they don't exist SchemaUtils.create( VereineTable, PersonenTable, PferdeTable, VeranstaltungenTable, TurniereTable, ArtikelTable, PlaetzeTable, LizenzenTable, DomQualifikationTable // Add more tables here if needed ) log.info("Database schema initialized successfully.") } catch (e: Exception) { log.error("Failed to initialize database schema!", e) // In production, a schema initialization failure is critical if (!isTestEnvironment && !isIdeaEnvironment) { throw e } // In test/development, just log the error } } }