Various refactorings
authorLanius Trolling <lanius@laniustrolling.dev>
Sat, 27 Apr 2024 14:01:33 +0000 (10:01 -0400)
committerLanius Trolling <lanius@laniustrolling.dev>
Sat, 27 Apr 2024 14:01:33 +0000 (10:01 -0400)
12 files changed:
build.gradle.kts
src/jvmMain/kotlin/info/mechyrdia/auth/WebDav.kt
src/jvmMain/kotlin/info/mechyrdia/data/Comments.kt
src/jvmMain/kotlin/info/mechyrdia/data/Data.kt
src/jvmMain/kotlin/info/mechyrdia/data/DataFiles.kt
src/jvmMain/kotlin/info/mechyrdia/data/Nations.kt
src/jvmMain/kotlin/info/mechyrdia/data/Visits.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ArticleListing.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocess.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessInclude.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocessMath.kt
src/jvmMain/kotlin/info/mechyrdia/robot/RobotUserLimiter.kt

index e56ea3aaa6f2c8d1e102635fe3f6d82c90ef3f3d..4a5b7b38e4d39af6a6a2d758f487293188ccdbaa 100644 (file)
@@ -175,7 +175,6 @@ kotlin {
                                
                                implementation("io.ktor:ktor-client-core:2.3.10")
                                implementation("io.ktor:ktor-client-java:2.3.10")
-                               implementation("io.ktor:ktor-client-auth:2.3.10")
                                implementation("io.ktor:ktor-client-content-negotiation:2.3.10")
                                implementation("io.ktor:ktor-client-logging:2.3.10")
                                
index 4fde137d8e520f59086c34ca9d2e6cdd63392a41..b1701c947e057bb544df30020d1714b9d7590a53 100644 (file)
@@ -27,7 +27,7 @@ data class WebDavToken(
                override val Table = DocumentTable<WebDavToken>()
                
                override suspend fun initialize() {
-                       Table.index(WebDavToken::holder)
+                       Table.index(WebDavToken::holder.ascending)
                        Table.expire(WebDavToken::validUntil)
                }
        }
index 4cd6cb11108fd6d78fe7b34ececbf41ad9b0d2f5..f2d4320fa023cb2a6ec610baaf4a3fc992108468 100644 (file)
@@ -25,8 +25,8 @@ data class Comment(
                override val Table = DocumentTable<Comment>()
                
                override suspend fun initialize() {
-                       Table.index(Comment::submittedBy, Comment::submittedAt)
-                       Table.index(Comment::submittedIn, Comment::submittedAt)
+                       Table.index(Comment::submittedBy.ascending, Comment::submittedAt.descending)
+                       Table.index(Comment::submittedIn.ascending, Comment::submittedAt.descending)
                }
                
                suspend fun getCommentsIn(page: List<String>): Flow<Comment> {
@@ -53,9 +53,9 @@ data class CommentReplyLink(
                override val Table = DocumentTable<CommentReplyLink>()
                
                override suspend fun initialize() {
-                       Table.index(CommentReplyLink::originalPost)
-                       Table.index(CommentReplyLink::replyingPost)
-                       Table.unique(CommentReplyLink::replyingPost, CommentReplyLink::originalPost)
+                       Table.index(CommentReplyLink::originalPost.ascending)
+                       Table.index(CommentReplyLink::replyingPost.ascending)
+                       Table.unique(CommentReplyLink::replyingPost.ascending, CommentReplyLink::originalPost.ascending)
                }
                
                suspend fun updateComment(updatedReply: Id<Comment>, repliesTo: Set<Id<Comment>>, now: Instant) {
index 66afe39f2442d7c3d4bc8c085ca003f1ca14227c..bb6b356d4519516842534722897257fbbc7bb892 100644 (file)
@@ -108,27 +108,60 @@ interface DataDocument<T : DataDocument<T>> {
 
 const val MONGODB_ID_KEY = "_id"
 
+enum class IndexSort {
+       ASCENDING,
+       DESCENDING;
+}
+
+typealias IndexSortProperty<T> = Pair<KProperty1<T, *>, IndexSort>
+
+val <T> KProperty1<T, *>.ascending: IndexSortProperty<T>
+       get() = this to IndexSort.ASCENDING
+
+val <T> KProperty1<T, *>.descending: IndexSortProperty<T>
+       get() = this to IndexSort.DESCENDING
+
 class DocumentTable<T : DataDocument<T>>(private val kClass: KClass<T>) {
        private suspend fun collection() = ConnectionHolder.getDatabase().getCollection(kClass.simpleName!!, kClass.java)
        
-       suspend fun index(vararg properties: KProperty1<T, *>) {
-               collection().createIndex(Indexes.ascending(*properties.map { it.serialName }.toTypedArray()))
+       suspend fun index(vararg properties: IndexSortProperty<T>) {
+               collection().createIndex(Indexes.compoundIndex(properties.map { (prop, sort) ->
+                       when (sort) {
+                               IndexSort.ASCENDING -> Indexes.ascending(prop.serialName)
+                               IndexSort.DESCENDING -> Indexes.descending(prop.serialName)
+                       }
+               }))
        }
        
-       suspend fun unique(vararg properties: KProperty1<T, *>) {
-               collection().createIndex(Indexes.ascending(*properties.map { it.serialName }.toTypedArray()), IndexOptions().unique(true))
+       suspend fun unique(vararg properties: IndexSortProperty<T>) {
+               collection().createIndex(Indexes.compoundIndex(properties.map { (prop, sort) ->
+                       when (sort) {
+                               IndexSort.ASCENDING -> Indexes.ascending(prop.serialName)
+                               IndexSort.DESCENDING -> Indexes.descending(prop.serialName)
+                       }
+               }), IndexOptions().unique(true))
        }
        
        suspend fun expire(property: KProperty1<T, Instant>) {
                collection().createIndex(Indexes.ascending(property.serialName), IndexOptions().expireAfter(0L))
        }
        
-       suspend fun indexIf(condition: Bson, vararg properties: KProperty1<T, *>) {
-               collection().createIndex(Indexes.ascending(*properties.map { it.serialName }.toTypedArray()), IndexOptions().partialFilterExpression(condition))
+       suspend fun indexIf(condition: Bson, vararg properties: IndexSortProperty<T>) {
+               collection().createIndex(Indexes.compoundIndex(properties.map { (prop, sort) ->
+                       when (sort) {
+                               IndexSort.ASCENDING -> Indexes.ascending(prop.serialName)
+                               IndexSort.DESCENDING -> Indexes.descending(prop.serialName)
+                       }
+               }), IndexOptions().partialFilterExpression(condition))
        }
        
-       suspend fun uniqueIf(condition: Bson, vararg properties: KProperty1<T, *>) {
-               collection().createIndex(Indexes.ascending(*properties.map { it.serialName }.toTypedArray()), IndexOptions().unique(true).partialFilterExpression(condition))
+       suspend fun uniqueIf(condition: Bson, vararg properties: IndexSortProperty<T>) {
+               collection().createIndex(Indexes.compoundIndex(properties.map { (prop, sort) ->
+                       when (sort) {
+                               IndexSort.ASCENDING -> Indexes.ascending(prop.serialName)
+                               IndexSort.DESCENDING -> Indexes.descending(prop.serialName)
+                       }
+               }), IndexOptions().unique(true).partialFilterExpression(condition))
        }
        
        suspend fun put(doc: T) {
@@ -201,13 +234,15 @@ class DocumentTable<T : DataDocument<T>>(private val kClass: KClass<T>) {
                return collection().deleteMany(where).deletedCount
        }
        
-       suspend fun <T : Any> aggregate(pipeline: List<Bson>, resultClass: KClass<T>): Flow<T> {
+       suspend fun <R : Any> aggregate(pipeline: List<Bson>, resultClass: KClass<R>): Flow<R> {
                return collection().aggregate(pipeline, resultClass.java)
        }
+       
+       suspend inline fun <reified R : Any> aggregate(pipeline: List<Bson>): Flow<R> {
+               return aggregate(pipeline, R::class)
+       }
 }
 
-suspend inline fun <T : DataDocument<T>, reified R : Any> DocumentTable<T>.aggregate(pipeline: List<Bson>) = aggregate(pipeline, R::class)
-
 suspend inline fun <T : DataDocument<T>> DocumentTable<T>.getOrPut(id: Id<T>, defaultValue: () -> T): T {
        val value = get(id)
        return if (value == null) {
index c6a9e80bf5af48b0b5e2b1d3b8b5768f49729fb7..77f9563eda7f865b9fa2eb714a7bdf424d31a10f 100644 (file)
@@ -351,7 +351,7 @@ private class GridFsStorage(val table: DocumentTable<GridFsEntry>, val bucket: G
        }
        
        override suspend fun prepare() {
-               table.unique(GridFsEntry::path)
+               table.unique(GridFsEntry::path.ascending)
                emptyFileId = getOrCreateEmptyFile()
        }
        
index ff42a35c9087fb6d5f4df86e75c8bf2ca2efd2ae..50e8ba9eead94a5a9cf25fea09d79dc3ed26e692 100644 (file)
@@ -29,7 +29,7 @@ data class NationData(
                override val Table = DocumentTable<NationData>()
                
                override suspend fun initialize() {
-                       Table.index(NationData::name)
+                       Table.index(NationData::name.ascending)
                }
                
                fun unknown(id: Id<NationData>): NationData {
index 2f1fa1024e9514dc5748fd086aa48b1b5a97e321..5b185f59e2c6ef1d706383a916b10772209fd5ef 100644 (file)
@@ -41,9 +41,9 @@ data class PageVisitData(
                override val Table = DocumentTable<PageVisitData>()
                
                override suspend fun initialize() {
-                       Table.index(PageVisitData::path)
-                       Table.unique(PageVisitData::path, PageVisitData::visitor)
-                       Table.index(PageVisitData::lastVisit)
+                       Table.index(PageVisitData::path.ascending)
+                       Table.unique(PageVisitData::path.ascending, PageVisitData::visitor.ascending)
+                       Table.index(PageVisitData::lastVisit.ascending)
                }
                
                suspend fun visit(path: String, visitor: String) {
@@ -61,7 +61,7 @@ data class PageVisitData(
                }
                
                suspend fun totalVisits(path: String): PageVisitTotals {
-                       return Table.aggregate<_, PageVisitTotals>(
+                       return Table.aggregate<PageVisitTotals>(
                                listOf(
                                        Aggregates.match(Filters.eq(PageVisitData::path.serialName, path)),
                                        Aggregates.group(
index 04ed0778b596ae60c9dcc9f41c2ff9491f5ae1be..1b7a65226bab4a3de6ff52fc02f2b006c7906578 100644 (file)
@@ -3,7 +3,6 @@ package info.mechyrdia.lore
 import info.mechyrdia.Configuration
 import info.mechyrdia.data.FileStorage
 import info.mechyrdia.data.StoragePath
-import info.mechyrdia.data.StoredFileType
 import info.mechyrdia.route.Root
 import info.mechyrdia.route.href
 import io.ktor.server.application.*
@@ -14,10 +13,8 @@ import kotlinx.html.UL
 import kotlinx.html.a
 import kotlinx.html.li
 import kotlinx.html.ul
-import java.text.CollationKey
 import java.text.Collator
 import java.util.*
-import kotlin.Comparator
 
 data class ArticleNode(val name: String, val title: String, val subNodes: List<ArticleNode>?)
 
@@ -40,12 +37,8 @@ private val collator: Collator = Collator.getInstance(Locale.US).apply {
        decomposition = Collator.FULL_DECOMPOSITION
 }
 
-private val collationSorter = Comparator<Pair<*, CollationKey>> { (_, a), (_, b) ->
-       a.compareTo(b)
-}
-
-fun <T> List<T>.sortedLexically(selector: (T) -> String) = map { it to collator.getCollationKey(selector(it)) }
-       .sortedWith(collationSorter)
+fun <T> List<T>.sortedLexically(selector: (T) -> String?) = map { it to collator.getCollationKey(selector(it)) }
+       .sortedBy { it.second }
        .map { (it, _) -> it }
 
 private fun List<ArticleNode>.sortedAsArticles() = sortedLexically { it.title }.sortedBy { it.subNodes == null }
@@ -61,7 +54,7 @@ val StoragePath.isViewable: Boolean
 
 context(ApplicationCall)
 fun List<ArticleNode>.renderInto(list: UL, base: List<String> = emptyList(), format: LoreArticleFormat = LoreArticleFormat.HTML) {
-       for (node in this) {
+       for (node in this)
                if (node.isViewable)
                        list.li {
                                val nodePath = base + node.name
@@ -72,7 +65,6 @@ fun List<ArticleNode>.renderInto(list: UL, base: List<String> = emptyList(), for
                                        }
                                }
                        }
-       }
 }
 
 suspend fun StoragePath.toFriendlyPageTitle() = ArticleTitleCache.get(this)
@@ -86,7 +78,7 @@ suspend fun StoragePath.toFriendlyPathTitle(): String {
        val lorePath = elements.drop(1)
        if (lorePath.isEmpty()) return TOC_TITLE
        
-       return lorePath.indices.drop(1).map { index ->
-               StoragePath(lorePath.take(index)).toFriendlyPageTitle()
+       return lorePath.indices.mapSuspend { index ->
+               StoragePath(lorePath.take(index + 1)).toFriendlyPageTitle()
        }.joinToString(separator = " - ")
 }
index a91c8f05737773ac8d6e4e73995321f7f1a62694..88247074a9346ada7f3baa1c409369edcc97322b 100644 (file)
@@ -14,8 +14,6 @@ class PreProcessorContext private constructor(
        val variables: MutableMap<String, ParserTree>,
        val parent: PreProcessorContext? = null,
 ) {
-       constructor(parent: PreProcessorContext? = null, vararg variables: Pair<String, ParserTree>) : this(mutableMapOf(*variables), parent)
-       
        operator fun get(name: String): ParserTree = variables[name] ?: parent?.get(name) ?: formatErrorToParserTree("Unable to resolve variable $name")
        
        operator fun set(name: String, value: ParserTree) {
@@ -40,13 +38,11 @@ class PreProcessorContext private constructor(
        
        operator fun plus(other: Map<String, ParserTree>) = PreProcessorContext(other.toMutableMap(), this)
        
-       fun toMap(): Map<String, ParserTree> = parent?.toMap().orEmpty() + variables
-       
        companion object {
                operator fun invoke(variables: Map<String, ParserTree>, parent: PreProcessorContext? = null) = PreProcessorContext(variables.toMutableMap(), parent)
                
-               const val PAGE_PATH_KEY = "PAGE_PATH"
-               const val INSTANT_NOW_KEY = "INSTANT_NOW"
+               private const val PAGE_PATH_KEY = "PAGE_PATH"
+               private const val INSTANT_NOW_KEY = "INSTANT_NOW"
                
                context(ApplicationCall)
                fun defaults() = defaults(StoragePath(request.path()))
@@ -283,14 +279,14 @@ enum class PreProcessorTags(val type: PreProcessorLexerTag) {
                }
        }),
        DEFAULT(PreProcessorLexerTag { env, param, subNodes ->
-               param.requireParam("var") { varName ->
+               param.requireParam("default") { varName ->
                        if (varName in env.context)
                                env.context[varName]
                        else env.processTree(subNodes)
                }
        }),
        SET_PARAM(PreProcessorLexerTag { env, param, subNodes ->
-               param.requireParam("var") { varName ->
+               param.requireParam("set_param") { varName ->
                        val paramValue = env.context[varName].treeToText()
                        val withParams = subNodes.map { node ->
                                if (node is ParserTreeNode.Tag && node.param == null)
@@ -397,7 +393,8 @@ enum class PreProcessorTags(val type: PreProcessorLexerTag) {
                }
        }),
        MATH(PreProcessorMathOperators),
-       LOGIC(PreProcessorLogicOperator),
+       LOGIC(PreProcessorLogicVariadicOperator),
+       FORMAT(PreProcessorFormatter),
        TEST(PreProcessorInputTest),
        JSON_PARSE(PreProcessorLexerTag { _, param, subNodes ->
                param.forbidParam("json_parse") {
@@ -418,7 +415,7 @@ enum class PreProcessorTags(val type: PreProcessorLexerTag) {
        }),
        FILTER(PreProcessorLexerTag { env, param, subNodes ->
                param.requireParam("filter") { scriptName ->
-                       PreProcessorScriptLoader.runScriptSafe(scriptName, env.processTree(subNodes), env) {
+                       PreProcessorScriptLoader.runScriptSafe(scriptName, env.processTree(subNodes).unparse(), env) {
                                it.renderInBBCode()
                        }
                }
index 055acfd17c6df9b63a9ef3338afdd76756c02a52..20904f68d2fc059567c22987801ddc74c5220848 100644 (file)
@@ -102,8 +102,8 @@ object PreProcessorScriptLoader {
                return runScriptWithBindings(scriptName, mapOf("args" to groovyArgs), env, errorHandler)
        }
        
-       suspend fun runScriptSafe(scriptName: String, input: ParserTree, env: AsyncLexerTagEnvironment<PreProcessorContext, PreProcessorSubject>, errorHandler: (Exception) -> ParserTree): ParserTree {
-               return runScriptWithBindings(scriptName, mapOf("text" to input.unparse()), env, errorHandler)
+       suspend fun runScriptSafe(scriptName: String, input: String, env: AsyncLexerTagEnvironment<PreProcessorContext, PreProcessorSubject>, errorHandler: (Exception) -> ParserTree): ParserTree {
+               return runScriptWithBindings(scriptName, mapOf("text" to input), env, errorHandler)
        }
 }
 
index afc0f3c40e226ccd6c75c328b14ef016b1d063ce..5a7de206f7a12ccb9c23340fcb868ceb650eadef 100644 (file)
@@ -18,6 +18,7 @@ fun Boolean.booleanToTree(): ParserTree = listOf(ParserTreeNode.Tag("val", null,
 object PreProcessorMathOperators : PreProcessorFunctionLibrary("math") {
        override val functions: Map<String, PreProcessorFunction> = mapOf(
                "neg" to PreProcessorMathUnaryOperator(Double::unaryMinus),
+               "inv" to PreProcessorMathUnaryOperator { 1.0 / it },
                "sin" to PreProcessorMathUnaryOperator(::sin),
                "cos" to PreProcessorMathUnaryOperator(::cos),
                "tan" to PreProcessorMathUnaryOperator(::tan),
@@ -65,7 +66,7 @@ fun interface PreProcessorMathUnaryOperator : PreProcessorFunction {
                return input.treeToNumberOrNull(String::toDoubleOrNull)
                        ?.let { calculate(it) }
                        ?.numberToTree()
-                       .formatError("Math operations require numerical inputs, got ${input.unparse()}")
+                       .formatError("Math operations require numerical inputs, got in = ${input.unparse()}")
        }
        
        fun calculate(input: Double): Double
@@ -80,7 +81,7 @@ fun interface PreProcessorMathBinaryOperator : PreProcessorFunction {
                val right = rightValue.treeToNumberOrNull(String::toDoubleOrNull)
                
                if (left == null || right == null)
-                       return formatErrorToParserTree("Math operations require numerical inputs, got ${leftValue.unparse()} and ${rightValue.unparse()}")
+                       return formatErrorToParserTree("Math operations require numerical inputs, got left = ${leftValue.unparse()} and right = ${rightValue.unparse()}")
                
                return calculate(left, right).numberToTree()
        }
@@ -94,7 +95,7 @@ fun interface PreProcessorMathVariadicOperator : PreProcessorFunction {
                val args = argsList.asPreProcessorList().mapNotNull { it.treeToNumberOrNull(String::toDoubleOrNull) }
                
                if (args.isEmpty() && argsList.isNotEmpty())
-                       return formatErrorToParserTree("Math operations require numerical inputs, got ${argsList.unparse()}")
+                       return formatErrorToParserTree("Math operations require numerical inputs, got in = ${argsList.unparse()}")
                
                return calculate(args).numberToTree()
        }
@@ -111,7 +112,7 @@ fun interface PreProcessorMathPredicate : PreProcessorFunction {
                val right = rightValue.treeToNumberOrNull(String::toDoubleOrNull)
                
                if (left == null || right == null)
-                       return formatErrorToParserTree("Math operations require numerical inputs, got ${leftValue.unparse()} and ${rightValue.unparse()}")
+                       return formatErrorToParserTree("Math operations require numerical inputs, got left = ${leftValue.unparse()} and right = ${rightValue.unparse()}")
                
                return calculate(left, right).booleanToTree()
        }
@@ -128,7 +129,7 @@ fun interface PreProcessorLogicBinaryOperator : PreProcessorFunction {
                val right = rightValue.treeToBooleanOrNull()
                
                if (left == null || right == null)
-                       return formatErrorToParserTree("Logical operations require boolean inputs, got ${leftValue.unparse()} and ${rightValue.unparse()}")
+                       return formatErrorToParserTree("Logical operations require boolean inputs, got left = ${leftValue.unparse()} and right = ${rightValue.unparse()}")
                
                return calculate(left, right).booleanToTree()
        }
@@ -136,13 +137,13 @@ fun interface PreProcessorLogicBinaryOperator : PreProcessorFunction {
        fun calculate(left: Boolean, right: Boolean): Boolean
 }
 
-fun interface PreProcessorLogicOperator : PreProcessorFunction {
+fun interface PreProcessorLogicVariadicOperator : PreProcessorFunction {
        override suspend fun execute(env: AsyncLexerTagEnvironment<PreProcessorContext, PreProcessorSubject>): ParserTree {
                val argsList = env.processTree(env.context["in"])
                val args = argsList.asPreProcessorList().mapNotNull { it.treeToBooleanOrNull() }
                
                if (args.isEmpty() && argsList.isNotEmpty())
-                       return formatErrorToParserTree("Logical operations require boolean inputs, got ${argsList.unparse()}")
+                       return formatErrorToParserTree("Logical operations require boolean inputs, got in = ${argsList.unparse()}")
                
                return calculate(args).booleanToTree()
        }
@@ -166,18 +167,18 @@ fun interface PreProcessorLogicOperator : PreProcessorFunction {
                        "nand" to PreProcessorLogicBinaryOperator { left, right -> !(left && right) },
                        "nor" to PreProcessorLogicBinaryOperator { left, right -> !(left || right) },
                        "xnor" to PreProcessorLogicBinaryOperator { left, right -> !(left xor right) },
-                       "implies" to PreProcessorLogicBinaryOperator { left, right -> left || !right },
+                       "implies" to PreProcessorLogicBinaryOperator { left, right -> !left || right },
                        
-                       "all" to PreProcessorLogicOperator { inputs -> inputs.all { it } },
-                       "any" to PreProcessorLogicOperator { inputs -> inputs.any { it } },
-                       "not_all" to PreProcessorLogicOperator { inputs -> inputs.any { !it } },
-                       "none" to PreProcessorLogicOperator { inputs -> inputs.none { it } },
+                       "all" to PreProcessorLogicVariadicOperator { inputs -> inputs.all { it } },
+                       "any" to PreProcessorLogicVariadicOperator { inputs -> inputs.any { it } },
+                       "not_all" to PreProcessorLogicVariadicOperator { inputs -> inputs.any { !it } },
+                       "none" to PreProcessorLogicVariadicOperator { inputs -> inputs.none { it } },
                        "count" to PreProcessorFunction { env ->
                                val argsList = env.processTree(env.context["in"])
                                val args = argsList.asPreProcessorList().mapNotNull { it.treeToBooleanOrNull() }
                                
                                if (args.isEmpty() && argsList.isNotEmpty())
-                                       formatErrorToParserTree("Logical operations require boolean inputs, got ${argsList.unparse()}")
+                                       formatErrorToParserTree("Logical operations require boolean inputs, got in = ${argsList.unparse()}")
                                else
                                        args.count { it }.numberToTree()
                        },
@@ -203,7 +204,7 @@ fun interface PreProcessorFormatter : PreProcessorFilter {
                        "local_instant" to PreProcessorFormatter {
                                it.toLongOrNull()
                                        ?.let { long ->
-                                               listOf(ParserTreeNode.Tag("moment", null, listOf(ParserTreeNode.Text(long.toString()))))
+                                               listOf(ParserTreeNode.Tag("moment", null, long.toString().textToTree()))
                                        }.formatError("ISO Instant values must be formatted as base-10 long values, got $it")
                        },
                )
index a03f41b40b8961137c792df226110f22bd27fcd0..75e3e83ee0bb71df2dca6cb86470bf3cbddac943 100644 (file)
@@ -23,7 +23,7 @@ data class RobotUser(
                override val Table: DocumentTable<RobotUser> = DocumentTable<RobotUser>()
                
                override suspend fun initialize() {
-                       Table.unique()
+                       Table.unique(RobotUser::usedByUser.ascending, RobotUser::usedInMonth.ascending)
                }
                
                private fun currentMonth(): Int {