diff --git a/README.md b/README.md index 3b5eb297..070f0e6c 100644 --- a/README.md +++ b/README.md @@ -150,8 +150,8 @@ fun main() { Outputs: ``` - SQL: CREATE TABLE IF NOT EXISTS Cities (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(50) NOT NULL, CONSTRAINT pk_Cities PRIMARY KEY (id)) - SQL: CREATE TABLE IF NOT EXISTS Users (id VARCHAR(10) NOT NULL, name VARCHAR(50) NOT NULL, city_id INT NULL, CONSTRAINT pk_Users PRIMARY KEY (id)) + SQL: CREATE TABLE IF NOT EXISTS Cities (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(50) NOT NULL, CONSTRAINT PK_Cities_ID PRIMARY KEY (id)) + SQL: CREATE TABLE IF NOT EXISTS Users (id VARCHAR(10) NOT NULL, name VARCHAR(50) NOT NULL, city_id INT NULL, CONSTRAINT PK_User_ID PRIMARY KEY (id)) SQL: ALTER TABLE Users ADD FOREIGN KEY (city_id) REFERENCES Cities(id) SQL: INSERT INTO Cities (name) VALUES ('St. Petersburg') SQL: INSERT INTO Cities (name) VALUES ('Munich') diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Function.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Function.kt index 8c411bfa..63bd30e5 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Function.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Function.kt @@ -27,6 +27,12 @@ class LowerCase(val expr: Expression) : Function(VarCharColumn override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append("LOWER(", expr,")") } } +class NextVal(val seq: Sequence) : Function(IntegerColumnType()) { + override fun toQueryBuilder(queryBuilder: QueryBuilder) { + currentDialect.functionProvider.nextVal(seq, queryBuilder) + } +} + class UpperCase(val expr: Expression) : Function(VarCharColumnType()) { override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder { append("UPPER(", expr,")") } } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLExpressionBuilder.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLExpressionBuilder.kt index 2cc6b0be..63620885 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLExpressionBuilder.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SQLExpressionBuilder.kt @@ -38,6 +38,8 @@ fun Expression.trim() : Function = Trim(this) fun Expression.lowerCase() : Function = LowerCase(this) fun Expression.upperCase() : Function = UpperCase(this) +fun Sequence.nextVal() : Function = NextVal(this) + fun ExpressionWithColumnType.function(functionName: String) : Function = CustomFunction(functionName, columnType, this) fun CustomStringFunction(functionName: String, vararg params: Expression<*>) = CustomFunction(functionName, VarCharColumnType(), *params) fun CustomLongFunction(functionName: String, vararg params: Expression<*>) = CustomFunction(functionName, LongColumnType(), *params) 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 008c036d..2bc31d43 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 @@ -86,7 +86,19 @@ object SchemaUtils { } + alters } - fun createSequence(name: String) = Seq(name).createStatement() + fun createSequence(vararg seq: Sequence, inBatch: Boolean = false) { + with(TransactionManager.current()) { + val createStatements = seq.flatMap { it.createStatement() } + execStatements(inBatch, createStatements) + } + } + + fun dropSequence(vararg seq: Sequence, inBatch: Boolean = false) { + with(TransactionManager.current()) { + val dropStatements = seq.flatMap { it.dropStatement() } + execStatements(inBatch, dropStatements) + } + } fun createFKey(reference: Column<*>) = ForeignKeyConstraint.from(reference).createStatement() diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Sequence.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Sequence.kt new file mode 100644 index 00000000..cc098fb4 --- /dev/null +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Sequence.kt @@ -0,0 +1,66 @@ +package org.jetbrains.exposed.sql + +import org.jetbrains.exposed.exceptions.UnsupportedByDialectException +import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.jetbrains.exposed.sql.vendors.currentDialect +import java.lang.StringBuilder + +/** + * Sequence : an object that generates a sequence of numeric values. + * + * @param name The name of the sequence + * @param startWith The first sequence number to be generated. + * @param incrementBy The interval between sequence numbers. + * @param minValue The minimum value of the sequence. + * @param maxValue The maximum value of the sequence. + * @param cycle Indicates that the sequence continues to generate values after reaching either its maximum or minimum value. + * @param cache Number of values of the sequence the database preallocates and keeps in memory for faster access. + */ +class Sequence(private val name: String, + val startWith: Int? = null, + val incrementBy: Int? = null, + val minValue: Int? = null, + val maxValue: Int? = null, + val cycle: Boolean? = null, + val cache: Int? = null) { + + val identifier get() = TransactionManager.current().db.identifierManager.cutIfNecessaryAndQuote(name) + + val ddl: List + get() = createStatement() + + fun createStatement(): List { + if (!currentDialect.supportsCreateSequence ) { + throw UnsupportedByDialectException("The current dialect doesn't support create sequence statement", currentDialect) + } + + val createTableDDL = buildString { + append("CREATE SEQUENCE ") + if (currentDialect.supportsIfNotExists) { + append("IF NOT EXISTS ") + } + append(identifier) + appendIfNotNull(" START WITH", startWith) + appendIfNotNull(" INCREMENT BY", incrementBy) + appendIfNotNull(" MINVALUE", minValue) + appendIfNotNull(" MAXVALUE", maxValue) + + if (cycle == true) { + append(" CYCLE") + } + + appendIfNotNull(" CACHE", cache) + } + + return listOf(createTableDDL) + } + + fun dropStatement() = listOf("DROP SEQUENCE $identifier") + + fun StringBuilder.appendIfNotNull(str: String, strToCheck: Any?) = apply { + if (strToCheck != null) { + this.append("$str $strToCheck") + } + } + +} diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt index 2d5b51d4..d177f7b0 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt @@ -696,7 +696,7 @@ open class Table(name: String = ""): ColumnSet(), DdlAware { override fun createStatement(): List { val seqDDL = autoIncColumn?.autoIncSeqName?.let { - Seq(it).createStatement() + Sequence(it).createStatement() }.orEmpty() val addForeignKeysInAlterPart = SchemaUtils.checkCycle(this) && currentDialect !is SQLiteDialect @@ -768,7 +768,7 @@ open class Table(name: String = ""): ColumnSet(), DdlAware { } } val seqDDL = autoIncColumn?.autoIncSeqName?.let { - Seq(it).dropStatement() + Sequence(it).dropStatement() }.orEmpty() return listOf(dropTableDDL) + seqDDL @@ -792,11 +792,10 @@ open class Table(name: String = ""): ColumnSet(), DdlAware { } } -data class Seq(private val name: String) { - private val identifier get() = TransactionManager.current().db.identifierManager.cutIfNecessaryAndQuote(name) - fun createStatement() = listOf("CREATE SEQUENCE $identifier") - fun dropStatement() = listOf("DROP SEQUENCE $identifier") -} +@Deprecated("Use Sequence class instead of Seq class.", + ReplaceWith("org.jetbrains.exposed.sql.Sequence"), + DeprecationLevel.ERROR) +data class Seq(private val name: String) fun ColumnSet.targetTables(): List = when (this) { is Alias<*> -> listOf(this.delegate) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/InsertStatement.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/InsertStatement.kt index 6658d82d..bc08f5b6 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/InsertStatement.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/InsertStatement.kt @@ -71,7 +71,7 @@ open class InsertStatement(val table: Table, val isIgnore: Boolean = fa } pairs.forEach { (col, value) -> if (value != DefaultValueMarker) { - if (col.columnType.isAutoInc) + if (col.columnType.isAutoInc || value is NextVal) map.getOrPut(col) { value } else map[col] = value @@ -118,8 +118,12 @@ open class InsertStatement(val table: Table, val isIgnore: Boolean = fa } } - protected val autoIncColumns = targets.flatMap { it.columns }.filter { - it.columnType.isAutoInc || (it.columnType is EntityIDColumnType<*> && !currentDialect.supportsOnlyIdentifiersInGeneratedKeys) + protected val autoIncColumns + get() = targets.flatMap { it.columns }.filter { column -> + column.columnType.isAutoInc + || (column.columnType is EntityIDColumnType<*> && !currentDialect.supportsOnlyIdentifiersInGeneratedKeys) + || (column in values.filter { it.value is NextVal }.map { it.key }) + } override fun prepared(transaction: Transaction, sql: String): PreparedStatementApi = when { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt index a539e16e..458973fa 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt @@ -3,6 +3,7 @@ package org.jetbrains.exposed.sql.vendors import org.jetbrains.exposed.exceptions.throwUnsupportedException import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.TransactionManager +import java.lang.StringBuilder import java.nio.ByteBuffer import java.util.* import java.util.concurrent.ConcurrentHashMap @@ -61,6 +62,9 @@ abstract class FunctionProvider { append(prefix, "(", expr, ", ", start, ", ", length, ")") } + open fun nextVal(seq: Sequence, builder: QueryBuilder) = builder { + append(seq.identifier,".NEXTVAL") + } open fun random(seed: Int?): String = "RANDOM(${seed?.toString().orEmpty()})" @@ -264,11 +268,43 @@ interface DatabaseDialect { val supportsOnlyIdentifiersInGeneratedKeys get() = false + val supportsCreateSequence get() = true + // Specific SQL statements fun createIndex(index: Index): String fun dropIndex(tableName: String, indexName: String): String fun modifyColumn(column: Column<*>) : String + + fun createSequence(identifier: String, + startWith: Int?, + incrementBy: Int?, + minValue: Int?, + maxValue: Int?, + cycle: Boolean?, + cache: Int?): String = buildString { + append("CREATE SEQUENCE ") + if (currentDialect.supportsIfNotExists) { + append("IF NOT EXISTS ") + } + append(identifier) + appendIfNotNull(" START WITH", startWith) + appendIfNotNull(" INCREMENT BY", incrementBy) + appendIfNotNull(" MINVALUE", minValue) + appendIfNotNull(" MAXVALUE", maxValue) + + if (cycle == true) { + append(" CYCLE") + } + + appendIfNotNull(" CACHE", cache) + } + + fun StringBuilder.appendIfNotNull(str1: String, str2: Any?) = apply { + if (str2 != null) { + this.append("$str1 $str2") + } + } } abstract class VendorDialect(override val name: String, diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MariaDBDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MariaDBDialect.kt index 24f3b57c..7e663e6d 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MariaDBDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MariaDBDialect.kt @@ -2,11 +2,16 @@ package org.jetbrains.exposed.sql.vendors import org.jetbrains.exposed.sql.Expression import org.jetbrains.exposed.sql.QueryBuilder +import org.jetbrains.exposed.sql.Sequence internal object MariaDBFunctionProvider : MysqlFunctionProvider() { override fun regexp(expr1: Expression, pattern: Expression, caseSensitive: Boolean, queryBuilder: QueryBuilder) { queryBuilder{ append(expr1, " REGEXP ", pattern) } } + + override fun nextVal(seq: Sequence, builder: QueryBuilder) = builder { + append("NEXTVAL(", seq.identifier, ")") + } } class MariaDBDialect : MysqlDialect() { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Mysql.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Mysql.kt index 24acbe78..7a71735c 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Mysql.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Mysql.kt @@ -1,5 +1,6 @@ package org.jetbrains.exposed.sql.vendors +import org.jetbrains.exposed.exceptions.UnsupportedByDialectException import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.TransactionManager import java.math.BigDecimal @@ -71,6 +72,7 @@ internal open class MysqlFunctionProvider : FunctionProvider() { } open class MysqlDialect : VendorDialect(dialectName, MysqlDataTypeProvider, MysqlFunctionProvider.INSTANSE) { + override val supportsCreateSequence = false override fun isAllowedAsColumnDefault(e: Expression<*>): Boolean { if (super.isAllowedAsColumnDefault(e)) return true @@ -129,6 +131,7 @@ open class MysqlDialect : VendorDialect(dialectName, MysqlDataTypeProvider, Mysq TransactionManager.current().db.isVersionCovers(BigDecimal("8.0")) } + companion object { const val dialectName = "mysql" } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt index abb4b50f..bd041e74 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt @@ -119,6 +119,10 @@ internal object PostgreSQLFunctionProvider : FunctionProvider() { append(expr) append(")") } + + override fun nextVal(seq: Sequence, builder: QueryBuilder) = builder { + append("NEXTVAL('", seq.identifier, "')") + } } open class PostgreSQLDialect : VendorDialect(dialectName, PostgreSQLDataTypeProvider, PostgreSQLFunctionProvider) { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt index 63f32011..7a25ff69 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt @@ -95,6 +95,10 @@ internal object SQLServerFunctionProvider : FunctionProvider() { override fun year(expr: Expression, queryBuilder: QueryBuilder) = queryBuilder { append("DATEPART(YEAR, ", expr, ")") } + + override fun nextVal(seq: Sequence, builder: QueryBuilder) = builder { + append("NEXT VALUE FOR ", seq.identifier) + } } open class SQLServerDialect : VendorDialect(dialectName, SQLServerDataTypeProvider, SQLServerFunctionProvider) { diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLiteDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLiteDialect.kt index 64c17886..545a0715 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLiteDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLiteDialect.kt @@ -1,5 +1,6 @@ package org.jetbrains.exposed.sql.vendors +import org.jetbrains.exposed.exceptions.UnsupportedByDialectException import org.jetbrains.exposed.exceptions.throwUnsupportedException import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.TransactionManager @@ -98,6 +99,7 @@ internal object SQLiteFunctionProvider : FunctionProvider() { open class SQLiteDialect : VendorDialect(dialectName, SQLiteDataTypeProvider, SQLiteFunctionProvider) { override val supportsMultipleGeneratedKeys: Boolean = false + override val supportsCreateSequence = false override fun isAllowedAsColumnDefault(e: Expression<*>): Boolean = true override fun createIndex(index: Index): String { diff --git a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityClass.kt b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityClass.kt index f7296a0c..dfd52b3a 100644 --- a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityClass.kt +++ b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityClass.kt @@ -10,6 +10,7 @@ import java.util.concurrent.ConcurrentHashMap import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KClass import kotlin.reflect.full.primaryConstructor +import kotlin.sequences.Sequence @Suppress("UNCHECKED_CAST") abstract class EntityClass, out T: Entity>(val table: IdTable, entityType: Class? = null) { diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateSequenceTest.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateSequenceTest.kt new file mode 100644 index 00000000..8394f434 --- /dev/null +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateSequenceTest.kt @@ -0,0 +1,66 @@ +package org.jetbrains.exposed.sql.tests.shared.ddl + +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.tests.DatabaseTestsBase +import org.jetbrains.exposed.sql.tests.TestDB +import org.jetbrains.exposed.sql.tests.shared.assertEquals +import org.junit.Test + +class CreateSequenceTests : DatabaseTestsBase() { + @Test + fun createSequenceStatementTest() { + withDb(excludeSettings = listOf(TestDB.MYSQL, TestDB.H2_MYSQL, TestDB.SQLITE)) { + val seqDDL = myseq.ddl + + assertEquals("CREATE SEQUENCE " + addIfNotExistsIfSupported() + "${myseq.identifier} " + + "START WITH ${myseq.startWith} " + + "INCREMENT BY ${myseq.incrementBy} " + + "MINVALUE ${myseq.minValue} " + + "MAXVALUE ${myseq.maxValue} " + + "CYCLE " + + "CACHE ${myseq.cache}", + seqDDL) + } + } + + @Test + fun SequenceNextValTest() { + + // Exclude databases that doesn't support create sequence statement(Mysql and SQLite) + withTables(listOf(TestDB.MYSQL, TestDB.H2_MYSQL, TestDB.SQLITE), Developer) { + try { + SchemaUtils.createSequence(myseq) + + var developerId = Developer.insert { + it[id] = myseq.nextVal() + it[name] = "Hichem" + } get Developer.id + + assertEquals(4, developerId) + + developerId = Developer.insert { + it[id] = myseq.nextVal() + it[name] = "Andrey" + } get Developer.id + + assertEquals(6, developerId) + } finally { + SchemaUtils.dropSequence(myseq) + } + } + } + + object Developer : Table() { + val id = integer("id") + var name = varchar("name", 25) + + override val primaryKey = PrimaryKey(id, name) + } + val myseq = Sequence(name= "my_sequence", + startWith= 4, + incrementBy= 2, + minValue= 1, + maxValue= 10, + cycle= true, + cache=20) +}