diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnDiff.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnDiff.kt index dccbaf2387..2e26534fd2 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnDiff.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnDiff.kt @@ -1,15 +1,23 @@ package org.jetbrains.exposed.sql +/** + * Represents differences between a column definition and database metadata for the existing column. + */ data class ColumnDiff( + /** Whether the nullability of the existing column is correct. */ val nullability: Boolean, + /** Whether the existing column has a matching auto-increment sequence. */ val autoInc: Boolean, + /** Whether the default value of the existing column is correct. */ val defaults: Boolean, + /** Whether the existing column identifier matches and has the correct casing. */ val caseSensitiveName: Boolean, ) { - + /** Returns `true` if there is a difference between the column definition and the existing column in the database. */ fun hasDifferences() = this != NoneChanged companion object { + /** A [ColumnDiff] with no differences. */ val NoneChanged = ColumnDiff( nullability = false, autoInc = false, @@ -17,6 +25,7 @@ data class ColumnDiff( caseSensitiveName = false, ) + /** A [ColumnDiff] with differences for every matched property. */ val AllChanged = ColumnDiff( nullability = true, autoInc = true, diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Database.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Database.kt index 691a8ca646..e7d615a7f3 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Database.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Database.kt @@ -14,12 +14,15 @@ import java.util.concurrent.ConcurrentHashMap import javax.sql.ConnectionPoolDataSource import javax.sql.DataSource +/** + * Class representing the underlying database to which connections are made and on which transaction tasks are performed. + */ class Database private constructor( private val resolvedVendor: String? = null, val config: DatabaseConfig, val connector: () -> ExposedConnection<*> ) { - + /** Whether nested transaction blocks are configured to act like top-level transactions. */ var useNestedTransactions: Boolean = config.useNestedTransactions @Deprecated("Use DatabaseConfig to define the useNestedTransactions") @TestOnly @@ -42,26 +45,37 @@ class Database private constructor( } } + /** The connection URL for the database. */ val url: String by lazy { metadata { url } } + + /** The name of the database based on the name of the underlying JDBC driver. */ val vendor: String by lazy { resolvedVendor ?: metadata { databaseDialectName } } + /** The name of the database as a [DatabaseDialect]. */ val dialect by lazy { config.explicitDialect ?: dialects[vendor.lowercase()]?.invoke() ?: error("No dialect registered for $name. URL=$url") } + /** The version number of the database as a [BigDecimal]. */ val version by lazy { metadata { version } } + /** Whether the version number of the database is equal to or greater than the provided [version]. */ fun isVersionCovers(version: BigDecimal) = this.version >= version + /** Whether the database supports ALTER TABLE with an add column clause. */ val supportsAlterTableWithAddColumn by lazy( LazyThreadSafetyMode.NONE ) { metadata { supportsAlterTableWithAddColumn } } + + /** Whether the database supports getting multiple result sets from a single execute. */ val supportsMultipleResultSets by lazy(LazyThreadSafetyMode.NONE) { metadata { supportsMultipleResultSets } } + /** The database-specific class responsible for parsing and processing identifier tokens in SQL syntax. */ val identifierManager by lazy { metadata { identifierManager } } + /** The default number of results that should be fetched when queries are executed. */ var defaultFetchSize: Int? = config.defaultFetchSize private set @@ -111,10 +125,12 @@ class Database private constructor( registerDialect(MariaDBDialect.dialectName) { MariaDBDialect() } } + /** Registers a new [DatabaseDialect] with the identifier [prefix]. */ fun registerDialect(prefix: String, dialect: () -> DatabaseDialect) { dialects[prefix.lowercase()] = dialect } + /** Registers a new JDBC driver, using the specified [driverClassName], with the identifier [prefix]. */ fun registerJdbcDriver(prefix: String, driverClassName: String, dialect: String) { driverMapping[prefix] = driverClassName dialectMapping[prefix] = dialect @@ -134,6 +150,17 @@ class Database private constructor( } } + /** + * Creates a [Database] instance. + * + * **Note:** This function does not immediately instantiate an actual connection to a database, + * but instead provides the details necessary to do so whenever a connection is required by a transaction. + * + * @param datasource The [DataSource] object to be used as a means of getting a connection. + * @param setupConnection Any setup that should be applied to each new connection. + * @param databaseConfig Configuration parameters for this [Database] instance. + * @param manager The [TransactionManager] responsible for new transactions that use this [Database] instance. + */ fun connect( datasource: DataSource, setupConnection: (Connection) -> Unit = {}, @@ -149,6 +176,17 @@ class Database private constructor( ) } + /** + * Creates a [Database] instance. + * + * **Note:** This function does not immediately instantiate an actual connection to a database, + * but instead provides the details necessary to do so whenever a connection is required by a transaction. + * + * @param datasource The [ConnectionPoolDataSource] object to be used as a means of getting a pooled connection. + * @param setupConnection Any setup that should be applied to each new connection. + * @param databaseConfig Configuration parameters for this [Database] instance. + * @param manager The [TransactionManager] responsible for new transactions that use this [Database] instance. + */ fun connectPool( datasource: ConnectionPoolDataSource, setupConnection: (Connection) -> Unit = {}, @@ -164,6 +202,16 @@ class Database private constructor( ) } + /** + * Creates a [Database] instance. + * + * **Note:** This function does not immediately instantiate an actual connection to a database, + * but instead provides the details necessary to do so whenever a connection is required by a transaction. + * + * @param getNewConnection A function that returns a new connection. + * @param databaseConfig Configuration parameters for this [Database] instance. + * @param manager The [TransactionManager] responsible for new transactions that use this [Database] instance. + */ fun connect( getNewConnection: () -> Connection, databaseConfig: DatabaseConfig? = null, @@ -177,6 +225,21 @@ class Database private constructor( ) } + /** + * Creates a [Database] instance. + * + * **Note:** This function does not immediately instantiate an actual connection to a database, + * but instead provides the details necessary to do so whenever a connection is required by a transaction. + * + * @param url The URL that represents the database when getting a connection. + * @param driver The JDBC driver class. If not provided, the specified [url] will be used to find + * a match from the existing driver mappings. + * @param user The database user that owns the new connections. + * @param password The password specific for the database [user]. + * @param setupConnection Any setup that should be applied to each new connection. + * @param databaseConfig Configuration parameters for this [Database] instance. + * @param manager The [TransactionManager] responsible for new transactions that use this [Database] instance. + */ fun connect( url: String, driver: String = getDriver(url), @@ -199,6 +262,7 @@ class Database private constructor( ) } + /** Returns the stored default transaction isolation level for a specific database. */ fun getDefaultIsolationLevel(db: Database): Int = when (db.dialect) { is SQLiteDialect -> Connection.TRANSACTION_SERIALIZABLE @@ -210,12 +274,16 @@ class Database private constructor( url.startsWith(prefix) }?.value ?: error("Database driver not found for $url") + /** Returns the database name used internally for the provided connection [url]. */ fun getDialectName(url: String) = dialectMapping.entries.firstOrNull { (prefix, _) -> url.startsWith(prefix) }?.value } } +/** Represents an [ExposedConnection] that is loaded whenever a connection is accessed by a [Database] instance. */ interface DatabaseConnectionAutoRegistration : (Connection) -> ExposedConnection<*> -val Database.name: String get() = url.substringBefore('?').substringAfterLast('/') +/** Returns the name of the database obtained from its connection URL. */ +val Database.name: String + get() = url.substringBefore('?').substringAfterLast('/') diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/DatabaseConfig.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/DatabaseConfig.kt index 72cdfd857c..684629e6bd 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/DatabaseConfig.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/DatabaseConfig.kt @@ -2,6 +2,12 @@ package org.jetbrains.exposed.sql import org.jetbrains.exposed.sql.vendors.DatabaseDialect +/** + * A configuration class for a [Database]. + * + * Parameters set in this class apply to all transactions that use the [Database] instance, + * unless an applicable override is specified in an individual transaction block. + */ @Suppress("LongParameterList") class DatabaseConfig private constructor( val sqlLogger: SqlLogger, diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Exceptions.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Exceptions.kt index 7de5d70d84..08f68ba960 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Exceptions.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Exceptions.kt @@ -10,6 +10,10 @@ import org.jetbrains.exposed.sql.statements.expandArgs import org.jetbrains.exposed.sql.vendors.DatabaseDialect import java.sql.SQLException +/** + * An exception that provides information about a database access error, + * within the [contexts] of the executed statements that caused the exception. + */ class ExposedSQLException( cause: Throwable?, val contexts: List, @@ -40,7 +44,10 @@ class ExposedSQLException( override fun toString() = "${super.toString()}\nSQL: ${causedByQueries()}" } -@Suppress("MaximumLineLength") +/** + * An exception that provides information about an operation that is not supported by + * the provided [dialect]. + */ class UnsupportedByDialectException(baseMessage: String, val dialect: DatabaseDialect) : UnsupportedOperationException( baseMessage + ", dialect: ${dialect.name}." ) @@ -53,7 +60,6 @@ class UnsupportedByDialectException(baseMessage: String, val dialect: DatabaseDi * * @param columnName the duplicated column name */ -@Suppress("MaximumLineLength") class DuplicateColumnException(columnName: String, tableName: String) : ExceptionInInitializerError( "Duplicate column name \"$columnName\" in table \"$tableName\"" ) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLLog.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLLog.kt index c6d43f125e..7b0db486b5 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLLog.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLLog.kt @@ -1,4 +1,5 @@ package org.jetbrains.exposed.sql + import org.jetbrains.exposed.sql.statements.StatementContext import org.jetbrains.exposed.sql.statements.StatementInterceptor import org.jetbrains.exposed.sql.statements.api.PreparedStatementApi @@ -7,19 +8,30 @@ import org.jetbrains.exposed.sql.transactions.TransactionManager import org.slf4j.LoggerFactory import java.util.* +/** Base class representing a provider of log messages. */ interface SqlLogger { + /** Determines how a log message is routed. */ fun log(context: StatementContext, transaction: Transaction) } +/** Returns a [org.slf4j.Logger] named specifically for Exposed log messages. */ val exposedLogger = LoggerFactory.getLogger("Exposed")!! +/** Class representing a provider of log messages sent to standard output stream. */ object StdOutSqlLogger : SqlLogger { + /** Prints a log message containing the string representation of a complete SQL statement. */ override fun log(context: StatementContext, transaction: Transaction) { - System.out.println("SQL: ${context.expandArgs(transaction)}") + println("SQL: ${context.expandArgs(transaction)}") } } +/** Class representing a provider of log messages at DEBUG level. */ object Slf4jSqlDebugLogger : SqlLogger { + /** + * Logs a message containing the string representation of a complete SQL statement. + * + * **Note:** This is only logged if DEBUG level is currently enabled. + */ override fun log(context: StatementContext, transaction: Transaction) { if (exposedLogger.isDebugEnabled) { exposedLogger.debug(context.expandArgs(TransactionManager.current())) @@ -27,13 +39,16 @@ object Slf4jSqlDebugLogger : SqlLogger { } } +/** Class representing one or more [SqlLogger]s. */ class CompositeSqlLogger : SqlLogger, StatementInterceptor { private val loggers: ArrayList = ArrayList(2) + /** Adds an [SqlLogger] instance. */ fun addLogger(logger: SqlLogger) { loggers.add(logger) } + /** Removes an [SqlLogger] instance. */ fun removeLogger(logger: SqlLogger) { loggers.remove(logger) } @@ -51,6 +66,7 @@ class CompositeSqlLogger : SqlLogger, StatementInterceptor { } } +/** Adds one or more [SqlLogger]s to [this] transaction. */ fun Transaction.addLogger(vararg logger: SqlLogger): CompositeSqlLogger { return CompositeSqlLogger().apply { logger.forEach { this.addLogger(it) } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Schema.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Schema.kt index 403fa0c649..43f9862c78 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Schema.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Schema.kt @@ -6,18 +6,15 @@ import org.jetbrains.exposed.sql.vendors.currentDialect import java.lang.StringBuilder /** - * Database Schema - * - * @param name the schema name - * @param authorization owner_name Specifies the name of the database-level - * principal that will own the schema. - * @param password used only for oracle schema. - * @param defaultTablespace used only for oracle schema. - * @param temporaryTablespace used only for oracle schema. - * @param quota used only for oracle schema. - * @param on used only for oracle schema. - * + * Represents a database schema. * + * @param name The schema name. + * @param authorization Specifies the name of the database-level principal that will own the schema. + * @param password Used only for Oracle schema. + * @param defaultTablespace Used only for Oracle schema. + * @param temporaryTablespace Used only for Oracle schema. + * @param quota Used only for Oracle schema. + * @param on Used only for Oracle schema. */ data class Schema( private val name: String, @@ -28,9 +25,11 @@ data class Schema( val quota: String? = null, val on: String? = null ) { + /** This schema's name in proper database casing. */ + val identifier + get() = TransactionManager.current().db.identifierManager.cutIfNecessaryAndQuote(name) - val identifier get() = TransactionManager.current().db.identifierManager.cutIfNecessaryAndQuote(name) - + /** The SQL statements that create this schema. */ val ddl: List get() = createStatement() @@ -39,6 +38,7 @@ data class Schema( */ fun exists(): Boolean = currentDialect.schemaExists(this) + /** Returns the SQL statements that create this schema. */ fun createStatement(): List { if (!currentDialect.supportsCreateSchema) { throw UnsupportedByDialectException("The current dialect doesn't support create schema statement", currentDialect) @@ -47,6 +47,7 @@ data class Schema( return listOf(currentDialect.createSchema(this)) } + /** Returns the SQL statements that drop this schema, as well as all its objects if [cascade] is `true`. */ fun dropStatement(cascade: Boolean): List { if (!currentDialect.supportsCreateSchema) { throw UnsupportedByDialectException("The current dialect doesn't support drop schema statement", currentDialect) @@ -55,6 +56,7 @@ data class Schema( return listOf(currentDialect.dropSchema(this, cascade)) } + /** Returns the SQL statements that set this schema as the current schema. */ fun setSchemaStatement(): List { if (!currentDialect.supportsCreateSchema) { throw UnsupportedByDialectException("The current dialect doesn't support schemas", currentDialect) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt index 144e74a2f3..5c8a3aac25 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt @@ -5,6 +5,7 @@ import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.vendors.* import java.math.BigDecimal +/** Utility functions that assist with creating, altering, and dropping database schema objects. */ @Suppress("TooManyFunctions") object SchemaUtils { private inline fun logTimeSpent(message: String, withLogs: Boolean, block: () -> R): R { @@ -86,10 +87,13 @@ object SchemaUtils { } } - fun sortTablesByReferences(tables: Iterable) = TableDepthGraph(tables).sorted() + /** Returns a list of [tables] sorted according to the targets of their foreign key constraints, if any exist. */ + fun sortTablesByReferences(tables: Iterable
): List
= TableDepthGraph(tables).sorted() + /** Checks whether any of the [tables] have a sequence of foreign key constraints that cycle back to them. */ fun checkCycle(vararg tables: Table) = TableDepthGraph(tables.toList()).hasCycle() + /** Returns the SQL statements that create all [tables] that do not already exist. */ fun createStatements(vararg tables: Table): List { if (tables.isEmpty()) return emptyList() @@ -103,6 +107,7 @@ object SchemaUtils { } + alters } + /** Creates the provided sequences, using a batch execution if [inBatch] is set to `true`. */ fun createSequence(vararg seq: Sequence, inBatch: Boolean = false) { with(TransactionManager.current()) { val createStatements = seq.flatMap { it.createStatement() } @@ -110,6 +115,7 @@ object SchemaUtils { } } + /** Drops the provided sequences, using a batch execution if [inBatch] is set to `true`. */ fun dropSequence(vararg seq: Sequence, inBatch: Boolean = false) { with(TransactionManager.current()) { val dropStatements = seq.flatMap { it.dropStatement() } @@ -117,6 +123,7 @@ object SchemaUtils { } } + /** Returns the SQL statements that create the provided [ForeignKeyConstraint]. */ fun createFKey(foreignKey: ForeignKeyConstraint): List = with(foreignKey) { val allFromColumnsBelongsToTheSameTable = from.all { it.table == fromTable } require( @@ -133,6 +140,7 @@ object SchemaUtils { return createStatement() } + /** Returns the SQL statements that create the provided [index]. */ fun createIndex(index: Index): List = index.createStatement() @Suppress("NestedBlockDepth", "ComplexMethod") @@ -215,6 +223,16 @@ object SchemaUtils { } } + /** + * Returns the SQL statements that create any columns defined in [tables], which are missing from the existing + * tables in the database. + * + * By default, a description for each intermediate step, as well as its execution time, is logged at the INFO level. + * This can be disabled by setting [withLogs] to `false`. + * + * **Note:** Some dialects, like SQLite, do not support `ALTER TABLE ADD COLUMN` syntax completely. + * Please check the documentation. + */ fun addMissingColumnsStatements(vararg tables: Table, withLogs: Boolean = true): List { if (tables.isEmpty()) return emptyList() @@ -323,6 +341,7 @@ object SchemaUtils { } } + /** Creates all [tables] that do not already exist, using a batch execution if [inBatch] is set to `true`. */ fun create(vararg tables: T, inBatch: Boolean = false) { with(TransactionManager.current()) { execStatements(inBatch, createStatements(*tables)) @@ -408,20 +427,27 @@ object SchemaUtils { } /** - * This function should be used in cases when you want an easy-to-use auto-actualization of database scheme. - * It will create all absent tables, add missing columns for existing tables if it's possible (columns are nullable or have default values). + * This function should be used in cases when an easy-to-use auto-actualization of database schema is required. + * It creates any missing tables and, if possible, adds any missing columns for existing tables + * (for example, when columns are nullable or have default values). * - * Also if there is inconsistency in DB vs code mappings (excessive or absent indexes) - * then DDLs to fix it will be logged to exposedLogger. + * **Note:** Some dialects, like SQLite, do not support `ALTER TABLE ADD COLUMN` syntax completely, + * which restricts the behavior when adding some missing columns. Please check the documentation. * - * This functionality is based on jdbc metadata what might be a bit slow, so it is recommended to call this function once - * at application startup and provide all tables you want to actualize. + * Also, if there is inconsistency between the database schema and table objects (for example, + * excessive or missing indices), then SQL statements to fix this will be logged at the INFO level. * - * Please note, that execution of this function concurrently might lead to unpredictable state in database due to - * non-transactional behavior of some DBMS on processing DDL statements (e.g. MySQL) and metadata caches. - - * To prevent such cases is advised to use any "global" synchronization you prefer (via redis, memcached, etc) or - * with Exposed's provided lock based on synchronization on a dummy "Buzy" table (@see SchemaUtils#withDataBaseLock). + * By default, a description for each intermediate step, as well as its execution time, is logged at the INFO level. + * This can be disabled by setting [withLogs] to `false`. + * + * **Note:** This functionality is reliant on retrieving JDBC metadata, which might be a bit slow. It is recommended + * to call this function only once at application startup and to provide all tables that need to be actualized. + * + * **Note:** Execution of this function concurrently might lead to unpredictable state in the database due to + * non-transactional behavior of some DBMS when processing DDL statements (for example, MySQL) and metadata caches. + * To prevent such cases, it is advised to use any preferred "global" synchronization (via redis or memcached) or + * to use a lock based on synchronization with a dummy table. + * @see SchemaUtils.withDataBaseLock */ fun createMissingTablesAndColumns(vararg tables: Table, inBatch: Boolean = false, withLogs: Boolean = true) { with(TransactionManager.current()) { @@ -455,8 +481,14 @@ object SchemaUtils { } /** - * The function provides a list of statements those need to be executed to make - * existing table definition compatible with Exposed tables mapping. + * Returns the SQL statements that need to be executed to make the existing database schema compatible with + * the table objects defined using Exposed. + * + * **Note:** Some dialects, like SQLite, do not support `ALTER TABLE ADD COLUMN` syntax completely, + * which restricts the behavior when adding some missing columns. Please check the documentation. + * + * By default, a description for each intermediate step, as well as its execution time, is logged at the INFO level. + * This can be disabled by setting [withLogs] to `false`. */ fun statementsRequiredToActualizeScheme(vararg tables: Table, withLogs: Boolean = true): List { val (tablesToCreate, tablesToAlter) = tables.partition { !it.exists() } @@ -486,6 +518,12 @@ object SchemaUtils { return checkMissingIndices(tables = tables, withLogs).flatMap { it.createStatement() } } + /** + * Checks all [tables] for any that have more than one defined foreign key constraint or index and + * logs the findings. + * + * If found, this function also logs the SQL statements that can be used to drop these constraints. + */ fun checkExcessiveIndices(vararg tables: Table) { val excessiveConstraints = currentDialect.columnConstraints(*tables).filter { it.value.size > 1 } @@ -616,6 +654,7 @@ object SchemaUtils { */ fun listTables(): List = currentDialect.allTablesNames() + /** Drops all [tables], using a batch execution if [inBatch] is set to `true`. */ fun drop(vararg tables: Table, inBatch: Boolean = false) { if (tables.isEmpty()) return with(TransactionManager.current()) { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Transaction.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Transaction.kt index 6503a0d270..239c6a0b57 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Transaction.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Transaction.kt @@ -17,31 +17,52 @@ import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit import kotlin.collections.HashMap +/** Represents a key for a value of type [T]. */ class Key +/** + * Class for storing transaction data that should remain available to the transaction scope even + * after the transaction is committed. + */ @Suppress("UNCHECKED_CAST") open class UserDataHolder { + /** A mapping of a [Key] to any data value. */ protected val userdata = ConcurrentHashMap, Any?>() + /** Maps the specified [key] to the specified [value]. */ fun putUserData(key: Key, value: T) { userdata[key] = value } + /** Removes the specified [key] and its corresponding value. */ fun removeUserData(key: Key) = userdata.remove(key) + /** Returns the value to which the specified [key] is mapped, as a value of type [T]. */ fun getUserData(key: Key): T? = userdata[key] as T? + /** + * Returns the value for the specified [key]. If the [key] is not found, the [init] function is called, + * then its result is mapped to the [key] and returned. + */ fun getOrCreate(key: Key, init: () -> T): T = userdata.getOrPut(key, init) as T } +/** Class representing a unit block of work that is performed on a database. */ open class Transaction( private val transactionImpl: TransactionInterface ) : UserDataHolder(), TransactionInterface by transactionImpl { final override val db: Database = transactionImpl.db + /** The current number of statements executed in this transaction. */ var statementCount: Int = 0 + + /** The current total amount of time, in milliseconds, spent executing statements in this transaction. */ var duration: Long = 0 + + /** The threshold in milliseconds for query execution to exceed before logging a warning. */ var warnLongQueriesDuration: Long? = db.config.warnLongQueriesDuration + + /** Whether tracked values like [statementCount] and [duration] should be stored in [statementStats] for debugging. */ var debug = false /** The number of retries that will be made inside this `transaction` block if SQLException happens */ @@ -59,18 +80,30 @@ open class Transaction( */ var queryTimeout: Int? = null + /** The unique ID for this transaction. */ val id by lazy { UUID.randomUUID().toString() } - // currently executing statement. Used to log error properly + /** The currently executing statement. */ var currentStatement: PreparedStatementApi? = null internal val executedStatements: MutableList = arrayListOf() internal var openResultSetsCount: Int = 0 internal val interceptors = arrayListOf() + /** + * A [StringBuilder] containing string representations of previously executed statements + * prefixed by their execution time in milliseconds. + * + * **Note:** [Transaction.debug] must be set to `true` for execution strings to be appended. + */ val statements = StringBuilder() - // prepare statement as key and count to execution time as value + /** + * A mapping of previously executed statements in this transaction, with a string representation of + * the prepared statement as the key and the statement count to execution time as the value. + * + * **Note:** [Transaction.debug] must be set to `true` for this mapping to be populated. + */ val statementStats by lazy { hashMapOf>() } init { @@ -78,8 +111,10 @@ open class Transaction( globalInterceptors // init interceptors } + /** Adds the specified [StatementInterceptor] to act on this transaction. */ fun registerInterceptor(interceptor: StatementInterceptor) = interceptors.add(interceptor) + /** Removes the specified [StatementInterceptor] from acting on this transaction. */ fun unregisterInterceptor(interceptor: StatementInterceptor) = interceptors.remove(interceptor) override fun commit() { @@ -229,10 +264,12 @@ open class Transaction( return answer.first?.let { stmt.body(it) } } + /** Returns the string identifier of a [table], based on its [Table.tableName] and [Table.alias], if applicable. */ fun identity(table: Table): String = (table as? Alias<*>)?.let { "${identity(it.delegate)} ${db.identifierManager.quoteIfNecessary(it.alias)}" } ?: db.identifierManager.quoteIfNecessary(table.tableName.inProperCase()) + /** Returns the complete string identifier of a [column], based on its [Table.tableName] and [Column.name]. */ fun fullIdentity(column: Column<*>): String = QueryBuilder(false).also { fullIdentity(column, it) }.toString() @@ -247,8 +284,10 @@ open class Transaction( append(identity(column)) } + /** Returns the string identifier of a [column], based on its [Column.name]. */ fun identity(column: Column<*>): String = db.identifierManager.quoteIdentifierWhenWrongCaseOrNecessary(column.name) + /** Closes all previously executed statements and resets or releases any used database and/or driver resources. */ fun closeExecutedStatements() { executedStatements.forEach { it.closeIfPossible()