From 4ab5040f737156b57cbe8e7fe83d5ee1abf66ff9 Mon Sep 17 00:00:00 2001 From: hermannm Date: Mon, 21 Oct 2024 15:57:14 +0200 Subject: [PATCH] Add support for database-generated integer entity IDs --- docs/usage.md | 2 +- .../documentstore/DocumentStorePlugin.kt | 18 ++- .../liflig/documentstore/entity/EntityId.kt | 28 +++- .../repository/RepositoryJdbi.kt | 149 ++++++++++++++++-- .../documentstore/repository/RowMapper.kt | 15 ++ .../documentstore/repository/BatchTest.kt | 24 +++ .../documentstore/repository/CrudTest.kt | 94 ++++++++++- .../documentstore/repository/ListTest.kt | 3 +- .../documentstore/testutils/ExampleEntity.kt | 10 ++ .../testutils/ExampleRepository.kt | 15 ++ .../{TestDependencies.kt => TestServices.kt} | 8 + ...ple_table.sql => V001__initial_schema.sql} | 30 +++- 12 files changed, 365 insertions(+), 31 deletions(-) rename src/test/kotlin/no/liflig/documentstore/testutils/{TestDependencies.kt => TestServices.kt} (89%) rename src/test/resources/migrations/{V001__Example_table.sql => V001__initial_schema.sql} (63%) diff --git a/docs/usage.md b/docs/usage.md index 04f32eb..5734691 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -6,7 +6,7 @@ column type from Postgres to store data. So you first have to create your table ```sql CREATE TABLE example ( - -- Can have type `text` if using `StringEntityId` + -- Can have type `text` if using `StringEntityId`, or `bigint` if using `IntegerEntityId` id uuid NOT NULL PRIMARY KEY, created_at timestamptz NOT NULL, modified_at timestamptz NOT NULL, diff --git a/src/main/kotlin/no/liflig/documentstore/DocumentStorePlugin.kt b/src/main/kotlin/no/liflig/documentstore/DocumentStorePlugin.kt index a6f1374..fa8cc84 100644 --- a/src/main/kotlin/no/liflig/documentstore/DocumentStorePlugin.kt +++ b/src/main/kotlin/no/liflig/documentstore/DocumentStorePlugin.kt @@ -3,6 +3,7 @@ package no.liflig.documentstore import java.sql.Types +import no.liflig.documentstore.entity.IntegerEntityId import no.liflig.documentstore.entity.StringEntityId import no.liflig.documentstore.entity.UuidEntityId import no.liflig.documentstore.entity.Version @@ -22,23 +23,27 @@ import org.jdbi.v3.core.spi.JdbiPlugin * Once this plugin is installed, the following types can be used as JDBI bind arguments: * - [UuidEntityId] * - [StringEntityId] + * - [IntegerEntityId] * - [Version] * - * In addition, [UuidEntityId] and [StringEntityId] may be used as arguments to + * In addition, [UuidEntityId], [StringEntityId] and [IntegerEntityId] may be used as arguments to * [bindArray][org.jdbi.v3.core.statement.Query.bindArray], like this: * ``` * bindArray("ids", UuidEntityId::class.java, ids) // where ids: List * bindArray("ids", StringEntityId::class.java, ids) // where ids: List + * bindArray("ids", IntegerEntityId::class.java, ids) // where ids: List * ``` */ class DocumentStorePlugin : JdbiPlugin.Singleton() { override fun customizeJdbi(jdbi: Jdbi) { jdbi + .registerArgument(VersionArgumentFactory()) .registerArgument(UuidEntityIdArgumentFactory()) .registerArgument(StringEntityIdArgumentFactory()) - .registerArgument(VersionArgumentFactory()) + .registerArgument(IntegerEntityIdArgumentFactory()) .registerArrayType(UuidEntityId::class.java, "uuid", { id -> id.value }) .registerArrayType(StringEntityId::class.java, "text", { id -> id.value }) + .registerArrayType(IntegerEntityId::class.java, "bigint", { id -> id.value }) } } @@ -58,6 +63,15 @@ private class StringEntityIdArgumentFactory : AbstractArgumentFactory(Types.BIGINT) { + override fun build(value: IntegerEntityId, config: ConfigRegistry?): Argument = + Argument { position, statement, _ -> + statement.setLong(position, value.value) + } +} + /** JDBI argument factory that lets us use [Version] as a bind argument. */ private class VersionArgumentFactory : AbstractArgumentFactory(Types.OTHER) { override fun build(value: Version, config: ConfigRegistry?): Argument = diff --git a/src/main/kotlin/no/liflig/documentstore/entity/EntityId.kt b/src/main/kotlin/no/liflig/documentstore/entity/EntityId.kt index e50cfe1..29a9c83 100644 --- a/src/main/kotlin/no/liflig/documentstore/entity/EntityId.kt +++ b/src/main/kotlin/no/liflig/documentstore/entity/EntityId.kt @@ -1,20 +1,41 @@ package no.liflig.documentstore.entity -import java.util.* +import java.util.UUID +import no.liflig.documentstore.repository.RepositoryJdbi /** EntityId represents the ID pointing to a specific [Entity]. */ sealed interface EntityId -/** UUID version of an [EntityId], for `id UUID` columns. */ +/** UUID version of an [EntityId], for `uuid` columns. */ interface UuidEntityId : EntityId { val value: UUID } -/** String version of an [EntityId], for `id TEXT` columns. */ +/** String version of an [EntityId], for `text` columns. */ interface StringEntityId : EntityId { val value: String } +/** + * Integer version of an [EntityId], for `bigint` columns. + * + * If using [RepositoryJdbi.idsGeneratedByDatabase], the `id` column can be: + * ```sql + * id bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY + * ``` + */ +interface IntegerEntityId : EntityId { + val value: Long + + companion object { + /** + * Temporary value to use when creating entities that have their IDs generated by the database + * (see [RepositoryJdbi.idsGeneratedByDatabase]). + */ + const val GENERATED: Long = -1 + } +} + /** * In order to use JDBI's `bindArray` method to bind list arguments, we have to supply the list's * element type at runtime. We use this in `RepositoryJdbi.listByIds`. However, we don't have the @@ -25,5 +46,6 @@ internal fun getEntityIdType(entityId: EntityId): Class { return when (entityId) { is UuidEntityId -> UuidEntityId::class.java is StringEntityId -> StringEntityId::class.java + is IntegerEntityId -> IntegerEntityId::class.java } } diff --git a/src/main/kotlin/no/liflig/documentstore/repository/RepositoryJdbi.kt b/src/main/kotlin/no/liflig/documentstore/repository/RepositoryJdbi.kt index 320ae5c..e64cd88 100644 --- a/src/main/kotlin/no/liflig/documentstore/repository/RepositoryJdbi.kt +++ b/src/main/kotlin/no/liflig/documentstore/repository/RepositoryJdbi.kt @@ -3,12 +3,15 @@ package no.liflig.documentstore.repository import java.time.Instant +import no.liflig.documentstore.DocumentStorePlugin import no.liflig.documentstore.entity.Entity import no.liflig.documentstore.entity.EntityId +import no.liflig.documentstore.entity.IntegerEntityId import no.liflig.documentstore.entity.Version import no.liflig.documentstore.entity.Versioned import no.liflig.documentstore.entity.getEntityIdType import no.liflig.documentstore.utils.executeBatchOperation +import org.jdbi.v3.core.Handle import org.jdbi.v3.core.Jdbi import org.jdbi.v3.core.mapper.RowMapper import org.jdbi.v3.core.statement.Query @@ -26,7 +29,7 @@ import org.jdbi.v3.core.statement.Query * ```sql * CREATE TABLE example * ( - * -- Can have type `text` if using `StringEntityId` + * -- Can have type `text` if using `StringEntityId`, or `bigint` if using `IntegerEntityId` * id uuid NOT NULL PRIMARY KEY, * created_at timestamptz NOT NULL, * modified_at timestamptz NOT NULL, @@ -35,14 +38,27 @@ import org.jdbi.v3.core.statement.Query * ); * ``` * - * Also, the given [Jdbi] instance must have the - * [DocumentStorePlugin][no.liflig.documentstore.DocumentStorePlugin] installed for the queries in - * this class to work. + * @param jdbi Must have the [DocumentStorePlugin] installed for the queries in this class to work. + * @param serializationAdapter See [SerializationAdapter] for an example of how to implement this. + * @param idsGeneratedByDatabase When using [IntegerEntityId], one often wants the entity IDs to be + * generated by the database. This affects how we perform [create], so if your `id` column is + * `PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY`, you must set this flag to true. + * + * The `create` method still takes an entity with an `id`, but when this flag is set, the `id` + * given to `create` is ignored (you can use [IntegerEntityId.GENERATED] as a dummy ID value). The + * `Versioned` returned by `create` will then have the database-generated ID set on it. If + * you use this entity for anything after creating it (such as returning it to the user), make + * sure to use the entity returned by `create`, so that you get the actual generated ID and not + * the dummy value. + * + * Note that the `id` column cannot be `GENERATED ALWAYS` - see [createEntityWithGeneratedId] for + * the reason. */ open class RepositoryJdbi>( protected val jdbi: Jdbi, protected val tableName: String, protected val serializationAdapter: SerializationAdapter, + protected val idsGeneratedByDatabase: Boolean = false, ) : Repository { protected val rowMapper: RowMapper> = EntityRowMapper(serializationAdapter) @@ -50,12 +66,18 @@ open class RepositoryJdbi>( EntityRowMapperWithTotalCount(serializationAdapter) private val updateResultMapper: RowMapper = UpdateResultMapper() + private val entityDataMapper: RowMapper = EntityDataRowMapper(serializationAdapter) override fun create(entity: EntityT): Versioned { try { useHandle(jdbi) { handle -> - val now = Instant.now() + val createdAt = Instant.now() val version = Version.initial() + + if (idsGeneratedByDatabase) { + return createEntityWithGeneratedId(handle, entity, createdAt, version) + } + handle .createUpdate( """ @@ -67,10 +89,10 @@ open class RepositoryJdbi>( .bind("id", entity.id) .bind("data", serializationAdapter.toJson(entity)) .bind("version", version) - .bind("createdAt", now) - .bind("modifiedAt", now) + .bind("createdAt", createdAt) + .bind("modifiedAt", createdAt) .execute() - return Versioned(entity, version, createdAt = now, modifiedAt = now) + return Versioned(entity, version, createdAt = createdAt, modifiedAt = createdAt) } } catch (e: Exception) { // Call mapDatabaseException first to handle connection-related exceptions, before calling @@ -79,6 +101,72 @@ open class RepositoryJdbi>( } } + /** + * We need a separate query to implement `create` for entities with database-generated IDs (see + * [idsGeneratedByDatabase]), because these entities don't have their IDs already set when given + * to `create`, like we assume for UUID and String IDs. + * + * We have an interesting challenge to solve here, because we store IDs in two places in our + * tables: both in the `id` column, and in the `data` JSONB column (since `id` is part of the + * [Entity] interface, so it becomes part of the JSON). When IDs are generated by the database, we + * need to make sure that the `id` field on `data` is set correctly to the generated `id` column. + * To achieve this, we: + * - Use the fact that `GENERATED` columns in Postgres are backed by a _sequence_ (essentially a + * number generator) + * - We can use the + * [`nextval` function](https://www.postgresql.org/docs/17/functions-sequence.html) to get + * and reserve the next value in a sequence + * - And we can use the + * [`pg_get_serial_sequence` function](https://www.postgresql.org/docs/17/functions-info.html) + * to get the name of the sequence associated with a table's `GENERATED` column + * - Now that we have gotten and reserved the next generated ID, we can use that to set both the + * `id` column and the `data.id` JSONB field at once in our INSERT + * + * Finally, we use `RETURNING data` to get the entity with the generated ID. + * + * This works well, though there is one drawback: since we first get the generated ID with + * `nextval`, and then set the `id` column explicitly, we can't use `GENERATED ALWAYS` on our `id` + * column, since that errors when we set `id` explictly. So we must use `GENERATED BY DEFAULT`, + * although the behavior we have here is more akin to `GENERATED ALWAYS`. + */ + private fun createEntityWithGeneratedId( + handle: Handle, + entity: EntityT, + createdAt: Instant, + version: Version + ): Versioned { + val createdEntity = + handle + .createQuery( + """ + WITH generated_id AS ( + SELECT nextval(pg_get_serial_sequence('${tableName}', 'id')) AS value + ) + INSERT INTO "${tableName}" (id, data, version, created_at, modified_at) + SELECT + generated_id.value, + jsonb_set(:data::jsonb, '{id}', to_jsonb(generated_id.value)), + :version, + :createdAt, + :modifiedAt + FROM generated_id + RETURNING data + """ + .trimIndent(), + ) + .bind("data", serializationAdapter.toJson(entity)) + .bind("version", version) + .bind("createdAt", createdAt) + .bind("modifiedAt", createdAt) + .map(entityDataMapper) + .firstOrNull() + ?: throw IllegalStateException( + "INSERT query for entity with generated ID did not return entity data", + ) + + return Versioned(createdEntity, version, createdAt = createdAt, modifiedAt = createdAt) + } + override fun get(id: EntityIdT, forUpdate: Boolean): Versioned? { useHandle(jdbi) { handle -> return handle @@ -194,9 +282,14 @@ open class RepositoryJdbi>( override fun batchCreate(entities: Iterable) { transactional { useHandle(jdbi) { handle -> - val now = Instant.now() + val createdAt = Instant.now() val version = Version.initial() + if (idsGeneratedByDatabase) { + batchCreateEntitiesWithGeneratedId(handle, entities, createdAt, version) + return@transactional + } + executeBatchOperation( handle, entities, @@ -211,14 +304,48 @@ open class RepositoryJdbi>( .bind("id", entity.id) .bind("data", serializationAdapter.toJson(entity)) .bind("version", version) - .bind("createdAt", now) - .bind("modifiedAt", now) + .bind("createdAt", createdAt) + .bind("modifiedAt", createdAt) }, ) } } } + private fun batchCreateEntitiesWithGeneratedId( + handle: Handle, + entities: Iterable, + createdAt: Instant, + version: Version + ) { + executeBatchOperation( + handle, + entities, + statement = + """ + WITH generated_id AS ( + SELECT nextval(pg_get_serial_sequence('${tableName}', 'id')) AS value + ) + INSERT INTO "${tableName}" (id, data, version, created_at, modified_at) + SELECT + generated_id.value, + jsonb_set(:data::jsonb, '{id}', to_jsonb(generated_id.value)), + :version, + :createdAt, + :modifiedAt + FROM generated_id + """ + .trimIndent(), + bindParameters = { batch, entity -> + batch + .bind("data", serializationAdapter.toJson(entity)) + .bind("version", version) + .bind("createdAt", createdAt) + .bind("modifiedAt", createdAt) + }, + ) + } + override fun batchUpdate(entities: Iterable>) { transactional { useHandle(jdbi) { handle -> diff --git a/src/main/kotlin/no/liflig/documentstore/repository/RowMapper.kt b/src/main/kotlin/no/liflig/documentstore/repository/RowMapper.kt index c791c1d..330a8ee 100644 --- a/src/main/kotlin/no/liflig/documentstore/repository/RowMapper.kt +++ b/src/main/kotlin/no/liflig/documentstore/repository/RowMapper.kt @@ -38,6 +38,21 @@ internal class EntityRowMapper>( } } +/** + * If [RepositoryJdbi.idsGeneratedByDatabase] is set to true, we need to return the entity from our + * INSERT query in order to get the generated ID. In this case, we don't need to return the + * version/createdAt/modifiedAt fields, so we use this row mapper that only maps the entity data. + */ +internal class EntityDataRowMapper>( + private val serializationAdapter: SerializationAdapter, +) : RowMapper { + override fun map(resultSet: ResultSet, ctx: StatementContext): EntityT { + val data = getStringFromRowOrThrow(resultSet, Columns.DATA) + + return serializationAdapter.fromJson(data) + } +} + /** * In [RepositoryJdbi.update], we receive an entity and its previous [Version], and want to return a * [Versioned] wrapper around that entity. We know the [Versioned.modifiedAt] field, since that is diff --git a/src/test/kotlin/no/liflig/documentstore/repository/BatchTest.kt b/src/test/kotlin/no/liflig/documentstore/repository/BatchTest.kt index 1a84f49..43ffe09 100644 --- a/src/test/kotlin/no/liflig/documentstore/repository/BatchTest.kt +++ b/src/test/kotlin/no/liflig/documentstore/repository/BatchTest.kt @@ -5,10 +5,14 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNotEquals import kotlin.test.assertNotNull +import no.liflig.documentstore.entity.IntegerEntityId import no.liflig.documentstore.entity.Version import no.liflig.documentstore.entity.Versioned +import no.liflig.documentstore.testutils.EntityWithIntegerId import no.liflig.documentstore.testutils.ExampleEntity +import no.liflig.documentstore.testutils.ExampleIntegerId import no.liflig.documentstore.testutils.exampleRepo +import no.liflig.documentstore.testutils.exampleRepoWithGeneratedIntegerId import org.junit.jupiter.api.MethodOrderer import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test @@ -89,4 +93,24 @@ class BatchTest { entities = exampleRepo.listByIds(entities.map { it.item.id }) assertEquals(0, entities.size) } + + @Test + fun `test batchCreate with generated IDs`() { + val entitiesToCreate = + (1..largeBatchSize).map { number -> + EntityWithIntegerId( + id = ExampleIntegerId(IntegerEntityId.GENERATED), + text = "batch-test-with-generated-id-${testNumberFormat.format(number)}", + ) + } + exampleRepoWithGeneratedIntegerId.batchCreate(entitiesToCreate) + + val entities = exampleRepoWithGeneratedIntegerId.listAll() + assertNotEquals(0, entities.size) + assert(entities.size >= entitiesToCreate.size) + + val expectedTextFields = entitiesToCreate.map { it.text } + val actualTextFields = entities.map { it.item.text } + assert(actualTextFields.containsAll(expectedTextFields)) + } } diff --git a/src/test/kotlin/no/liflig/documentstore/repository/CrudTest.kt b/src/test/kotlin/no/liflig/documentstore/repository/CrudTest.kt index 60e8b04..4cae713 100644 --- a/src/test/kotlin/no/liflig/documentstore/repository/CrudTest.kt +++ b/src/test/kotlin/no/liflig/documentstore/repository/CrudTest.kt @@ -7,11 +7,16 @@ import kotlin.test.assertFailsWith import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertNull +import no.liflig.documentstore.entity.IntegerEntityId import no.liflig.documentstore.entity.Version +import no.liflig.documentstore.testutils.EntityWithIntegerId import no.liflig.documentstore.testutils.EntityWithStringId import no.liflig.documentstore.testutils.ExampleEntity +import no.liflig.documentstore.testutils.ExampleIntegerId import no.liflig.documentstore.testutils.ExampleStringId import no.liflig.documentstore.testutils.exampleRepo +import no.liflig.documentstore.testutils.exampleRepoWithGeneratedIntegerId +import no.liflig.documentstore.testutils.exampleRepoWithIntegerId import no.liflig.documentstore.testutils.exampleRepoWithStringId import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows @@ -91,22 +96,22 @@ class CrudTest { exampleRepoWithStringId.create(entity) } - val result1 = exampleRepoWithStringId.get(ExampleStringId("test1")) - assertNotNull(result1) - assertEquals(entities[0], result1.item) + val getResult = exampleRepoWithStringId.get(ExampleStringId("test1")) + assertNotNull(getResult) + assertEquals(entities[0], getResult.item) - val result2 = + val listResult = exampleRepoWithStringId.listByIds( listOf( ExampleStringId("test2"), ExampleStringId("test3"), ), ) - assertEquals(2, result2.size) + assertEquals(2, listResult.size) - val resultEntities = result2.map { it.item } - assertContains(resultEntities, entities[1]) - assertContains(resultEntities, entities[2]) + val listEntities = listResult.map { it.item } + assertContains(listEntities, entities[1]) + assertContains(listEntities, entities[2]) assertThrows { exampleRepoWithStringId.create( @@ -117,4 +122,77 @@ class CrudTest { ) } } + + @Test + fun `test entity with generated integer ID`() { + val entities = + listOf( + EntityWithIntegerId( + id = ExampleIntegerId(IntegerEntityId.GENERATED), + text = "test", + ), + EntityWithIntegerId( + id = ExampleIntegerId(IntegerEntityId.GENERATED), + text = "test", + ), + EntityWithIntegerId( + id = ExampleIntegerId(IntegerEntityId.GENERATED), + text = "test", + ), + ) + .map { entity -> exampleRepoWithGeneratedIntegerId.create(entity) } + + // After calling RepositoryJdbi.create, the IDs should now have been set by the database + val entityIds = entities.map { it.item.id } + entityIds.forEach { id -> assertNotEquals(IntegerEntityId.GENERATED, id.value) } + assertEquals(entityIds, listOf(ExampleIntegerId(1), ExampleIntegerId(2), ExampleIntegerId(3))) + + val getResult = exampleRepoWithGeneratedIntegerId.get(entities[0].item.id) + assertNotNull(getResult) + assertEquals(entities[0].item, getResult.item) + + val listResult = + exampleRepoWithGeneratedIntegerId.listByIds(entities.take(2).map { it.item.id }) + assertEquals(2, listResult.size) + + val listEntities = listResult.map { it.item } + assertContains(listEntities, entities[0].item) + assertContains(listEntities, entities[1].item) + } + + @Test + fun `test entity with manual integer ID`() { + val entities = + listOf( + EntityWithIntegerId(id = ExampleIntegerId(1), text = "test"), + EntityWithIntegerId(id = ExampleIntegerId(2), text = "test"), + EntityWithIntegerId(id = ExampleIntegerId(3), text = "test"), + ) + for (entity in entities) { + exampleRepoWithIntegerId.create(entity) + } + + val result1 = exampleRepoWithIntegerId.get(ExampleIntegerId(1)) + assertNotNull(result1) + assertEquals(entities[0], result1.item) + + val result2 = + exampleRepoWithIntegerId.listByIds( + listOf( + ExampleIntegerId(2), + ExampleIntegerId(3), + ), + ) + assertEquals(2, result2.size) + + val resultEntities = result2.map { it.item } + assertContains(resultEntities, entities[1]) + assertContains(resultEntities, entities[2]) + + assertThrows { + exampleRepoWithIntegerId.create( + EntityWithIntegerId(id = ExampleIntegerId(1), text = "test"), + ) + } + } } diff --git a/src/test/kotlin/no/liflig/documentstore/repository/ListTest.kt b/src/test/kotlin/no/liflig/documentstore/repository/ListTest.kt index 04abcd4..c46cf89 100644 --- a/src/test/kotlin/no/liflig/documentstore/repository/ListTest.kt +++ b/src/test/kotlin/no/liflig/documentstore/repository/ListTest.kt @@ -44,7 +44,8 @@ class ListTest { * We want to test that [listByIds][no.liflig.documentstore.repository.RepositoryJdbi.listByIds] * works with both [UuidEntityId][no.liflig.documentstore.entity.UuidEntityId] and * [StringEntityId][no.liflig.documentstore.entity.StringEntityId], since we want to verify that - * both ID types work with the `registerArrayType` we use in [DocumentStorePlugin]. + * both ID types work with the `registerArrayType` we use in + * [no.liflig.documentstore.DocumentStorePlugin]. */ @Test fun `test listByIds for StringEntityId`() { diff --git a/src/test/kotlin/no/liflig/documentstore/testutils/ExampleEntity.kt b/src/test/kotlin/no/liflig/documentstore/testutils/ExampleEntity.kt index f57ec4b..4c6cf83 100644 --- a/src/test/kotlin/no/liflig/documentstore/testutils/ExampleEntity.kt +++ b/src/test/kotlin/no/liflig/documentstore/testutils/ExampleEntity.kt @@ -6,6 +6,7 @@ import java.util.UUID import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import no.liflig.documentstore.entity.Entity +import no.liflig.documentstore.entity.IntegerEntityId import no.liflig.documentstore.entity.StringEntityId import no.liflig.documentstore.entity.UuidEntityId @@ -30,6 +31,15 @@ data class EntityWithStringId( @Serializable @JvmInline value class ExampleStringId(override val value: String) : StringEntityId +@Serializable +data class EntityWithIntegerId( + override val id: ExampleIntegerId, + val text: String, + val moreText: String? = null, +) : Entity + +@Serializable @JvmInline value class ExampleIntegerId(override val value: Long) : IntegerEntityId + @Serializable data class MigratedExampleEntity( override val id: ExampleId = ExampleId(), diff --git a/src/test/kotlin/no/liflig/documentstore/testutils/ExampleRepository.kt b/src/test/kotlin/no/liflig/documentstore/testutils/ExampleRepository.kt index 0fd800d..b31b1fa 100644 --- a/src/test/kotlin/no/liflig/documentstore/testutils/ExampleRepository.kt +++ b/src/test/kotlin/no/liflig/documentstore/testutils/ExampleRepository.kt @@ -96,6 +96,21 @@ class ExampleRepositoryWithStringEntityId(jdbi: Jdbi) : serializationAdapter = KotlinSerialization(EntityWithStringId.serializer()), ) +class ExampleRepositoryWithIntegerEntityId(jdbi: Jdbi) : + RepositoryJdbi( + jdbi, + tableName = "example_with_integer_id", + serializationAdapter = KotlinSerialization(EntityWithIntegerId.serializer()), + ) + +class ExampleRepositoryWithGeneratedIntegerEntityId(jdbi: Jdbi) : + RepositoryJdbi( + jdbi, + tableName = "example_with_generated_integer_id", + serializationAdapter = KotlinSerialization(EntityWithIntegerId.serializer()), + idsGeneratedByDatabase = true, + ) + class ExampleRepositoryForMigration(jdbi: Jdbi) : RepositoryJdbi( jdbi, diff --git a/src/test/kotlin/no/liflig/documentstore/testutils/TestDependencies.kt b/src/test/kotlin/no/liflig/documentstore/testutils/TestServices.kt similarity index 89% rename from src/test/kotlin/no/liflig/documentstore/testutils/TestDependencies.kt rename to src/test/kotlin/no/liflig/documentstore/testutils/TestServices.kt index f1d407b..8779f64 100644 --- a/src/test/kotlin/no/liflig/documentstore/testutils/TestDependencies.kt +++ b/src/test/kotlin/no/liflig/documentstore/testutils/TestServices.kt @@ -36,6 +36,14 @@ val exampleRepoWithStringId: ExampleRepositoryWithStringEntityId by lazy { ExampleRepositoryWithStringEntityId(jdbi) } +val exampleRepoWithIntegerId: ExampleRepositoryWithIntegerEntityId by lazy { + ExampleRepositoryWithIntegerEntityId(jdbi) +} + +val exampleRepoWithGeneratedIntegerId: ExampleRepositoryWithGeneratedIntegerEntityId by lazy { + ExampleRepositoryWithGeneratedIntegerEntityId(jdbi) +} + const val MIGRATION_TABLE = "example_for_migration" val exampleRepoPreMigration: ExampleRepository by lazy { diff --git a/src/test/resources/migrations/V001__Example_table.sql b/src/test/resources/migrations/V001__initial_schema.sql similarity index 63% rename from src/test/resources/migrations/V001__Example_table.sql rename to src/test/resources/migrations/V001__initial_schema.sql index 61e5038..b3eb366 100644 --- a/src/test/resources/migrations/V001__Example_table.sql +++ b/src/test/resources/migrations/V001__initial_schema.sql @@ -1,4 +1,4 @@ -CREATE TABLE example +CREATE TABLE "example" ( id uuid NOT NULL PRIMARY KEY, created_at timestamptz NOT NULL, @@ -10,7 +10,7 @@ CREATE UNIQUE INDEX example_unique_field_index ON "example" ((data ->> 'uniqueFi -- Create index on text field to speed up tests CREATE INDEX example_text_index ON "example" ((data ->> 'text')); -CREATE TABLE example_with_count +CREATE TABLE "example_with_count" ( id uuid NOT NULL PRIMARY KEY, created_at timestamptz NOT NULL, @@ -20,7 +20,7 @@ CREATE TABLE example_with_count ); CREATE INDEX example_with_count_text_index ON "example_with_count" ((data ->> 'text')); -CREATE TABLE example_for_list_all +CREATE TABLE "example_for_list_all" ( id uuid NOT NULL PRIMARY KEY, created_at timestamptz NOT NULL, @@ -30,7 +30,7 @@ CREATE TABLE example_for_list_all ); CREATE INDEX example_for_list_all_text_index ON "example_for_list_all" ((data ->> 'text')); -CREATE TABLE example_with_string_id +CREATE TABLE "example_with_string_id" ( id text NOT NULL PRIMARY KEY, created_at timestamptz NOT NULL, @@ -40,7 +40,27 @@ CREATE TABLE example_with_string_id ); CREATE INDEX example_with_string_id_text_index ON "example_with_string_id" ((data ->> 'text')); -CREATE TABLE example_for_migration +CREATE TABLE "example_with_integer_id" +( + id bigint NOT NULL PRIMARY KEY, + created_at timestamptz NOT NULL, + modified_at timestamptz NOT NULL, + version bigint NOT NULL, + data jsonb NOT NULL +); +CREATE INDEX example_with_integer_id_text_index ON "example_with_integer_id" ((data ->> 'text')); + +CREATE TABLE "example_with_generated_integer_id" +( + id bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + created_at timestamptz NOT NULL, + modified_at timestamptz NOT NULL, + version bigint NOT NULL, + data jsonb NOT NULL +); +CREATE INDEX example_with_generated_integer_id_text_index ON "example_with_generated_integer_id" ((data ->> 'text')); + +CREATE TABLE "example_for_migration" ( id uuid NOT NULL PRIMARY KEY, created_at timestamptz NOT NULL,