Skip to content

Commit 34276eb

Browse files
authored
Avoid inserting identical records (#371)
* Avoid inserting identical records * Bump API dump
1 parent ad5c514 commit 34276eb

7 files changed

Lines changed: 123 additions & 6 deletions

File tree

normalized-cache-sqlite/src/commonMain/kotlin/com/apollographql/cache/normalized/sql/SqlNormalizedCache.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,11 @@ class SqlNormalizedCache internal constructor(
173173
val record = record.withDates(receivedDate = receivedDate, expirationDate = expirationDate)
174174
val existingRecord = existingRecords[record.key]
175175
if (existingRecord == null) {
176-
recordDatabase.insertOrUpdateRecord(record)
176+
recordDatabase.insertOrUpdateRecord(record, deleteFirst = false)
177177
record.fieldKeys()
178178
} else {
179179
val (mergedRecord, changedKeys) = recordMerger.merge(RecordMergerContext(existing = existingRecord, incoming = record, cacheHeaders = cacheHeaders))
180-
if (mergedRecord.isNotEmpty()) {
180+
if (mergedRecord.isNotEmpty() && mergedRecord != existingRecord) {
181181
recordDatabase.insertOrUpdateRecord(mergedRecord)
182182
}
183183
changedKeys

normalized-cache-sqlite/src/commonMain/kotlin/com/apollographql/cache/normalized/sql/internal/RecordDatabase.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ internal class RecordDatabase(
100100
/**
101101
* Must be called inside a transaction.
102102
*/
103-
suspend fun insertOrUpdateRecord(record: Record) {
104-
recordQueries.deleteRecords(listOf(record.key.key))
103+
suspend fun insertOrUpdateRecord(record: Record, deleteFirst: Boolean = true) {
104+
if (deleteFirst) recordQueries.deleteRecords(listOf(record.key.key))
105105
val recordBytes = RecordSerializer.serialize(record)
106106
val updatedDate = currentTimeMillis()
107107
// Fast path for small records
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.apollographql.cache.normalized.sql
2+
3+
import app.cash.sqldelight.db.QueryResult
4+
import app.cash.sqldelight.db.SqlCursor
5+
import app.cash.sqldelight.db.SqlDriver
6+
import app.cash.sqldelight.db.SqlPreparedStatement
7+
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
8+
import com.apollographql.cache.normalized.api.CacheHeaders
9+
import com.apollographql.cache.normalized.api.CacheKey
10+
import com.apollographql.cache.normalized.api.DefaultRecordMerger
11+
import com.apollographql.cache.normalized.api.Record
12+
import com.apollographql.cache.normalized.sql.internal.RecordDatabase
13+
import com.apollographql.cache.normalized.testing.runTest
14+
import java.util.Properties
15+
import kotlin.test.Test
16+
import kotlin.test.assertEquals
17+
18+
class JvmSqlNormalizedCacheTest {
19+
@Test
20+
fun mergingIdenticalRecordIsANoOp() = runTest {
21+
val driver = SpyDriver(JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY, Properties()))
22+
val cache = SqlNormalizedCache(RecordDatabase(driver, null))
23+
// Initial insert
24+
cache.merge(
25+
record = Record(
26+
key = CacheKey.QUERY_ROOT,
27+
fields = mapOf(
28+
"a" to "0",
29+
),
30+
),
31+
cacheHeaders = CacheHeaders.NONE,
32+
recordMerger = DefaultRecordMerger,
33+
)
34+
var count = driver.count
35+
36+
// Insert the same record
37+
cache.merge(
38+
record = Record(
39+
key = CacheKey.QUERY_ROOT,
40+
fields = mapOf(
41+
"a" to "0",
42+
),
43+
),
44+
cacheHeaders = CacheHeaders.NONE,
45+
recordMerger = DefaultRecordMerger,
46+
)
47+
48+
// Expected 1 select, 0 insert
49+
assertEquals(1, driver.count - count)
50+
count = driver.count
51+
52+
// Insert a different record
53+
cache.merge(
54+
record = Record(
55+
key = CacheKey.QUERY_ROOT,
56+
fields = mapOf(
57+
"a" to "1",
58+
),
59+
),
60+
cacheHeaders = CacheHeaders.NONE,
61+
recordMerger = DefaultRecordMerger,
62+
)
63+
64+
// Expected 1 select, 1 delete, 1 insert
65+
assertEquals(3, driver.count - count)
66+
67+
}
68+
}
69+
70+
private class SpyDriver(private val delegate: SqlDriver) : SqlDriver by delegate {
71+
var count = 0
72+
73+
override fun execute(
74+
identifier: Int?,
75+
sql: String,
76+
parameters: Int,
77+
binders: (SqlPreparedStatement.() -> Unit)?,
78+
): QueryResult<Long> {
79+
count++
80+
return delegate.execute(identifier, sql, parameters, binders)
81+
}
82+
83+
override fun <R> executeQuery(
84+
identifier: Int?,
85+
sql: String,
86+
mapper: (SqlCursor) -> QueryResult<R>,
87+
parameters: Int,
88+
binders: (SqlPreparedStatement.() -> Unit)?,
89+
): QueryResult<R> {
90+
count++
91+
return delegate.executeQuery(identifier, sql, mapper, parameters, binders)
92+
}
93+
}

normalized-cache/api/normalized-cache.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,7 @@ public final class com/apollographql/cache/normalized/api/Record : java/util/Map
696696
public fun containsKey (Ljava/lang/String;)Z
697697
public fun containsValue (Ljava/lang/Object;)Z
698698
public final fun entrySet ()Ljava/util/Set;
699+
public fun equals (Ljava/lang/Object;)Z
699700
public final fun fieldKeys ()Ljava/util/Set;
700701
public final fun get (Ljava/lang/Object;)Ljava/lang/Object;
701702
public fun get (Ljava/lang/String;)Ljava/lang/Object;
@@ -708,6 +709,7 @@ public final class com/apollographql/cache/normalized/api/Record : java/util/Map
708709
public fun getSize ()I
709710
public final fun getSizeInBytes ()I
710711
public fun getValues ()Ljava/util/Collection;
712+
public fun hashCode ()I
711713
public fun isEmpty ()Z
712714
public final fun keySet ()Ljava/util/Set;
713715
public synthetic fun merge (Ljava/lang/Object;Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object;

normalized-cache/api/normalized-cache.klib.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,8 +370,10 @@ final class com.apollographql.cache.normalized.api/Record : kotlin.collections/M
370370

371371
final fun containsKey(kotlin/String): kotlin/Boolean // com.apollographql.cache.normalized.api/Record.containsKey|containsKey(kotlin.String){}[0]
372372
final fun containsValue(kotlin/Any?): kotlin/Boolean // com.apollographql.cache.normalized.api/Record.containsValue|containsValue(kotlin.Any?){}[0]
373+
final fun equals(kotlin/Any?): kotlin/Boolean // com.apollographql.cache.normalized.api/Record.equals|equals(kotlin.Any?){}[0]
373374
final fun fieldKeys(): kotlin.collections/Set<kotlin/String> // com.apollographql.cache.normalized.api/Record.fieldKeys|fieldKeys(){}[0]
374375
final fun get(kotlin/String): kotlin/Any? // com.apollographql.cache.normalized.api/Record.get|get(kotlin.String){}[0]
376+
final fun hashCode(): kotlin/Int // com.apollographql.cache.normalized.api/Record.hashCode|hashCode(){}[0]
375377
final fun isEmpty(): kotlin/Boolean // com.apollographql.cache.normalized.api/Record.isEmpty|isEmpty(){}[0]
376378
final fun mergeWith(com.apollographql.cache.normalized.api/Record): kotlin/Pair<com.apollographql.cache.normalized.api/Record, kotlin.collections/Set<kotlin/String>> // com.apollographql.cache.normalized.api/Record.mergeWith|mergeWith(com.apollographql.cache.normalized.api.Record){}[0]
377379
final fun referencedFields(): kotlin.collections/List<com.apollographql.cache.normalized.api/CacheKey> // com.apollographql.cache.normalized.api/Record.referencedFields|referencedFields(){}[0]

normalized-cache/src/commonMain/kotlin/com/apollographql/cache/normalized/api/Record.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,24 @@ class Record(
5757
return result
5858
}
5959

60+
override fun equals(other: Any?): Boolean {
61+
if (this === other) return true
62+
if (other !is Record) return false
63+
64+
if (key != other.key) return false
65+
if (fields != other.fields) return false
66+
if (metadata != other.metadata) return false
67+
68+
return true
69+
}
70+
71+
override fun hashCode(): Int {
72+
var result = key.hashCode()
73+
result = 31 * result + fields.hashCode()
74+
result = 31 * result + metadata.hashCode()
75+
return result
76+
}
77+
6078
companion object {
6179
internal fun changedKeys(record1: Record, record2: Record): Set<String> {
6280
check(record1.key == record2.key) {

normalized-cache/src/commonMain/kotlin/com/apollographql/cache/normalized/memory/MemoryCache.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,10 @@ class MemoryCache(
130130
record.fieldKeys()
131131
} else {
132132
val (mergedRecord, changedKeys) = recordMerger.merge(RecordMergerContext(existing = existingRecord, incoming = record, cacheHeaders = cacheHeaders))
133-
recordsToInsert.add(mergedRecord)
134-
lruCache[record.key] = mergedRecord
133+
if (mergedRecord != existingRecord) {
134+
recordsToInsert.add(mergedRecord)
135+
lruCache[record.key] = mergedRecord
136+
}
135137
changedKeys
136138
}
137139
}.toSet()

0 commit comments

Comments
 (0)