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