From ae61cc801495d2d17fafcd6459d3e044e89e36dc Mon Sep 17 00:00:00 2001 From: Jocelyne Date: Tue, 13 Aug 2024 16:31:27 +0200 Subject: [PATCH] feat: Add ability to pass custom sequence to auto-increment column --- exposed-core/api/exposed-core.api | 4 + .../org/jetbrains/exposed/sql/ColumnType.kt | 26 +++- .../org/jetbrains/exposed/sql/SchemaUtils.kt | 6 +- .../org/jetbrains/exposed/sql/Sequence.kt | 2 +- .../kotlin/org/jetbrains/exposed/sql/Table.kt | 30 +++-- .../sql/tests/shared/ddl/CreateTableTests.kt | 54 +------- .../sql/tests/shared/ddl/SequencesTests.kt | 118 +++++++++++++++++- 7 files changed, 171 insertions(+), 69 deletions(-) diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index 4976238a14..4d7821983b 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -221,11 +221,13 @@ public final class org/jetbrains/exposed/sql/ArrayColumnType : org/jetbrains/exp public final class org/jetbrains/exposed/sql/AutoIncColumnType : org/jetbrains/exposed/sql/IColumnType { public fun (Lorg/jetbrains/exposed/sql/ColumnType;Ljava/lang/String;Ljava/lang/String;)V + public fun (Lorg/jetbrains/exposed/sql/ColumnType;Lorg/jetbrains/exposed/sql/Sequence;)V public fun equals (Ljava/lang/Object;)Z public final fun getAutoincSeq ()Ljava/lang/String; public final fun getDelegate ()Lorg/jetbrains/exposed/sql/ColumnType; public final fun getNextValExpression ()Lorg/jetbrains/exposed/sql/NextVal; public fun getNullable ()Z + public final fun getSequence ()Lorg/jetbrains/exposed/sql/Sequence; public fun hashCode ()I public fun nonNullValueAsDefaultString (Ljava/lang/Object;)Ljava/lang/String; public fun nonNullValueToString (Ljava/lang/Object;)Ljava/lang/String; @@ -2132,6 +2134,7 @@ public final class org/jetbrains/exposed/sql/Sequence { public final fun getIncrementBy ()Ljava/lang/Long; public final fun getMaxValue ()Ljava/lang/Long; public final fun getMinValue ()Ljava/lang/Long; + public final fun getName ()Ljava/lang/String; public final fun getStartWith ()Ljava/lang/Long; } @@ -2424,6 +2427,7 @@ public class org/jetbrains/exposed/sql/Table : org/jetbrains/exposed/sql/ColumnS public static synthetic fun array$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Lorg/jetbrains/exposed/sql/ColumnType;Ljava/lang/Integer;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; public final fun autoGenerate (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/Column; public final fun autoIncrement (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; + public final fun autoIncrement (Lorg/jetbrains/exposed/sql/Column;Lorg/jetbrains/exposed/sql/Sequence;)Lorg/jetbrains/exposed/sql/Column; public static synthetic fun autoIncrement$default (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; public final fun autoinc (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; public static synthetic fun autoinc$default (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt index 6d4b5488cb..b081f68211 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt @@ -136,9 +136,21 @@ class AutoIncColumnType( private val fallbackSeqName: String ) : IColumnType by delegate { - private val nextValValue = run { - val sequence = Sequence(_autoincSeq ?: fallbackSeqName) - if (delegate is IntegerColumnType) sequence.nextIntVal() else sequence.nextLongVal() + private var _sequence: Sequence? = null + + /** The sequence used to generate new values for this auto-increment column. */ + val sequence: Sequence? + get() = _sequence ?: autoincSeq?.let { + Sequence( + it, + startWith = 1, + minValue = 1, + maxValue = Long.MAX_VALUE + ) + } + + constructor(delegate: ColumnType, sequence: Sequence) : this(delegate, sequence.name, sequence.name) { + _sequence = sequence } /** The name of the sequence used to generate new values for this auto-increment column. */ @@ -148,7 +160,9 @@ class AutoIncColumnType( /** The SQL expression that advances the sequence of this auto-increment column. */ val nextValExpression: NextVal<*>? - get() = nextValValue.takeIf { autoincSeq != null } + get() = autoincSeq?.let { + if (delegate is IntegerColumnType) sequence?.nextIntVal() else sequence?.nextLongVal() + } private fun resolveAutoIncType(columnType: IColumnType<*>): String = when { columnType is EntityIDColumnType<*> -> resolveAutoIncType(columnType.idColumn.columnType) @@ -178,6 +192,7 @@ class AutoIncColumnType( delegate != other.delegate -> false _autoincSeq != other._autoincSeq -> false fallbackSeqName != other.fallbackSeqName -> false + sequence != other.sequence -> false else -> true } } @@ -186,6 +201,7 @@ class AutoIncColumnType( var result = delegate.hashCode() result = 31 * result + (_autoincSeq?.hashCode() ?: 0) result = 31 * result + fallbackSeqName.hashCode() + result = 31 * result + (sequence?.hashCode() ?: 0) return result } } @@ -265,7 +281,7 @@ interface ColumnTransformer { fun wrap(value: Unwrapped): Wrapped } -fun columnTransformer(unwrap: (value: Wrapped) -> Unwrapped, wrap: (value: Unwrapped) -> Wrapped): ColumnTransformer { +fun columnTransformer(unwrap: (value: Wrapped) -> Unwrapped, wrap: (value: Unwrapped) -> Wrapped): ColumnTransformer { return object : ColumnTransformer { override fun unwrap(value: Wrapped): Unwrapped = unwrap(value) override fun wrap(value: Unwrapped): Wrapped = wrap(value) 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 7c677f23a6..edfc6da40c 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 @@ -109,12 +109,12 @@ object SchemaUtils { } private fun tableDdlWithoutExistingSequence(table: Table): List { - val existingAutoIncSeq = table.autoIncColumn?.autoIncColumnType?.autoincSeq - ?.takeIf { currentDialect.sequenceExists(Sequence(it)) } + val existingAutoIncSeq = table.autoIncColumn?.autoIncColumnType?.sequence + ?.takeIf { currentDialect.sequenceExists(it) } return table.ddl.filter { statement -> if (existingAutoIncSeq != null) { - !statement.lowercase().startsWith("create sequence") || !statement.contains(existingAutoIncSeq) + !statement.lowercase().startsWith("create sequence") || !statement.contains(existingAutoIncSeq.name) } else { true } 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 index 0a89902279..e1aa458cee 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Sequence.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Sequence.kt @@ -17,7 +17,7 @@ import org.jetbrains.exposed.sql.vendors.currentDialect * @param cache Specifies how many sequence numbers are to be pre-allocated and stored in memory for faster access. */ class Sequence( - private val name: String, + val name: String, val startWith: Long? = null, val incrementBy: Long? = null, val minValue: Long? = null, 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 1a4de991be..53b20a2b00 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 @@ -920,6 +920,17 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { fun Column.autoIncrement(idSeqName: String? = null): Column = cloneWithAutoInc(idSeqName).also { replaceColumn(this, it) } + /** + * Make @receiver column an auto-increment column to generate its values in a database. + * **Note:** Only integer and long columns are supported (signed and unsigned types). + * Some databases, like PostgreSQL, support auto-increment via sequences. + * In this case, a sequence should be provided using the [sequence] param. + * + * @param sequence a parameter to provide a sequence + */ + fun Column.autoIncrement(sequence: Sequence): Column = + cloneWithAutoInc(sequence).also { replaceColumn(this, it) } + /** * Make @receiver column an auto-increment column to generate its values in a database. * **Note:** Only integer and long columns are supported (signed and unsigned types). @@ -1571,6 +1582,12 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { else -> error("Unsupported column type for auto-increment $columnType") } + private fun Column.cloneWithAutoInc(sequence: Sequence): Column = when (columnType) { + is AutoIncColumnType -> this + is ColumnType -> this.withColumnType(AutoIncColumnType(columnType, sequence)) + else -> error("Unsupported column type for auto-increment $columnType") + } + // DDL statements internal fun primaryKeyConstraint(): String? { @@ -1636,16 +1653,7 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { } private fun createAutoIncColumnSequence(): List { - return autoIncColumn?.autoIncColumnType?.autoincSeq?.let { - Sequence( - it, - startWith = 1, - minValue = 1, - maxValue = Long.MAX_VALUE - ) - } - ?.createStatement() - .orEmpty() + return autoIncColumn?.autoIncColumnType?.sequence?.createStatement().orEmpty() } override fun modifyStatement(): List = @@ -1665,7 +1673,7 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { } } - val dropSequence = autoIncColumn?.autoIncColumnType?.autoincSeq?.let { Sequence(it).dropStatement() }.orEmpty() + val dropSequence = autoIncColumn?.autoIncColumnType?.sequence?.dropStatement().orEmpty() return listOf(dropTable) + dropSequence } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateTableTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateTableTests.kt index f585b16efe..588621f391 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateTableTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateTableTests.kt @@ -308,14 +308,7 @@ class CreateTableTests : DatabaseTestsBase() { withDb { val t = TransactionManager.current() val expected = listOfNotNull( - child.autoIncColumn?.autoIncColumnType?.autoincSeq?.let { - Sequence( - it, - startWith = 1, - minValue = 1, - maxValue = Long.MAX_VALUE - ).createStatement().single() - }, + child.autoIncColumn?.autoIncColumnType?.sequence?.createStatement()?.single(), "CREATE TABLE " + addIfNotExistsIfSupported() + "${t.identity(child)} (" + "${child.columns.joinToString { it.descriptionDdl(false) }}," + " CONSTRAINT ${t.db.identifierManager.cutIfNecessaryAndQuote(fkName).inProperCase()}" + @@ -389,14 +382,7 @@ class CreateTableTests : DatabaseTestsBase() { withDb { val t = TransactionManager.current() val expected = listOfNotNull( - child.autoIncColumn?.autoIncColumnType?.autoincSeq?.let { - Sequence( - it, - startWith = 1, - minValue = 1, - maxValue = Long.MAX_VALUE - ).createStatement().single() - }, + child.autoIncColumn?.autoIncColumnType?.sequence?.createStatement()?.single(), "CREATE TABLE " + addIfNotExistsIfSupported() + "${t.identity(child)} (" + "${child.columns.joinToString { it.descriptionDdl(false) }}," + " CONSTRAINT ${t.db.identifierManager.cutIfNecessaryAndQuote(fkName).inProperCase()}" + @@ -424,14 +410,7 @@ class CreateTableTests : DatabaseTestsBase() { withDb { val t = TransactionManager.current() val expected = listOfNotNull( - child.autoIncColumn?.autoIncColumnType?.autoincSeq?.let { - Sequence( - it, - startWith = 1, - minValue = 1, - maxValue = Long.MAX_VALUE - ).createStatement().single() - }, + child.autoIncColumn?.autoIncColumnType?.sequence?.createStatement()?.single(), "CREATE TABLE " + addIfNotExistsIfSupported() + "${t.identity(child)} (" + "${child.columns.joinToString { it.descriptionDdl(false) }}," + " CONSTRAINT ${t.db.identifierManager.cutIfNecessaryAndQuote(fkName).inProperCase()}" + @@ -462,14 +441,7 @@ class CreateTableTests : DatabaseTestsBase() { withDb { val t = TransactionManager.current() val expected = listOfNotNull( - child.autoIncColumn?.autoIncColumnType?.autoincSeq?.let { - Sequence( - it, - startWith = 1, - minValue = 1, - maxValue = Long.MAX_VALUE - ).createStatement().single() - }, + child.autoIncColumn?.autoIncColumnType?.sequence?.createStatement()?.single(), "CREATE TABLE " + addIfNotExistsIfSupported() + "${t.identity(child)} (" + "${child.columns.joinToString { it.descriptionDdl(false) }}," + " CONSTRAINT ${t.db.identifierManager.cutIfNecessaryAndQuote(fkName).inProperCase()}" + @@ -507,14 +479,7 @@ class CreateTableTests : DatabaseTestsBase() { val t = TransactionManager.current() val updateCascadePart = if (testDb != TestDB.ORACLE) " ON UPDATE CASCADE" else "" val expected = listOfNotNull( - child.autoIncColumn?.autoIncColumnType?.autoincSeq?.let { - Sequence( - it, - startWith = 1, - minValue = 1, - maxValue = Long.MAX_VALUE - ).createStatement().single() - }, + child.autoIncColumn?.autoIncColumnType?.sequence?.createStatement()?.single(), "CREATE TABLE " + addIfNotExistsIfSupported() + "${t.identity(child)} (" + "${child.columns.joinToString { it.descriptionDdl(false) }}," + " CONSTRAINT ${t.db.identifierManager.cutIfNecessaryAndQuote(fkName).inProperCase()}" + @@ -553,14 +518,7 @@ class CreateTableTests : DatabaseTestsBase() { withDb { val t = TransactionManager.current() val expected = listOfNotNull( - child.autoIncColumn?.autoIncColumnType?.autoincSeq?.let { - Sequence( - it, - startWith = 1, - minValue = 1, - maxValue = Long.MAX_VALUE - ).createStatement().single() - }, + child.autoIncColumn?.autoIncColumnType?.sequence?.createStatement()?.single(), "CREATE TABLE " + addIfNotExistsIfSupported() + "${t.identity(child)} (" + "${child.columns.joinToString { it.descriptionDdl(false) }}," + " CONSTRAINT ${t.db.identifierManager.cutIfNecessaryAndQuote(fkName).inProperCase()}" + diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/SequencesTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/SequencesTests.kt index 4191439697..9859ad8f50 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/SequencesTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/SequencesTests.kt @@ -9,6 +9,7 @@ import org.jetbrains.exposed.sql.tests.TestDB import org.jetbrains.exposed.sql.tests.currentDialectTest import org.jetbrains.exposed.sql.tests.inProperCase import org.jetbrains.exposed.sql.tests.shared.assertEquals +import org.jetbrains.exposed.sql.tests.shared.assertFalse import org.jetbrains.exposed.sql.tests.shared.assertTrue import org.jetbrains.exposed.sql.vendors.currentDialect import org.junit.Test @@ -61,6 +62,48 @@ class SequencesTests : DatabaseTestsBase() { } } + @Test + fun `testInsertWithCustomSequence`() { + val customSequence = Sequence( + name = "my_sequence", + startWith = 4, + incrementBy = 2, + minValue = 1, + maxValue = 100, + cycle = true, + cache = 20 + ) + val tester = object : Table("tester") { + val id = integer("id").autoIncrement(customSequence) + var name = varchar("name", 25) + + override val primaryKey = PrimaryKey(id, name) + } + withDb { + if (currentDialectTest.supportsSequenceAsGeneratedKeys) { + try { + SchemaUtils.create(tester) + assertTrue(customSequence.exists()) + + var testerId = tester.insert { + it[name] = "Hichem" + } get tester.id + + assertEquals(customSequence.startWith, testerId.toLong()) + + testerId = tester.insert { + it[name] = "Andrey" + } get tester.id + + assertEquals(customSequence.startWith!! + customSequence.incrementBy!!, testerId.toLong()) + } finally { + SchemaUtils.drop(tester) + assertFalse(customSequence.exists()) + } + } + } + } + @Test fun `test insert int IdTable with sequences`() { withTables(DeveloperWithLongId) { @@ -88,7 +131,49 @@ class SequencesTests : DatabaseTestsBase() { } @Test - fun `test insert LongIdTable with auth-increment with sequence`() { + fun `testInsertInIdTableWithCustomSequence`() { + val customSequence = Sequence( + name = "my_sequence", + startWith = 4, + incrementBy = 2, + minValue = 1, + maxValue = 100, + cycle = true, + cache = 20 + ) + val tester = object : IdTable("tester") { + override val id = long("id").autoIncrement(customSequence).entityId() + var name = varchar("name", 25) + + override val primaryKey = PrimaryKey(id, name) + } + withDb { + if (currentDialectTest.supportsSequenceAsGeneratedKeys) { + try { + SchemaUtils.create(tester) + assertTrue(customSequence.exists()) + + var testerId = tester.insert { + it[name] = "Hichem" + } get tester.id + + assertEquals(customSequence.startWith, testerId.value) + + testerId = tester.insert { + it[name] = "Andrey" + } get tester.id + + assertEquals(customSequence.startWith!! + customSequence.incrementBy!!, testerId.value) + } finally { + SchemaUtils.drop(tester) + assertFalse(customSequence.exists()) + } + } + } + } + + @Test + fun `test insert LongIdTable with auto-increment with sequence`() { withDb { if (currentDialectTest.supportsSequenceAsGeneratedKeys) { try { @@ -152,6 +237,37 @@ class SequencesTests : DatabaseTestsBase() { } } + @Test + fun testExistingSequencesForAutoIncrementWithCustomSequence() { + val customSequence = Sequence( + name = "my_sequence", + startWith = 4, + incrementBy = 2, + minValue = 1, + maxValue = 100, + cycle = true, + cache = 20 + ) + val tableWithExplicitSequenceName = object : IdTable() { + override val id: Column> = long("id").autoIncrement(customSequence).entityId() + } + + withDb { + if (currentDialectTest.supportsSequenceAsGeneratedKeys) { + try { + SchemaUtils.create(tableWithExplicitSequenceName) + + val sequences = currentDialectTest.sequences() + + assertTrue(sequences.isNotEmpty()) + assertTrue(sequences.any { it == customSequence.name.inProperCase() }) + } finally { + SchemaUtils.drop(tableWithExplicitSequenceName) + } + } + } + } + @Test fun testExistingSequencesForAutoIncrementWithExplicitSequenceName() { val sequenceName = "id_seq"