From 7f446ab96a0dd051a56b37884714532dc13ec368 Mon Sep 17 00:00:00 2001 From: Rohan Dhruva Date: Tue, 21 Oct 2025 23:00:52 -0700 Subject: [PATCH 1/2] Use deterministic order for type policy keyFields and field policy keyArgs --- .../apollocompilerplugin/internal/CacheSchemaCodeGenerator.kt | 2 +- .../com/apollographql/cache/normalized/api/CacheResolver.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/normalized-cache-apollo-compiler-plugin/src/main/kotlin/com/apollographql/cache/apollocompilerplugin/internal/CacheSchemaCodeGenerator.kt b/normalized-cache-apollo-compiler-plugin/src/main/kotlin/com/apollographql/cache/apollocompilerplugin/internal/CacheSchemaCodeGenerator.kt index 1909b91b..0cae14c7 100644 --- a/normalized-cache-apollo-compiler-plugin/src/main/kotlin/com/apollographql/cache/apollocompilerplugin/internal/CacheSchemaCodeGenerator.kt +++ b/normalized-cache-apollo-compiler-plugin/src/main/kotlin/com/apollographql/cache/apollocompilerplugin/internal/CacheSchemaCodeGenerator.kt @@ -147,7 +147,7 @@ internal class CacheSchemaCodeGenerator( withIndent { addStatement("keyFields = setOf(") withIndent { - typePolicy.keyFields.forEach { keyField -> + typePolicy.keyFields.sorted().forEach { keyField -> addStatement("%S, ", keyField) } } diff --git a/normalized-cache/src/commonMain/kotlin/com/apollographql/cache/normalized/api/CacheResolver.kt b/normalized-cache/src/commonMain/kotlin/com/apollographql/cache/normalized/api/CacheResolver.kt index 1318f253..245d6f3a 100644 --- a/normalized-cache/src/commonMain/kotlin/com/apollographql/cache/normalized/api/CacheResolver.kt +++ b/normalized-cache/src/commonMain/kotlin/com/apollographql/cache/normalized/api/CacheResolver.kt @@ -261,10 +261,10 @@ fun FieldPolicyCacheResolver( ) = object : CacheResolver { override fun resolveField(context: ResolverContext): Any? { val keyArgs = context.field.argumentValues(context.variables) { it.definition.isKey } - val keyArgsValues = keyArgs.values - if (keyArgsValues.isEmpty()) { + if (keyArgs.values.isEmpty()) { return DefaultCacheResolver.resolveField(context) } + val keyArgsValues = keyArgs.entries.sortedBy { it.key }.map { it.value } var type = context.field.type if (type is CompiledNotNullType) { type = type.ofType From cd0be1f2c16d6da29fe256bb3691f431c8b0429a Mon Sep 17 00:00:00 2001 From: Rohan Dhruva Date: Wed, 22 Oct 2025 12:26:18 -0700 Subject: [PATCH 2/2] Add test, use SortedSet in internal data classs --- .../internal/AddKeyFieldsDocumentTransform.kt | 2 +- .../internal/getTypePolicies.kt | 5 ++- .../internal/GetTypePoliciesTest.kt | 43 ++++++++++++++++--- 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/normalized-cache-apollo-compiler-plugin/src/main/kotlin/com/apollographql/cache/apollocompilerplugin/internal/AddKeyFieldsDocumentTransform.kt b/normalized-cache-apollo-compiler-plugin/src/main/kotlin/com/apollographql/cache/apollocompilerplugin/internal/AddKeyFieldsDocumentTransform.kt index 1b576e03..db38473b 100644 --- a/normalized-cache-apollo-compiler-plugin/src/main/kotlin/com/apollographql/cache/apollocompilerplugin/internal/AddKeyFieldsDocumentTransform.kt +++ b/normalized-cache-apollo-compiler-plugin/src/main/kotlin/com/apollographql/cache/apollocompilerplugin/internal/AddKeyFieldsDocumentTransform.kt @@ -109,7 +109,7 @@ internal object AddKeyFieldsExecutableDocumentTransform : ExecutableDocumentTran val parentTypeKeyFields = keyFields[parentType] ?: emptySet() newSelections.filterIsInstance().forEach { // Disallow fields whose alias conflicts with a key field, or is "__typename" - if (parentTypeKeyFields.contains(it.alias) || it.alias == "__typename") { + if (it.alias != null && (parentTypeKeyFields.contains(it.alias) || it.alias == "__typename")) { throw SourceAwareException( error = "Apollo: Field '${it.alias}: ${it.name}' in $parentType conflicts with key fields", sourceLocation = it.sourceLocation diff --git a/normalized-cache-apollo-compiler-plugin/src/main/kotlin/com/apollographql/cache/apollocompilerplugin/internal/getTypePolicies.kt b/normalized-cache-apollo-compiler-plugin/src/main/kotlin/com/apollographql/cache/apollocompilerplugin/internal/getTypePolicies.kt index bf5c2f0f..8ca5c5f5 100644 --- a/normalized-cache-apollo-compiler-plugin/src/main/kotlin/com/apollographql/cache/apollocompilerplugin/internal/getTypePolicies.kt +++ b/normalized-cache-apollo-compiler-plugin/src/main/kotlin/com/apollographql/cache/apollocompilerplugin/internal/getTypePolicies.kt @@ -7,9 +7,10 @@ import com.apollographql.apollo.ast.GQLTypeDefinition import com.apollographql.apollo.ast.Schema import com.apollographql.apollo.ast.Schema.Companion.TYPE_POLICY import com.apollographql.apollo.ast.SourceAwareException +import java.util.SortedSet internal data class TypePolicy( - val keyFields: Set, + val keyFields: SortedSet, val embeddedFields: Set, ) @@ -83,7 +84,7 @@ private fun Schema.validateAndComputeTypePolicy( private fun GQLDirective.toTypePolicy(): TypePolicy { return TypePolicy( - keyFields = extractFields("keyFields"), + keyFields = extractFields("keyFields").toSortedSet(), embeddedFields = extractFields("embeddedFields") ) } diff --git a/normalized-cache-apollo-compiler-plugin/src/test/kotlin/com/apollographql/cache/apollocompilerplugin/internal/GetTypePoliciesTest.kt b/normalized-cache-apollo-compiler-plugin/src/test/kotlin/com/apollographql/cache/apollocompilerplugin/internal/GetTypePoliciesTest.kt index e3efe3a5..8055221d 100644 --- a/normalized-cache-apollo-compiler-plugin/src/test/kotlin/com/apollographql/cache/apollocompilerplugin/internal/GetTypePoliciesTest.kt +++ b/normalized-cache-apollo-compiler-plugin/src/test/kotlin/com/apollographql/cache/apollocompilerplugin/internal/GetTypePoliciesTest.kt @@ -58,12 +58,43 @@ class GetTypePoliciesTest { ).getOrThrow() val expected = mapOf( - "User" to TypePolicy(keyFields = setOf("id"), embeddedFields = emptySet()), - "Animal" to TypePolicy(keyFields = setOf("kingdom", "species"), embeddedFields = emptySet()), - "Lion" to TypePolicy(keyFields = setOf("kingdom", "species"), embeddedFields = emptySet()), - "HasId" to TypePolicy(keyFields = setOf("id"), embeddedFields = emptySet()), - "Circle" to TypePolicy(keyFields = setOf("id"), embeddedFields = emptySet()), - "Square" to TypePolicy(keyFields = setOf("radius"), embeddedFields = emptySet()), + "User" to TypePolicy(keyFields = sortedSetOf("id"), embeddedFields = emptySet()), + "Animal" to TypePolicy(keyFields = sortedSetOf("kingdom", "species"), embeddedFields = emptySet()), + "Lion" to TypePolicy(keyFields = sortedSetOf("kingdom", "species"), embeddedFields = emptySet()), + "HasId" to TypePolicy(keyFields = sortedSetOf("id"), embeddedFields = emptySet()), + "Circle" to TypePolicy(keyFields = sortedSetOf("id"), embeddedFields = emptySet()), + "Square" to TypePolicy(keyFields = sortedSetOf("radius"), embeddedFields = emptySet()), + ) + + assertEquals(expected, schema.getTypePolicies()) + } + + @Test + fun ensureTypePolicyKeyFieldsAreSorted() { + // language=GraphQL + val schema = """ + type Query { + animal: Animal + } + + type Animal @typePolicy(keyFields: "kingdom species genus class domain") { + kingdom: String! + species: String! + genus: String! + domain: String! + class: String! + } + """.trimIndent() + .parseAsGQLDocument().getOrThrow() + .validateAsSchema( + SchemaValidationOptions( + addKotlinLabsDefinitions = true, + foreignSchemas = emptyList() + ) + ).getOrThrow() + + val expected = mapOf( + "Animal" to TypePolicy(keyFields = sortedSetOf("class", "domain", "genus", "kingdom", "species"), embeddedFields = emptySet()), ) assertEquals(expected, schema.getTypePolicies())