libeufin

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

commit 9439260e0b60b5a5fa4a1d636e9f0075396a6b76
parent 4e734df083a38519c198a5b199807e85bcbd2789
Author: Antoine A <>
Date:   Tue, 31 Oct 2023 14:30:50 +0000

Add tiny_amount and rounding_mode to conversion info

Diffstat:
Mbank/conf/test.conf | 4+++-
Mbank/src/main/kotlin/tech/libeufin/bank/Config.kt | 55+++++++++++++++++++++++++++++++++++++++++++------------
Mbank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt | 22++++++++++++++++------
Mbank/src/main/kotlin/tech/libeufin/bank/Database.kt | 43+++++++++++++++++++++++++++++++++----------
Mbank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt | 8+++++++-
Mbank/src/main/kotlin/tech/libeufin/bank/helpers.kt | 6+++---
Mbank/src/test/kotlin/AmountTest.kt | 10+++++-----
Mbank/src/test/kotlin/CoreBankApiTest.kt | 32+++++++++++++++++++++++++-------
Mdatabase-versioning/libeufin-bank-0001.sql | 2+-
Mdatabase-versioning/libeufin-bank-procedures.sql | 55++++++++++++++++++++++++++++++++++---------------------
Mutil/src/main/kotlin/DB.kt | 4++++
11 files changed, 174 insertions(+), 67 deletions(-)

