diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt index 68c0fe845..4795ef309 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt @@ -2,10 +2,7 @@ package naksha.model.objects -import naksha.base.Int64 -import naksha.base.NotNullProperty -import naksha.base.NullableProperty -import naksha.base.StringList +import naksha.base.* import naksha.model.Flags import kotlin.js.JsExport import kotlin.js.JsName @@ -18,22 +15,26 @@ import kotlin.jvm.JvmStatic @JsExport open class NakshaCollection() : NakshaFeature() { - // TODO: Add documentation! + /** + * Create a Naksha collection with settings. + */ @JsName("of") constructor( id: String, partitions: Int = 1, storageClass: String? = null, - autoPurge: Boolean = false, - disableHistory: Boolean = false, - geoIndex: String? = null + geoIndex: String? = null, + storeDeleted: StoreMode = StoreMode.ON, + storeHistory: StoreMode = StoreMode.ON, + storeMeta: StoreMode = StoreMode.ON, ) : this() { this.id = id this.storageClass = storageClass this.partitions = partitions - this.autoPurge = autoPurge - this.disableHistory = disableHistory this.geoIndex = geoIndex ?: DEFAULT_GEO_INDEX + this.storeDeleted = storeDeleted + this.storeHistory = storeHistory + this.storeMeta = storeMeta } override fun defaultFeatureType(): String = FEATURE_TYPE @@ -113,15 +114,22 @@ open class NakshaCollection() : NakshaFeature() { var encodeDict by STRING_NULL /** - * _true_ - disables history of features' modifications. + * If [StoreMode.OFF] there will be no history table in the database for features in this collection, + * which boosts performance in certain operations. + */ + var storeHistory by STORE_HISTORY + + /** + * If [StoreMode.OFF] there will be no table in the database for deleted features from this collection, + * which boosts performance in certain operations. */ - var disableHistory by DISABLE_HISTORY + var storeDeleted by STORE_DELETED /** - * If autoPurge is enabled, deleted features are automatically purged and no shadow state is kept available. - * Note that if [disableHistory] is false, the deleted features will still be around in the history. This mainly effects lib-view. + * If [StoreMode.OFF] there will be no meta table in the database for statistics of features in this collection, + * which boosts performance in certain operations. */ - var autoPurge by AUTO_PURGE + var storeMeta by STORE_META /** * The indices list contains the list of indices to add to the collection. @@ -193,12 +201,21 @@ open class NakshaCollection() : NakshaFeature() { private val DEFAULT_TYPE = NotNullProperty(String::class) { _, _ -> "Feature" } private val DEFAULT_FLAGS = NullableProperty(Flags::class) private val STRING_NULL = NullableProperty(String::class) - private val DISABLE_HISTORY = NotNullProperty(Boolean::class) { _, _ -> false } - private val AUTO_PURGE = NotNullProperty(Boolean::class) { _, _ -> false } private val INDICES = NotNullProperty(StringList::class) private val MAX_AGE = NotNullProperty(Int64::class) { _, _ -> Int64(-1) } private val QUAD_PARTITION_SIZE = NotNullProperty(Int::class) { _, _ -> 10_485_760 } private val ESTIMATED_FEATURE_COUNT = NotNullProperty(Int64::class) { _, _ -> BEFORE_ESTIMATION } private val ESTIMATED_DELETED_FEATURES = NotNullProperty(Int64::class) { _, _ -> BEFORE_ESTIMATION } + private val STORE_HISTORY = NotNullEnum(StoreMode::class) { self, _ -> + // For downward compatibility with Naksha version 2 + val old = self.getRaw("disableHistory") + if (old == true) StoreMode.SUSPEND else StoreMode.ON + } + private val STORE_DELETED = NotNullEnum(StoreMode::class) { self, _ -> + // For downward compatibility with Naksha version 2 + val old = self.getRaw("autoPurge") + if (old == true) StoreMode.SUSPEND else StoreMode.ON + } + private val STORE_META = NotNullEnum(StoreMode::class) { _, _ -> StoreMode.ON } } } \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StoreMode.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StoreMode.kt new file mode 100644 index 000000000..6320e3e0a --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StoreMode.kt @@ -0,0 +1,32 @@ +@file:Suppress("OPT_IN_USAGE") +package naksha.model.objects + +import naksha.base.JsEnum +import kotlin.js.JsExport +import kotlin.jvm.JvmField +import kotlin.reflect.KClass + +/** + * How the data should be stored for certain components of [NakshaCollection]. + * [ON] data should be stored. + * [SUSPEND] newer data should not be collected, older data still available. + * [OFF] all data is wiped and no new data collected. + */ +@JsExport +class StoreMode: JsEnum() { + companion object StoreMode_C { + @JvmField + val ON = defIgnoreCase(StoreMode::class, "on") + + @JvmField + val SUSPEND = defIgnoreCase(StoreMode::class, "suspend") + + @JvmField + val OFF = defIgnoreCase(StoreMode::class, "off") + } + + @Suppress("NON_EXPORTABLE_TYPE") + override fun namespace(): KClass = StoreMode::class + + override fun initClass() {} +} \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/NakshaCollectionProxyTest.kt b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/NakshaCollectionProxyTest.kt index 545cc7d69..858a3bbc0 100644 --- a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/NakshaCollectionProxyTest.kt +++ b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/NakshaCollectionProxyTest.kt @@ -2,9 +2,9 @@ package naksha.model import naksha.base.Int64 import naksha.model.objects.NakshaCollection +import naksha.model.objects.StoreMode import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertTrue class NakshaCollectionProxyTest { @@ -14,8 +14,8 @@ class NakshaCollectionProxyTest { val collection = NakshaCollection( id = "ID", partitions = 3, - autoPurge = true, - disableHistory = true + storeDeleted = StoreMode.SUSPEND, + storeHistory = StoreMode.OFF ) collection.maxAge = Int64(42) @@ -23,7 +23,7 @@ class NakshaCollectionProxyTest { assertEquals("ID", collection.id) assertEquals(3, collection.partitions) assertEquals(42, collection.maxAge.toInt()) - assertTrue(collection.autoPurge) - assertTrue(collection.disableHistory) + assertEquals(StoreMode.SUSPEND, collection.storeDeleted) + assertEquals(StoreMode.OFF, collection.storeHistory) } } \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt index 0ac287908..af4ffdfe6 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt @@ -354,7 +354,6 @@ ${if (addFillFactor) "WITH (fillfactor="+if (table.isVolatile) "65)" else "100)" @JvmField @JsStatic var DEFAULT_INDICES = listOf( - id_txn_uid, gist_geo, geo_grid_id_txn_uid, tags_id_txn_uid, diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/executors/write/CreateCollection.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/executors/write/CreateCollection.kt index 92b853d66..7200ef224 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/executors/write/CreateCollection.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/executors/write/CreateCollection.kt @@ -7,6 +7,7 @@ import naksha.model.* import naksha.model.Naksha.NakshaCompanion.VIRT_COLLECTIONS_QUOTED import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaFeature +import naksha.model.objects.StoreMode import naksha.psql.* import naksha.psql.executors.WriteExt import naksha.psql.executors.write.WriteFeatureUtils.allColumnValues @@ -41,7 +42,11 @@ class CreateCollection( collection.create( connection = session.usePgConnection(), partitions = feature.partitions, - storageClass = PgStorageClass.of(feature.storageClass) + storageClass = PgStorageClass.of(feature.storageClass), + indices = PgIndex.DEFAULT_INDICES, + storeHistory = feature.storeHistory != StoreMode.OFF, + storedDeleted = feature.storeDeleted != StoreMode.OFF, + storeMeta = feature.storeMeta != StoreMode.OFF ) return tuple } diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt new file mode 100644 index 000000000..f539de901 --- /dev/null +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt @@ -0,0 +1,223 @@ +package naksha.psql + +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import naksha.model.Naksha +import naksha.model.objects.NakshaCollection +import naksha.model.objects.NakshaFeature +import naksha.model.objects.StoreMode +import naksha.model.request.ReadFeatures +import naksha.model.request.Write +import naksha.model.request.WriteRequest +import naksha.psql.base.PgTestBase +import kotlin.test.* + +class CollectionTests : PgTestBase(collection = null) { + + @Test + fun shouldDropCollection() { + // Given: collection that will be tested + val collection = NakshaCollection("drop_collection_test") + + // When: creating empty collection + executeWrite( + WriteRequest().add( + Write().createCollection(null, collection) + ) + ) + + // Then: this collection is queryable and empty + val readAllFromCollection = ReadFeatures().apply { collectionIds += collection.id } + val collectionContent = executeRead(readAllFromCollection) + assertEquals(0, collectionContent.features.size) + + // And: Virtual Collections contain the created collection + val selectCollectionFromVirt = ReadFeatures().apply { + collectionIds += Naksha.VIRT_COLLECTIONS + featureIds += collection.id + } + val virtBeforeDelete = executeRead(selectCollectionFromVirt) + assertEquals(1, virtBeforeDelete.features.size) + + // When: Collection gets deleted + executeWrite( + WriteRequest().add( + Write().deleteCollectionById(null, collectionId = collection.id) + ) + ) + + // Then: it is not present in Virtual Collections anymore + val virtAfterDelete = executeRead(selectCollectionFromVirt) + assertEquals(0, virtAfterDelete.features.size) + + // And: reading from this collection fails + assertFails("ERROR: relation \"${collection.id}\" does not exist") { + executeRead(readAllFromCollection) + } + } + + @Test + fun collectionShouldHasAllDbColumns() { + val collection = NakshaCollection("check_db_columns_test") + executeWrite( + WriteRequest().add( + Write().createCollection(null, collection) + ) + ) + val cursor = useConnection().execute( + sql = """ SELECT column_name + FROM information_schema.columns + WHERE table_name = $1 + """.trimIndent(), + args = arrayOf(collection.id) + ) + val columns = mutableListOf() + while (cursor.next()) { + columns.add(cursor["column_name"]) + } + assertEquals(PgColumn.allColumns.size, columns.size) + assertTrue(PgColumn.allColumns.all { column -> columns.contains(column.name) }) + cursor.close() + } + + @Test + fun collectionShouldHasAllDbIndices() { + val collection = NakshaCollection("check_db_indices_test") + executeWrite( + WriteRequest().add( + Write().createCollection(null, collection) + ) + ) + val currentYear = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).year + checkAllDefaultIndicesCreatedForTable(collection.id) + checkAllDefaultIndicesCreatedForTable("${collection.id}\$meta") + checkAllDefaultIndicesCreatedForTable("${collection.id}\$del") + checkAllDefaultIndicesCreatedForTable("${collection.id}\$hst\$y$currentYear") + checkAllDefaultIndicesCreatedForTable("${collection.id}\$hst\$y${currentYear + 1}") + checkAllDefaultIndicesCreatedForTable("${collection.id}\$meta") + } + + private fun checkAllDefaultIndicesCreatedForTable(tableName: String) { + val cursor = useConnection().execute( + sql = """ SELECT indexname + FROM pg_indexes + WHERE tablename = $1; + """.trimIndent(), + args = arrayOf(tableName) + ) + val indices = mutableListOf() + while (cursor.next()) { + indices.add(cursor["indexname"]) + } + assertTrue(PgIndex.DEFAULT_INDICES.size <= indices.size) + assertTrue(PgIndex.DEFAULT_INDICES.all { index -> indices.any { addedIndex -> addedIndex.contains(index)}}) + cursor.close() + } + + @Test + fun collectionShouldHasNoHistoryDBTable() { + val collectionName = "check_no_hst_table_test" + val collection = NakshaCollection( + id = collectionName, + storeHistory = StoreMode.OFF + ) + executeWrite( + WriteRequest().add( + Write().createCollection(null, collection) + ) + ) + val hstTableName = "$collectionName\$hst" + val cursor = useConnection().execute( + sql = """ SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = $1 + ) + """.trimIndent(), + args = arrayOf(hstTableName) + ) + // Check that hst table was not created + assertFalse(cursor.fetch()["exists"]) + cursor.close() + // Check that creating, updating and deleting features still work + val feature = NakshaFeature() + val readFeature = ReadFeatures() + readFeature.collectionIds.add(collectionName) + readFeature.featureIds.add(feature.id) + executeWrite( + WriteRequest().add( + Write().createFeature(null, collectionName,feature) + ) + ) + val insertedFeatureResponse = executeRead(readFeature) + assertEquals(1,insertedFeatureResponse.features.size) + feature.properties["foo"] = "bar" + executeWrite( + WriteRequest().add( + Write().updateFeature(null, collectionName,feature) + ) + ) + val updatedFeatureResponse = executeRead(readFeature) + assertEquals("bar", updatedFeatureResponse.features[0]?.properties!!["foo"]) + executeWrite( + WriteRequest().add( + Write().deleteFeatureById(null, collectionName,feature.id) + ) + ) + val deletedFeatureResponse = executeRead(readFeature) + assertEquals(0, deletedFeatureResponse.features.size) + } + + @Test + fun collectionShouldHasNoDeleteDBTable() { + val collectionName = "check_no_del_table_test" + val collection = NakshaCollection( + id = collectionName, + storeDeleted = StoreMode.OFF + ) + executeWrite( + WriteRequest().add( + Write().createCollection(null, collection) + ) + ) + val delTableName = "$collectionName\$del" + val cursor = useConnection().execute( + sql = """ SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = $1 + ) + """.trimIndent(), + args = arrayOf(delTableName) + ) + // Check that del table was not created + assertFalse(cursor.fetch()["exists"]) + cursor.close() + // Check that creating, updating and deleting features still work + val feature = NakshaFeature() + val readFeature = ReadFeatures() + readFeature.collectionIds.add(collectionName) + readFeature.featureIds.add(feature.id) + executeWrite( + WriteRequest().add( + Write().createFeature(null, collectionName,feature) + ) + ) + val insertedFeatureResponse = executeRead(readFeature) + assertEquals(1,insertedFeatureResponse.features.size) + feature.properties["foo"] = "bar" + executeWrite( + WriteRequest().add( + Write().updateFeature(null, collectionName,feature) + ) + ) + val updatedFeatureResponse = executeRead(readFeature) + assertEquals("bar", updatedFeatureResponse.features[0]?.properties!!["foo"]) + executeWrite( + WriteRequest().add( + Write().deleteFeatureById(null, collectionName,feature.id) + ) + ) + val deletedFeatureResponse = executeRead(readFeature) + assertEquals(0, deletedFeatureResponse.features.size) + } +} \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DropCollectionTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DropCollectionTest.kt deleted file mode 100644 index 750e46197..000000000 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DropCollectionTest.kt +++ /dev/null @@ -1,56 +0,0 @@ -package naksha.psql - -import naksha.model.Naksha -import naksha.model.objects.NakshaCollection -import naksha.model.request.ReadFeatures -import naksha.model.request.Write -import naksha.model.request.WriteRequest -import naksha.psql.base.PgTestBase -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFails - -class DropCollectionTest : PgTestBase(collection = null) { - - @Test - fun shouldDropCollection() { - // Given: collection that will be tested - val collection = NakshaCollection("drop_collection_test") - - // When: creating empty collection - executeWrite( - WriteRequest().add( - Write().createCollection(null, collection) - ) - ) - - // Then: this collection is queryable and empty - val readAllFromCollection = ReadFeatures().apply { collectionIds += collection.id } - val collectionContent = executeRead(readAllFromCollection) - assertEquals(0, collectionContent.features.size) - - // And: Virtual Collections contain the created collection - val selectCollectionFromVirt = ReadFeatures().apply { - collectionIds += Naksha.VIRT_COLLECTIONS - featureIds += collection.id - } - val virtBeforeDelete = executeRead(selectCollectionFromVirt) - assertEquals(1, virtBeforeDelete.features.size) - - // When: Collection gets deleted - executeWrite( - WriteRequest().add( - Write().deleteCollectionById(null, collectionId = collection.id) - ) - ) - - // Then: it is not present in Virtual Collections anymore - val virtAfterDelete = executeRead(selectCollectionFromVirt) - assertEquals(0, virtAfterDelete.features.size) - - // And: reading from this collection fails - assertFails("ERROR: relation \"${collection.id}\" does not exist") { - executeRead(readAllFromCollection) - } - } -} \ No newline at end of file