libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

schema.kt (4384B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 2024-2025 Taler Systems S.A.
      4  *
      5  * LibEuFin is free software; you can redistribute it and/or modify
      6  * it under the terms of the GNU Affero General Public License as
      7  * published by the Free Software Foundation; either version 3, or
      8  * (at your option) any later version.
      9  *
     10  * LibEuFin is distributed in the hope that it will be useful, but
     11  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13  * Public License for more details.
     14  *
     15  * You should have received a copy of the GNU Affero General Public
     16  * License along with LibEuFin; see the file COPYING.  If not, see
     17  * <http://www.gnu.org/licenses/>
     18  */
     19 
     20 package tech.libeufin.common.db
     21 
     22 import org.postgresql.ds.*
     23 import org.postgresql.jdbc.PgConnection
     24 import java.sql.Connection
     25 import kotlin.io.path.Path
     26 import kotlin.io.path.exists
     27 import kotlin.io.path.readText
     28 
     29 /**
     30  * Only runs versioning.sql if the _v schema is not found.
     31  *
     32  * @param conn database connection
     33  * @param cfg database configuration
     34  */
     35 private fun maybeApplyV(conn: PgConnection, cfg: DatabaseConfig) {
     36     conn.transaction {
     37         val checkVSchema = conn.talerStatement(
     38             "SELECT schema_name FROM information_schema.schemata WHERE schema_name = '_v'"
     39         )
     40         if (!checkVSchema.executeQueryCheck()) {
     41             logger.debug("_v schema not found, applying versioning.sql")
     42             val sqlVersioning = Path("${cfg.sqlDir}/versioning.sql").readText()
     43             conn.execSQLUpdate(sqlVersioning)
     44         }
     45     }
     46 }
     47 
     48 private fun migrationsPath(sqlFilePrefix: String): Sequence<String> = sequence {
     49     for (n in 1..9999) {
     50         val padded = n.toString().padStart(4, '0')
     51         yield("$sqlFilePrefix-$padded")
     52     }
     53 }
     54 
     55 // sqlFilePrefix is, for example, "libeufin-bank" or "libeufin-nexus" (no trailing dash).
     56 private fun initializeDatabaseTables(conn: PgConnection, cfg: DatabaseConfig, sqlFilePrefix: String) {
     57     logger.info("doing DB initialization, sqldir ${cfg.sqlDir}")
     58     maybeApplyV(conn, cfg)
     59     conn.transaction {
     60         val checkStmt = conn.talerStatement("SELECT EXISTS(SELECT FROM _v.patches where patch_name = ?)")
     61 
     62         for (patchName in migrationsPath(sqlFilePrefix)) {
     63             checkStmt.bind(patchName)
     64             val applied = checkStmt.one { it.getBoolean(1) }
     65             if (applied) {
     66                 logger.debug("patch $patchName already applied")
     67                 continue
     68             }
     69 
     70             val path = Path("${cfg.sqlDir}/$patchName.sql")
     71             if (!path.exists()) {
     72                 logger.debug("path {} doesn't exist anymore, stopping", path)
     73                 break
     74             }
     75             logger.info("applying patch $path")
     76             conn.execSQLUpdate(path.readText())
     77         }
     78         val sqlProcedures = Path("${cfg.sqlDir}/$sqlFilePrefix-procedures.sql")
     79         if (!sqlProcedures.exists()) {
     80             logger.warn("no procedures.sql for the SQL collection: $sqlFilePrefix")
     81             return@transaction
     82         }
     83         logger.info("run procedure.sql")
     84         conn.execSQLUpdate(sqlProcedures.readText())
     85     }
     86 }
     87 
     88 internal fun checkMigrations(conn: PgConnection, cfg: DatabaseConfig, sqlFilePrefix: String) {
     89     val checkStmt = conn.talerStatement("SELECT EXISTS(SELECT FROM _v.patches where patch_name = ?)")
     90 
     91     for (patchName in migrationsPath(sqlFilePrefix)) {
     92         checkStmt.bind(patchName)
     93         val path = Path("${cfg.sqlDir}/$patchName.sql")
     94         if (!path.exists()) break
     95         val applied = checkStmt.one { it.getBoolean(1) }
     96         if (!applied) {
     97             throw Exception("patch $patchName not applied, run '$sqlFilePrefix dbinit'")
     98         }
     99     }
    100 }
    101 
    102 // sqlFilePrefix is, for example, "libeufin-bank" or "libeufin-nexus" (no trailing dash).
    103 private fun resetDatabaseTables(conn: PgConnection, cfg: DatabaseConfig, sqlFilePrefix: String) {
    104     logger.info("reset DB, sqldir ${cfg.sqlDir}")
    105     val sqlDrop = Path("${cfg.sqlDir}/$sqlFilePrefix-drop.sql").readText()
    106     conn.execSQLUpdate(sqlDrop)
    107 }
    108 
    109 fun PGSimpleDataSource.dbInit(cfg: DatabaseConfig, sqlFilePrefix: String, reset: Boolean) {
    110     pgConnection().use { conn ->
    111         if (reset) {
    112             resetDatabaseTables(conn, cfg, sqlFilePrefix)
    113         }
    114         initializeDatabaseTables(conn, cfg, sqlFilePrefix)
    115     }
    116 }