diff --git a/bank/conf/test.conf b/bank/conf/test.conf @@ -13,6 +13,8 @@ CONFIG = postgresql:///libeufincheck [libeufin-bank-conversion] buy_at_ratio = 0.8 +buy_in_fee = 0.02 +buy_tiny_amount = KUDOS:0.01 +buy_rounding_mode = nearest sell_at_ratio = 1.25 -buy_in_fee = 0.002 sell_out_fee = 0.003 \ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt @@ -85,9 +85,13 @@ data class BankConfig( @Serializable data class ConversionInfo ( val buy_at_ratio: DecimalNumber, - val sell_at_ratio: DecimalNumber, val buy_in_fee: DecimalNumber, + val buy_tiny_amount: TalerAmount, + val buy_rounding_mode: RoundingMode, + val sell_at_ratio: DecimalNumber, val sell_out_fee: DecimalNumber, + val sell_tiny_amount: TalerAmount, + val sell_rounding_mode: RoundingMode, ) data class ServerConfig( @@ -120,7 +124,13 @@ fun TalerConfig.loadBankConfig(): BankConfig = catchError { val currencySpecification = sections.find { it.startsWith("CURRENCY-") && requireBoolean(it, "enabled") && requireString(it, "code") == currency }?.let { loadCurrencySpecification(it) } ?: throw TalerConfigError("missing currency specification for $currency") + var fiatCurrency: String? = null; + var conversionInfo: ConversionInfo? = null; val haveCashout = lookupBoolean("libeufin-bank", "have_cashout") ?: false; + if (haveCashout) { + fiatCurrency = requireString("libeufin-bank", "fiat_currency"); + conversionInfo = loadConversionInfo(currency, fiatCurrency) + } BankConfig( currency = currency, restrictRegistration = lookupBoolean("libeufin-bank", "restrict_registration") ?: false, @@ -133,17 +143,21 @@ fun TalerConfig.loadBankConfig(): BankConfig = catchError { restrictAccountDeletion = lookupBoolean("libeufin-bank", "restrict_account_deletion") ?: true, currencySpecification = currencySpecification, haveCashout = haveCashout, - fiatCurrency = if (haveCashout) { requireString("libeufin-bank", "fiat_currency") } else { null }, - conversionInfo = if (haveCashout) { loadConversionInfo() } else { null } + fiatCurrency = fiatCurrency, + conversionInfo = conversionInfo ) } -private fun TalerConfig.loadConversionInfo(): ConversionInfo = catchError { +private fun TalerConfig.loadConversionInfo(currency: String, fiatCurrency: String): ConversionInfo = catchError { ConversionInfo( buy_at_ratio = requireDecimalNumber("libeufin-bank-conversion", "buy_at_ratio"), - sell_at_ratio = requireDecimalNumber("libeufin-bank-conversion", "sell_at_ratio"), buy_in_fee = requireDecimalNumber("libeufin-bank-conversion", "buy_in_fee"), - sell_out_fee = requireDecimalNumber("libeufin-bank-conversion", "sell_out_fee") + buy_tiny_amount = amount("libeufin-bank-conversion", "buy_tiny_amount", currency) ?: TalerAmount(0, 1, currency), + buy_rounding_mode = RoundingMode("libeufin-bank-conversion", "buy_rounding_mode") ?: RoundingMode.zero, + sell_at_ratio = requireDecimalNumber("libeufin-bank-conversion", "sell_at_ratio"), + sell_out_fee = requireDecimalNumber("libeufin-bank-conversion", "sell_out_fee"), + sell_tiny_amount = amount("libeufin-bank-conversion", "sell_tiny_amount", fiatCurrency) ?: TalerAmount(0, 1, fiatCurrency), + sell_rounding_mode = RoundingMode("libeufin-bank-conversion", "sell_rounding_mode") ?: RoundingMode.zero, ) } @@ -159,9 +173,8 @@ private fun TalerConfig.loadCurrencySpecification(section: String): CurrencySpec ) } -private fun TalerConfig.requireAmount(section: String, option: String, currency: String): TalerAmount = catchError { - val amountStr = lookupString(section, option) ?: - throw TalerConfigError("expected amount for section $section, option $option, but config value is empty") +private fun TalerConfig.amount(section: String, option: String, currency: String): TalerAmount? = catchError { + val amountStr = lookupString(section, option) ?: return@catchError null val amount = try { TalerAmount(amountStr) } catch (e: Exception) { @@ -176,9 +189,13 @@ private fun TalerConfig.requireAmount(section: String, option: String, currency: amount } -private fun TalerConfig.requireDecimalNumber(section: String, option: String): DecimalNumber = catchError { - val numberStr = lookupString(section, option) ?: - throw TalerConfigError("expected decimal number for section $section, option $option, but config value is empty") +private fun TalerConfig.requireAmount(section: String, option: String, currency: String): TalerAmount = catchError { + amount(section, option, currency) ?: + throw TalerConfigError("expected amount for section $section, option $option, but config value is empty") +} + +private fun TalerConfig.decimalNumber(section: String, option: String): DecimalNumber? = catchError { + val numberStr = lookupString(section, option) ?: return@catchError null try { DecimalNumber(numberStr) } catch (e: Exception) { @@ -186,6 +203,20 @@ private fun TalerConfig.requireDecimalNumber(section: String, option: String): D } } +private fun TalerConfig.requireDecimalNumber(section: String, option: String): DecimalNumber = catchError { + decimalNumber(section, option) ?: + throw TalerConfigError("expected decimal number for section $section, option $option, but config value is empty") +} + +private fun TalerConfig.RoundingMode(section: String, option: String): RoundingMode? = catchError { + val str = lookupString(section, option) ?: return@catchError null; + try { + RoundingMode.valueOf(str) + } catch (e: Exception) { + throw TalerConfigError("expected rouding mode for section $section, option $option, but $str is unknown") + } +} + private fun <R> catchError(lambda: () -> R): R { try { return lambda() diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt @@ -561,19 +561,29 @@ fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) { } } get("/cashout-rate") { - val params = CashoutRateParams.extract(call.request.queryParameters) + val params = RateParams.extract(call.request.queryParameters) params.debit?.let { ctx.checkInternalCurrency(it) } params.credit?.let { ctx.checkFiatCurrency(it) } if (params.debit != null) { val credit = db.conversionInternalToFiat(params.debit) - if (params.credit != null && params.credit != credit) { - throw badRequest("Bad conversion expected $credit got $params.credit") - } - call.respond(CashoutConversionResponse(params.debit, credit)) + call.respond(ConversionResponse(params.debit, credit)) } else { - // TODO + call.respond(HttpStatusCode.NotImplemented) // TODO + } + } + get("/cashin-rate") { + val params = RateParams.extract(call.request.queryParameters) + + params.debit?.let { ctx.checkFiatCurrency(it) } + params.credit?.let { ctx.checkInternalCurrency(it) } + + if (params.debit != null) { + val credit = db.conversionFiatToInternal(params.debit) + call.respond(ConversionResponse(params.debit, credit)) + } else { + call.respond(HttpStatusCode.NotImplemented) // TODO } } } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt @@ -1542,33 +1542,56 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f suspend fun conversionUpdateConfig(cfg: ConversionInfo) = conn { it.transaction { conn -> - val stmt = conn.prepareStatement("CALL config_set_amount(?, (?, ?)::taler_amount)") + var stmt = conn.prepareStatement("CALL config_set_amount(?, (?, ?)::taler_amount)") for ((name, amount) in listOf( - Pair("buy_at_ratio", cfg.buy_at_ratio), - Pair("sell_at_ratio", cfg.sell_at_ratio), - Pair("buy_in_fee", cfg.buy_in_fee), - Pair("sell_out_fee", cfg.sell_out_fee) + Pair("buy_ratio", cfg.buy_at_ratio), + Pair("buy_fee", cfg.buy_in_fee), + Pair("sell_ratio", cfg.sell_at_ratio), + Pair("sell_fee", cfg.sell_out_fee), )) { stmt.setString(1, name) stmt.setLong(2, amount.value) stmt.setInt(3, amount.frac) stmt.executeUpdate() } + for ((name, amount) in listOf( + Pair("buy_tiny_amount", cfg.buy_tiny_amount), + Pair("sell_tiny_amount", cfg.sell_tiny_amount) + )) { + stmt.setString(1, name) + stmt.setLong(2, amount.value) + stmt.setInt(3, amount.frac) + stmt.executeUpdate() + } + stmt = conn.prepareStatement("CALL config_set_rounding_mode(?, ?::rounding_mode)") + for ((name, value) in listOf( + Pair("buy_rounding_mode", cfg.buy_rounding_mode), + Pair("sell_rounding_mode", cfg.sell_rounding_mode) + )) { + stmt.setString(1, name) + stmt.setString(2, value.name) + stmt.executeUpdate() + } } } - suspend fun conversionInternalToFiat(internalAmount: TalerAmount): TalerAmount = conn { conn -> - val stmt = conn.prepareStatement("SELECT fiat_amount.val AS amount_val, fiat_amount.frac AS amount_frac FROM conversion_internal_to_fiat((?, ?)::taler_amount) as fiat_amount") - stmt.setLong(1, internalAmount.value) - stmt.setInt(2, internalAmount.frac) + private suspend fun conversionTo(amount: TalerAmount, name: String, currency: String): TalerAmount = conn { conn -> + val stmt = conn.prepareStatement("SELECT amount.val AS amount_val, amount.frac AS amount_frac FROM conversion_to((?, ?)::taler_amount, ?) as amount") + stmt.setLong(1, amount.value) + stmt.setInt(2, amount.frac) + stmt.setString(3, name) stmt.oneOrNull { TalerAmount( value = it.getLong("amount_val"), frac = it.getInt("amount_frac"), - currency = fiatCurrency!! + currency = currency ) }!! } + + suspend fun conversionInternalToFiat(amount: TalerAmount): TalerAmount = conversionTo(amount, "sell", fiatCurrency!!) + suspend fun conversionFiatToInternal(amount: TalerAmount): TalerAmount = conversionTo(amount, "buy", bankCurrency) + } /** Result status of customer account creation */ diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt @@ -51,6 +51,12 @@ enum class CashoutStatus { confirmed } +enum class RoundingMode { + zero, + up, + nearest +} + /** * HTTP response type of successful token refresh. * access_token is the Crockford encoding of the 32 byte @@ -482,7 +488,7 @@ data class CashoutConfirm( ) @Serializable -data class CashoutConversionResponse( +data class ConversionResponse( val amount_debit: TalerAmount, val amount_credit: TalerAmount, ) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt @@ -171,11 +171,11 @@ data class HistoryParams( } } -data class CashoutRateParams( +data class RateParams( val debit: TalerAmount?, val credit: TalerAmount? ) { companion object { - fun extract(params: Parameters): CashoutRateParams { + fun extract(params: Parameters): RateParams { val debit = try { params["amount_debit"]?.run(::TalerAmount) } catch (e: Exception) { @@ -189,7 +189,7 @@ data class CashoutRateParams( if (debit == null && credit == null) { throw badRequest("Either param 'amount_debit' or 'amount_credit' is required") } - return CashoutRateParams(debit, credit) + return RateParams(debit, credit) } } } diff --git a/bank/src/test/kotlin/AmountTest.kt b/bank/src/test/kotlin/AmountTest.kt @@ -208,7 +208,7 @@ class AmountTest { db.conn { conn -> val stmt = conn.prepareStatement("SELECT product.val, product.frac FROM amount_mul((?, ?)::taler_amount, (?, ?)::taler_amount, (?, ?)::taler_amount, ?::rounding_mode) as product") - fun mul(nb: TalerAmount, times: DecimalNumber, tiny: DecimalNumber = DecimalNumber("0.00000001"), roundingMode: String = "round-to-zero"): TalerAmount? { + fun mul(nb: TalerAmount, times: DecimalNumber, tiny: DecimalNumber = DecimalNumber("0.00000001"), roundingMode: String = "zero"): TalerAmount? { stmt.setLong(1, nb.value) stmt.setInt(2, nb.frac) stmt.setLong(3, times.value) @@ -236,9 +236,9 @@ class AmountTest { // Check rounding mode for ((mode, rounding) in listOf( - Pair("round-to-zero", listOf(Pair(1, listOf(10, 11, 12, 12, 14, 15, 16, 17, 18, 19)))), - Pair("round-up", listOf(Pair(1, listOf(10)), Pair(2, listOf(11, 12, 12, 14, 15, 16, 17, 18, 19)))), - Pair("round-to-nearest", listOf(Pair(1, listOf(10, 11, 12, 12, 14)), Pair(2, listOf(15, 16, 17, 18, 19)))) + Pair("zero", listOf(Pair(1, listOf(10, 11, 12, 12, 14, 15, 16, 17, 18, 19)))), + Pair("up", listOf(Pair(1, listOf(10)), Pair(2, listOf(11, 12, 12, 14, 15, 16, 17, 18, 19)))), + Pair("nearest", listOf(Pair(1, listOf(10, 11, 12, 12, 14)), Pair(2, listOf(15, 16, 17, 18, 19)))) )) { for ((rounded, amounts) in rounding) { for (amount in amounts) { @@ -257,7 +257,7 @@ class AmountTest { Pair(20, listOf(18, 19)), )) { for (amount in amounts) { - assertEquals(TalerAmount("HUF:$rounded"), mul(TalerAmount("HUF:$amount"), DecimalNumber("1.01"), DecimalNumber("5"), "round-to-nearest")) + assertEquals(TalerAmount("HUF:$rounded"), mul(TalerAmount("HUF:$amount"), DecimalNumber("1.01"), DecimalNumber("5"), "nearest")) } } } diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -1032,7 +1032,7 @@ class CoreBankCashoutApiTest { private suspend fun ApplicationTestBuilder.convert(amount: String): TalerAmount { // Check conversion client.get("/cashout-rate?amount_debit=$amount").assertOk().run { - val resp = Json.decodeFromString<CashoutConversionResponse>(bodyAsText()) + val resp = Json.decodeFromString<ConversionResponse>(bodyAsText()) return resp.amount_credit } } @@ -1250,16 +1250,12 @@ class CoreBankCashoutApiTest { // GET /cashout-rate @Test - fun rate() = bankSetup { _ -> + fun cashoutRate() = bankSetup { _ -> // Check conversion client.get("/cashout-rate?amount_debit=KUDOS:1").assertOk().run { - val resp = Json.decodeFromString<CashoutConversionResponse>(bodyAsText()) + val resp = Json.decodeFromString<ConversionResponse>(bodyAsText()) assertEquals(TalerAmount("FIAT:1.247"), resp.amount_credit) } - // Check OK - client.get("/cashout-rate?amount_debit=KUDOS:1&amount_credit=FIAT:1.247").assertOk() - // Check bad conversion - client.get("/cashout-rate?amount_debit=KUDOS:1&amount_credit=FIAT:1.25").assertBadRequest() // No amount client.get("/cashout-rate").assertBadRequest() @@ -1269,4 +1265,26 @@ class CoreBankCashoutApiTest { client.get("/cashout-rate?amount_credit=KUDOS:1").assertBadRequest() .assertBadRequest().assertErr(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) } + + // GET /cashin-rate + @Test + fun cashinRate() = bankSetup { _ -> + // Check conversion + for ((amount, converted) in listOf( + Pair(0.75, 0.58), Pair(0.33, 0.24), Pair(0.66, 0.51) + )) { + client.get("/cashin-rate?amount_debit=FIAT:$amount").assertOk().run { + val resp = Json.decodeFromString<ConversionResponse>(bodyAsText()) + assertEquals(TalerAmount("KUDOS:$converted"), resp.amount_credit) + } + } + + // No amount + client.get("/cashin-rate").assertBadRequest() + // Wrong currency + client.get("/cashin-rate?amount_debit=KUDOS:1").assertBadRequest() + .assertBadRequest().assertErr(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) + client.get("/cashin-rate?amount_credit=FIAT:1").assertBadRequest() + .assertBadRequest().assertErr(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) + } } \ No newline at end of file diff --git a/database-versioning/libeufin-bank-0001.sql b/database-versioning/libeufin-bank-0001.sql @@ -51,7 +51,7 @@ CREATE TYPE stat_timeframe_enum AS ENUM ('hour', 'day', 'month', 'year'); CREATE TYPE rounding_mode - AS ENUM ('round-to-zero', 'round-up', 'round-to-nearest'); -- up is toward infinity and down toward zero + AS ENUM ('zero', 'up', 'nearest'); -- FIXME: comments on types (see exchange for example)! diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql @@ -40,23 +40,22 @@ CREATE OR REPLACE FUNCTION amount_mul( ) LANGUAGE plpgsql AS $$ DECLARE - product_numeric NUMERIC(33, 9); -- 16 digit for val, 8 for frac and 1 for rounding error - tiny_numeric NUMERIC; + product_numeric NUMERIC(33, 8); -- 16 digit for val, 8 for frac and 1 for rounding error + tiny_numeric NUMERIC(24); rounding_error int2; BEGIN -- Perform multiplication using big numbers - product_numeric = (a.val::numeric(25, 9) + a.frac::numeric(25, 9) / 100000000) * (b.val::numeric(25, 9) + b.frac::numeric(25, 8) / 100000000); + product_numeric = (a.val::numeric(24) * 100000000 + a.frac::numeric(24)) * (b.val::numeric(24, 8) + b.frac::numeric(24, 8) / 100000000); -- Round to tiny amounts - product_numeric = product_numeric * 100000000; - tiny_numeric = (tiny.val::numeric(33, 9) * 100000000 + tiny.frac::numeric(33, 9)); + tiny_numeric = (tiny.val::numeric(24) * 100000000 + tiny.frac::numeric(24)); product_numeric = product_numeric / tiny_numeric; - rounding_error = trunc(product_numeric * 10 % 10)::int2; - product_numeric = trunc(product_numeric)*tiny_numeric; + rounding_error = (product_numeric * 10 % 10)::int2; + product_numeric = trunc(product_numeric) * tiny_numeric; -- Apply rounding mode - IF (rounding = 'round-to-nearest'::rounding_mode AND rounding_error >= 5) - OR (rounding = 'round-up'::rounding_mode AND rounding_error > 0) THEN + IF (rounding = 'nearest'::rounding_mode AND rounding_error >= 5) + OR (rounding = 'up'::rounding_mode AND rounding_error > 0) THEN product_numeric = product_numeric + tiny_numeric; END IF; @@ -1054,7 +1053,7 @@ DECLARE account_id BIGINT; BEGIN -- check conversion -SELECT in_amount_credit!=expected INTO out_bad_conversion FROM conversion_internal_to_fiat(in_amount_debit) AS expected; +SELECT in_amount_credit!=expected INTO out_bad_conversion FROM conversion_to(in_amount_debit, 'sell'::text) AS expected; IF out_bad_conversion THEN RETURN; END IF; @@ -1273,24 +1272,38 @@ LANGUAGE sql AS $$ ON CONFLICT (key) DO UPDATE SET value = excluded.value $$; -CREATE OR REPLACE FUNCTION conversion_internal_to_fiat( - IN internal_amount taler_amount, - OUT fiat_amount taler_amount +CREATE OR REPLACE PROCEDURE config_set_rounding_mode( + IN name TEXT, + IN mode rounding_mode +) +LANGUAGE sql AS $$ + INSERT INTO config (key, value) VALUES (name, jsonb_build_object('mode', mode::text)) + ON CONFLICT (key) DO UPDATE SET value = excluded.value +$$; + +CREATE OR REPLACE FUNCTION conversion_to( + IN from_amount taler_amount, + IN name TEXT, + OUT to_amount taler_amount ) LANGUAGE plpgsql AS $$ DECLARE - sell_at_ratio taler_amount; - sell_out_fee taler_amount; + at_ratio taler_amount; + out_fee taler_amount; + tiny_amount taler_amount; + mode rounding_mode; calculation_ok BOOLEAN; BEGIN - SELECT value['val']::int8, value['frac']::int4 INTO sell_at_ratio.val, sell_at_ratio.frac FROM config WHERE key='sell_at_ratio'; - SELECT value['val']::int8, value['frac']::int4 INTO sell_out_fee.val, sell_out_fee.frac FROM config WHERE key='sell_out_fee'; - - SELECT product.val, product.frac INTO fiat_amount.val, fiat_amount.frac FROM amount_mul(internal_amount, sell_at_ratio, (0, 1)::taler_amount, 'round-to-zero'::rounding_mode) as product; - SELECT (diff).val, (diff).frac, ok INTO fiat_amount.val, fiat_amount.frac, calculation_ok FROM amount_left_minus_right(fiat_amount, sell_out_fee); + SELECT value['val']::int8, value['frac']::int4 INTO at_ratio.val, at_ratio.frac FROM config WHERE key=name||'_ratio'; + SELECT value['val']::int8, value['frac']::int4 INTO out_fee.val, out_fee.frac FROM config WHERE key=name||'_fee'; + SELECT value['val']::int8, value['frac']::int4 INTO tiny_amount.val, tiny_amount.frac FROM config WHERE key=name||'_tiny_amount'; + SELECT (value->>'mode')::rounding_mode INTO mode FROM config WHERE key=name||'_rounding_mode'; + -- TODO minimum amount + SELECT product.val, product.frac INTO to_amount.val, to_amount.frac FROM amount_mul(from_amount, at_ratio, tiny_amount, mode) as product; + SELECT (diff).val, (diff).frac, ok INTO to_amount.val, to_amount.frac, calculation_ok FROM amount_left_minus_right(to_amount, out_fee); IF NOT calculation_ok THEN - fiat_amount = (0, 0); -- TODO how to handle zero and less than zero ? + to_amount = (0, 0); -- TODO how to handle zero and less than zero ? END IF; END $$; diff --git a/util/src/main/kotlin/DB.kt b/util/src/main/kotlin/DB.kt @@ -236,6 +236,10 @@ fun resetDatabaseTables(cfg: DatabaseConfig, sqlFilePrefix: String) { } val sqlDrop = File("${cfg.sqlDir}/$sqlFilePrefix-drop.sql").readText() + try { conn.execSQLUpdate(sqlDrop) // TODO can fail ? + } catch (e: Exception) { + + } } } \ No newline at end of file