From 80d3112080ecec1ec7dc50844d6b4c87f7dee353 Mon Sep 17 00:00:00 2001 From: Miha_x64 Date: Fri, 29 May 2020 14:45:17 +0300 Subject: [PATCH] support for type overrides with JDBC, #32 --- .../net/aquadc/persistence/type/types.kt | 3 - .../persistence/sql/blocking/JdbcSession.kt | 122 ++++++++++++++---- .../persistence/sql/dialect/BaseDialect.kt | 49 ++++--- .../aquadc/persistence/sql/dialect/Dialect.kt | 11 ++ .../aquadc/persistence/sql/dialect/common.kt | 25 ++++ .../persistence/sql/dialect/postgres.kt | 7 +- .../aquadc/persistence/sql/dialect/sqlite.kt | 3 +- .../net/aquadc/persistence/sql/database.kt | 2 +- .../net/aquadc/persistence/sql/postgres.kt | 72 +++++++---- 9 files changed, 215 insertions(+), 79 deletions(-) diff --git a/persistence/src/main/kotlin/net/aquadc/persistence/type/types.kt b/persistence/src/main/kotlin/net/aquadc/persistence/type/types.kt index b0c8225b..0bd6493f 100644 --- a/persistence/src/main/kotlin/net/aquadc/persistence/type/types.kt +++ b/persistence/src/main/kotlin/net/aquadc/persistence/type/types.kt @@ -137,9 +137,6 @@ sealed class DataType { open fun storeAsString(value: T): CharSequence = throw UnsupportedOperationException() - /*open val sqlType: CharSequence? get() = null - open fun storeSqlObject(value: T): Any? = throw UnsupportedOperationException()*/ - override val type: Simple get() = this } diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/JdbcSession.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/JdbcSession.kt index db19fb33..9a708ee3 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/JdbcSession.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/JdbcSession.kt @@ -1,6 +1,8 @@ package net.aquadc.persistence.sql.blocking import net.aquadc.persistence.array +import net.aquadc.persistence.fatAsList +import net.aquadc.persistence.fatMapTo import net.aquadc.persistence.sql.Dao import net.aquadc.persistence.sql.ExperimentalSql import net.aquadc.persistence.sql.Fetch @@ -17,14 +19,16 @@ import net.aquadc.persistence.sql.bindInsertionParams import net.aquadc.persistence.sql.bindQueryParams import net.aquadc.persistence.sql.bindValues import net.aquadc.persistence.sql.dialect.Dialect -import net.aquadc.persistence.sql.flattened +import net.aquadc.persistence.sql.dialect.foldArrayType import net.aquadc.persistence.sql.mapIndexedToArray import net.aquadc.persistence.sql.noOrder import net.aquadc.persistence.struct.Schema import net.aquadc.persistence.struct.Struct +import net.aquadc.persistence.type.AnyCollection import net.aquadc.persistence.type.DataType import net.aquadc.persistence.type.Ilk import net.aquadc.persistence.type.i64 +import net.aquadc.persistence.type.serialized import org.intellij.lang.annotations.Language import java.sql.Connection import java.sql.PreparedStatement @@ -218,14 +222,22 @@ class JdbcSession( private fun Ilk.bind(statement: PreparedStatement, index: Int, value: T) { val i = 1 + index val custom = this.custom - if (custom == null) { - (type as DataType).flattened { isNullable, simple -> + if (custom != null) { + statement.setObject(i, custom.invoke(value)) + } else { + val t = type as DataType + val type = if (t is DataType.Nullable<*, *>) { if (value == null) { - check(isNullable) statement.setNull(i, Types.NULL) - } else { - val v = simple.store(value) - when (simple.kind) { + return + } + t.actualType as DataType.NotNull + } else type as DataType.NotNull + + when (type) { + is DataType.NotNull.Simple -> { + val v = type.store(value) + when (type.kind) { DataType.NotNull.Simple.Kind.Bool -> statement.setBoolean(i, v as Boolean) DataType.NotNull.Simple.Kind.I32 -> statement.setInt(i, v as Int) DataType.NotNull.Simple.Kind.I64 -> statement.setLong(i, v as Long) @@ -236,11 +248,42 @@ class JdbcSession( DataType.NotNull.Simple.Kind.Blob -> statement.setObject(i, v as ByteArray) }//.also { } } + is DataType.NotNull.Collect -> { + foldArrayType( + dialect.hasArraySupport, type.elementType, + { nullable, elT -> + statement.setArray(i, + connection.createArrayOf( + jdbcElType(type.elementType), + toArray(type.store(value), nullable, elT) + ) + ) + }, + { + statement.setObject(i, serialized(type).store(value)) + } + ) + } + is DataType.NotNull.Partial -> { + throw AssertionError() // 🤔 btw, Oracle supports Struct type + } } - } else { - statement.setObject(i, custom.invoke(value), Types.OTHER) } } + private fun jdbcElType(t: DataType<*>): String = when (t) { + is DataType.Nullable<*, *> -> jdbcElType(t.actualType) + is DataType.NotNull.Simple -> dialect.nameOf(t.kind) + is DataType.NotNull.Collect<*, *, *> -> jdbcElType(t.elementType) + is DataType.NotNull.Partial<*, *> -> dialect.nameOf(DataType.NotNull.Simple.Kind.Blob) + } + private fun toArray(value: AnyCollection, nullable: Boolean, elT: DataType.NotNull.Simple): Array = + (value.fatAsList() as List).let { value -> + Array(value.size) { + val el = value[it] + if (el == null) check(nullable).let { null } + else elT.store(el) + } + } @Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST") private /*wannabe inline*/ fun Ilk.get(resultSet: ResultSet, index: Int): T { @@ -248,26 +291,55 @@ class JdbcSession( } private fun Ilk.get1indexed(resultSet: ResultSet, i: Int): T = custom.let { custom -> - if (custom == null) { - (type as DataType).flattened { isNullable, simple -> - val v = when (simple.kind) { - DataType.NotNull.Simple.Kind.Bool -> resultSet.getBoolean(i) - DataType.NotNull.Simple.Kind.I32 -> resultSet.getInt(i) - DataType.NotNull.Simple.Kind.I64 -> resultSet.getLong(i) - DataType.NotNull.Simple.Kind.F32 -> resultSet.getFloat(i) - DataType.NotNull.Simple.Kind.F64 -> resultSet.getDouble(i) - DataType.NotNull.Simple.Kind.Str -> resultSet.getString(i) - DataType.NotNull.Simple.Kind.Blob -> resultSet.getBytes(i) - else -> throw AssertionError() + if (custom != null) { + custom.back(resultSet.getObject(i)) + } else { + val t = type as DataType + val nullable: Boolean + val type = + if (t is DataType.Nullable<*, *>) { nullable = true; t.actualType as DataType.NotNull } + else { nullable = false; type as DataType.NotNull } + when (type) { + is DataType.NotNull.Simple -> { + val v = when (type.kind) { + DataType.NotNull.Simple.Kind.Bool -> resultSet.getBoolean(i) + DataType.NotNull.Simple.Kind.I32 -> resultSet.getInt(i) + DataType.NotNull.Simple.Kind.I64 -> resultSet.getLong(i) + DataType.NotNull.Simple.Kind.F32 -> resultSet.getFloat(i) + DataType.NotNull.Simple.Kind.F64 -> resultSet.getDouble(i) + DataType.NotNull.Simple.Kind.Str -> resultSet.getString(i) + DataType.NotNull.Simple.Kind.Blob -> resultSet.getBytes(i) + else -> throw AssertionError() + } + // must check, will get zeroes otherwise + if (resultSet.wasNull()) check(nullable).let { null as T } + else type.load(v) + } + is DataType.NotNull.Collect -> { + foldArrayType(dialect.hasArraySupport, type.elementType, + { nullable, elT -> + val arr = resultSet.getArray(i) + if (resultSet.wasNull()) { check(nullable); null as T } + else fromArray(type, arr.array as Array, nullable, elT) + }, + { + val obj = resultSet.getObject(i) + if (resultSet.wasNull()) { check(nullable); null as T } + else serialized(type).load(obj) + } + ) + } + is DataType.NotNull.Partial -> { + throw AssertionError() } - // must check, will get zeroes otherwise - if (resultSet.wasNull()) check(isNullable).let { null as T } - else simple.load(v) } - } else { - custom.back(resultSet.getObject(i)) } } + private fun fromArray(type: DataType.NotNull.Collect, value: AnyCollection, nullable: Boolean, elT: DataType.NotNull.Simple<*>): T = + type.load(value.fatMapTo(ArrayList()) { it: Any? -> + if (it == null) { check(nullable); null } else elT.load(it) + }) + override fun cell( query: String, argumentTypes: Array>>, arguments: Array, type: Ilk, orElse: () -> T diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/BaseDialect.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/BaseDialect.kt index ffa30d21..c9ccea26 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/BaseDialect.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/BaseDialect.kt @@ -16,9 +16,10 @@ import java.io.ByteArrayOutputStream import java.io.DataOutputStream @RestrictTo(RestrictTo.Scope.LIBRARY) -/*internal*/ open class BaseDialect( +/*wannabe internal*/ open class BaseDialect( private val types: InlineEnumMap, - private val truncate: String + private val truncate: String, + private val arrayPostfix: String? ) : Dialect { override fun > insert(table: Table): String = buildString { @@ -128,7 +129,7 @@ import java.io.DataOutputStream sb.appendName(colNames[i]).append(' ') .let { val t = colTypes[i] - if (t is DataType<*>) it.appendNameOf(t) + if (t is DataType<*>) it.appendTwN(t) else it.append(t as CharSequence) } @@ -142,7 +143,7 @@ import java.io.DataOutputStream return sb.append(");").toString() } protected open fun StringBuilder.appendPkType(type: DataType.NotNull.Simple<*>, managed: Boolean): StringBuilder = - appendNameOf(type) // used by SQLite, overridden for Postgres + appendTwN(type) // used by SQLite, overridden for Postgres private fun StringBuilder.appendDefault(type: DataType, default: T) { val type = if (type is DataType.Nullable<*, *>) { @@ -172,25 +173,37 @@ import java.io.DataOutputStream } } - protected fun StringBuilder.appendNameOf(dataType: DataType) = apply { - val act = if (dataType is DataType.Nullable<*, *>) dataType.actualType else dataType - when (act) { - is DataType.Nullable<*, *> -> throw AssertionError() - is DataType.NotNull.Simple<*> -> append(types[act.kind]!!) - is DataType.NotNull.Collect<*, *, *> -> append(types[DataType.NotNull.Simple.Kind.Blob]!!) - is DataType.NotNull.Partial<*, *> -> throw UnsupportedOperationException() // column can't be of Partial type at this point - } - if (dataType === act) { - append(' ').append("NOT NULL") - } + /** Appends type along with its non-nullability */ + protected fun StringBuilder.appendTwN(dataType: DataType<*>): StringBuilder { + val nn = dataType is DataType.NotNull<*> + return appendTnN(if (nn) dataType as DataType.NotNull else (dataType as DataType.Nullable<*, *>).actualType) + .appendIf(nn, ' ', "NOT NULL") + } + + /** Appends type without its nullability info, i. e. like it is nullable. */ + private fun StringBuilder.appendTnN(dataType: DataType.NotNull<*>): StringBuilder = when (dataType) { + is DataType.NotNull.Simple<*> -> append(nameOf(dataType.kind)) + is DataType.NotNull.Collect<*, *, *> -> appendTArray(dataType.elementType) + is DataType.NotNull.Partial<*, *> -> throw UnsupportedOperationException() // column can't be of Partial type at this point } + private fun StringBuilder.appendTArray(elementType: DataType<*>): StringBuilder = + foldArrayType(arrayPostfix != null, elementType, + { _, elT -> appendTnN(elT).append(arrayPostfix) }, + //^ all array elements are nullable in Postgres, there's nothing we can do about it. + // Are there any databases which work another way? + { append(nameOf(DataType.NotNull.Simple.Kind.Blob)) } + ) + - /** - * {@implNote SQLite does not have TRUNCATE statement} - */ override fun truncate(table: Table<*, *>): String = buildString(13 + table.name.length) { append(truncate).append(' ').appendName(table.name) } + override val hasArraySupport: Boolean + get() = arrayPostfix != null + + override fun nameOf(kind: DataType.NotNull.Simple.Kind): String = + types[kind]!! + } diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/Dialect.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/Dialect.kt index a32bdfb3..4854cce0 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/Dialect.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/Dialect.kt @@ -4,6 +4,7 @@ import net.aquadc.persistence.struct.Schema import net.aquadc.persistence.sql.Order import net.aquadc.persistence.sql.Table import net.aquadc.persistence.sql.WhereCondition +import net.aquadc.persistence.type.DataType /** * Represents an SQL dialect. Provides functions for building queries. @@ -65,4 +66,14 @@ interface Dialect { */ fun truncate(table: Table<*, *>): String + /** + * Whether database has support for arrays. + */ + val hasArraySupport: Boolean + + /** + * Figures out simple name of a primitive type. + */ + fun nameOf(kind: DataType.NotNull.Simple.Kind): String + } diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/common.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/common.kt index d38c1100..db915cb2 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/common.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/common.kt @@ -1,5 +1,7 @@ package net.aquadc.persistence.sql.dialect +import net.aquadc.persistence.type.DataType + internal fun StringBuilder.appendPlaceholders(count: Int): StringBuilder { if (count == 0) return this @@ -25,3 +27,26 @@ internal inline fun StringBuilder.appendIf(cond: Boolean, what: String): StringB internal inline fun StringBuilder.appendIf(cond: Boolean, what: Char): StringBuilder = if (cond) append(what) else this + +internal inline fun StringBuilder.appendIf(cond: Boolean, what1: Char, what2: String): StringBuilder = + if (cond) append(what1).append(what2) else this + +internal inline fun foldArrayType( + hasArraySupport: Boolean, + elementType: DataType<*>, + ifAppropriate: (nullable: Boolean, actualElementType: DataType.NotNull.Simple<*>) -> R, + ifNot: () -> R +): R { + val nullable: Boolean = elementType is DataType.Nullable<*, *> + val actualElementType: DataType.NotNull<*> = // damn. I really miss Java assignment as expression + if (nullable) (elementType as DataType.Nullable<*, *>).actualType + else elementType as DataType.NotNull<*> + + // arrays of arrays or structs are still serialized. + // PostgreSQL multidimensional arrays are actually matrices + // which is kinda weird surprise and inappropriate constraint. + if (hasArraySupport && actualElementType is DataType.NotNull.Simple) + return ifAppropriate(nullable, actualElementType) + else + return ifNot() +} diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/postgres.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/postgres.kt index ce419434..14d16564 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/postgres.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/postgres.kt @@ -23,12 +23,13 @@ val PostgresDialect: Dialect = object : BaseDialect( DataType.NotNull.Simple.Kind.Str, "text", DataType.NotNull.Simple.Kind.Blob, "bytea" ), - truncate = "TRUNCATE TABLE" + truncate = "TRUNCATE TABLE", + arrayPostfix = "[]" ) { private val serial = DataType.NotNull.Simple.Kind.I32 + DataType.NotNull.Simple.Kind.I64 override fun StringBuilder.appendPkType(type: DataType.NotNull.Simple<*>, managed: Boolean): StringBuilder = - // If PK column is 'managed', we just take `structToInsert[pkField]`. - if (managed || type.kind !in serial) appendNameOf(type) + // If PK column is 'managed', we just take `structToInsert[pkField]`. todo unique constraint + if (managed || type.kind !in serial) appendTwN(type) // Otherwise its our responsibility to make PK auto-generated else append("serial") .appendIf(type.kind == DataType.NotNull.Simple.Kind.I64, '8') diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/sqlite.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/sqlite.kt index 5fddf059..45023366 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/sqlite.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/dialect/sqlite.kt @@ -20,5 +20,6 @@ object SqliteDialect : BaseDialect( DataType.NotNull.Simple.Kind.Str, "TEXT", DataType.NotNull.Simple.Kind.Blob, "BLOB" ), - truncate = "DELETE FROM" + truncate = "DELETE FROM", // SQLite does not have TRUNCATE statement + arrayPostfix = null // no array support ) diff --git a/sql/src/test/kotlin/net/aquadc/persistence/sql/database.kt b/sql/src/test/kotlin/net/aquadc/persistence/sql/database.kt index 6803aa9e..d851c81f 100644 --- a/sql/src/test/kotlin/net/aquadc/persistence/sql/database.kt +++ b/sql/src/test/kotlin/net/aquadc/persistence/sql/database.kt @@ -132,4 +132,4 @@ fun session(dialect: Dialect, url: String): JdbcSession = val stmt = conn.createStatement() TestTables.forEach { stmt.execute(dialect.createTable(it, temporary = true)) } stmt.close() - }, SqliteDialect) + }, dialect) diff --git a/sql/src/test/kotlin/net/aquadc/persistence/sql/postgres.kt b/sql/src/test/kotlin/net/aquadc/persistence/sql/postgres.kt index ccd71a53..1cd55656 100644 --- a/sql/src/test/kotlin/net/aquadc/persistence/sql/postgres.kt +++ b/sql/src/test/kotlin/net/aquadc/persistence/sql/postgres.kt @@ -3,6 +3,7 @@ package net.aquadc.persistence.sql import net.aquadc.persistence.extended.either.EitherLeft import net.aquadc.persistence.extended.either.EitherRight import net.aquadc.persistence.extended.either.fold +import net.aquadc.persistence.extended.intCollection import net.aquadc.persistence.extended.uuid import net.aquadc.persistence.sql.ColMeta.Companion.embed import net.aquadc.persistence.sql.ColMeta.Companion.nativeType @@ -14,7 +15,6 @@ import net.aquadc.persistence.struct.Struct import net.aquadc.persistence.struct.invoke import net.aquadc.persistence.type.DataType import net.aquadc.persistence.type.collection -import net.aquadc.persistence.type.i32 import net.aquadc.persistence.type.i64 import net.aquadc.persistence.type.serialized import net.aquadc.persistence.type.string @@ -57,7 +57,8 @@ class TemplatesPostgres : TemplatesTest() { val Id = "id" let uuid val Name = "name" let string val Extras = "extras" let SomeSchema - val Numbers = "numbers" let collection(i32) + val Numbers = "numbers" let intCollection + val MoreNumbers = "more_numbers" let collection(intCollection) } private fun PGobject(type: String, value: String) = PGobject().also { it.type = type @@ -71,21 +72,23 @@ class TemplatesPostgres : TemplatesTest() { it[B] = 123 it[C] = 10_987_654_321L } - it[Numbers] = intArrayOf(1, 2, 3).asList() + it[Numbers] = intArrayOf(0, 1, 2) + it[MoreNumbers] = listOf(intArrayOf(1, 2, 3), intArrayOf(4, 5, 6)) } @Test fun `just a table`() { val Yuzerz = tableOf(Yoozer, "yoozerz1", "_id", i64) { arrayOf(embed(SnakeCase, Extras)) } - val schema = PostgresDialect.createTable(Yuzerz) + val schema = PostgresDialect.createTable(Yuzerz, true) assertEquals( - """CREATE TABLE "yoozerz1" + """CREATE TEMP TABLE "yoozerz1" ("_id" serial8 NOT NULL PRIMARY KEY, "id" bytea NOT NULL, "name" text NOT NULL, "extras_a" text NOT NULL, "extras_b" int NOT NULL, "extras_c" int8 NOT NULL, - "numbers" bytea NOT NULL);""".replace(Regex("\n\\s+"), " "), + "numbers" int[] NOT NULL, + "more_numbers" bytea NOT NULL);""".replace(Regex("\n\\s+"), " "), schema ) assertInserts(schema, Yuzerz) @@ -98,14 +101,15 @@ class TemplatesPostgres : TemplatesTest() { nativeType(Extras, serialized(SomeSchema)) ) } - val schema = PostgresDialect.createTable(Yuzerz) + val schema = PostgresDialect.createTable(Yuzerz, true) assertEquals( - """CREATE TABLE "yoozerz2" + """CREATE TEMP TABLE "yoozerz2" ("_id" serial NOT NULL PRIMARY KEY, "id" bytea NOT NULL, "name" text NOT NULL, "extras" bytea NOT NULL, - "numbers" bytea NOT NULL);""".replace(Regex("\n\\s+"), " "), + "numbers" int[] NOT NULL, + "more_numbers" bytea NOT NULL);""".replace(Regex("\n\\s+"), " "), schema ) assertInserts(schema, Yuzerz) @@ -128,47 +132,59 @@ class TemplatesPostgres : TemplatesTest() { } } } - val intArray = object : NativeType, DataType.NotNull.Collect, Int, DataType.NotNull.Simple>>("int ARRAY NOT NULL", collection(i32)) { - override fun invoke(p1: List): Any? = - db.connection.unwrap(PgConnection::class.java).createArrayOf("int", p1.toIntArray()) - override fun back(p: Any?): List = - ((p as java.sql.Array).array as Array).asList() + val intMatrix = object : NativeType< + List, + DataType.NotNull.Collect< + List, + IntArray, + DataType.NotNull.Collect>> + >( + "int[][] NOT NULL", collection(intCollection) + ) { + override fun invoke(p1: List): Any? = + db.connection.unwrap(PgConnection::class.java).createArrayOf("int", p1.toTypedArray()) + override fun back(p: Any?): List = + ((p as java.sql.Array).array as Array<*>).map { (it as Array).toIntArray() } + // never cast to Array>: ^^^^^^^^^^^ empty array will be returned as Array } val Yoozerz = tableOf(Yoozer, "yoozerz3", Yoozer.Id) { arrayOf( nativeType(Id, "uuid NOT NULL DEFAULT uuid_generate_v4()"), type(Name, "varchar(128) NOT NULL"), nativeType(Extras, someJsonb), - nativeType(Numbers, intArray) + nativeType(MoreNumbers, intMatrix) ) } - val schema = PostgresDialect.createTable(Yoozerz) + val schema = PostgresDialect.createTable(Yoozerz, true) assertEquals( - """CREATE TABLE "yoozerz3" + """CREATE TEMP TABLE "yoozerz3" ("id" uuid NOT NULL DEFAULT uuid_generate_v4() PRIMARY KEY, "name" varchar(128) NOT NULL, "extras" jsonb NOT NULL, - "numbers" int ARRAY NOT NULL);""".replace(Regex("\n\\s+"), " "), + "numbers" int[] NOT NULL, + "more_numbers" int[][] NOT NULL);""".replace(Regex("\n\\s+"), " "), schema ) assertInserts(schema, Yoozerz) } private fun assertInserts(create: String, table: Table) { + var e: Throwable? = null db.withTransaction { + db.connection.createStatement().run { + execute(create) + close() + } try { - db.connection.createStatement().run { - execute(create) - close() - } - val rec = insert(table, sampleYoozer) assertNotSame(sampleYoozer, rec) assertEquals(sampleYoozer, rec) - } finally { - db.connection.createStatement().run { - execute("DROP TABLE " + table.name) - close() - } + } catch (t: Throwable) { + e = t + } + db.connection.createStatement().run { + execute("DROP TABLE " + table.name) + close() } } + e?.let { throw it } } }