-
Notifications
You must be signed in to change notification settings - Fork 498
/
QueryPagingSource.kt
144 lines (136 loc) · 4.94 KB
/
QueryPagingSource.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
/*
* Copyright (C) 2016 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package app.cash.sqldelight.paging3
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import app.cash.sqldelight.Query
import app.cash.sqldelight.Transacter
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlin.properties.Delegates
internal abstract class QueryPagingSource<Key : Any, RowType : Any> :
PagingSource<Key, RowType>(),
Query.Listener {
protected var currentQuery: Query<RowType>? by Delegates.observable(null) { _, old, new ->
old?.removeListener(this)
new?.addListener(this)
}
init {
registerInvalidatedCallback {
currentQuery?.removeListener(this)
currentQuery = null
}
}
final override fun queryResultsChanged() = invalidate()
}
/**
* Create a [PagingSource] that pages through results according to queries generated by
* [queryProvider]. Queries returned by [queryProvider] should expect to do SQL offset/limit
* based paging. For that reason, [countQuery] is required to calculate pages and page offsets.
*
* An example query returned by [queryProvider] could look like:
*
* ```sql
* SELECT value FROM numbers
* LIMIT 10
* OFFSET 100;
* ```
*
* Queries will be executed on [dispatcher].
*/
@Suppress("FunctionName")
fun <RowType : Any> QueryPagingSource(
countQuery: Query<Long>,
transacter: Transacter,
dispatcher: CoroutineDispatcher = Dispatchers.IO,
queryProvider: (limit: Long, offset: Long) -> Query<RowType>,
): PagingSource<Long, RowType> = OffsetQueryPagingSource(
queryProvider,
countQuery,
transacter,
dispatcher,
)
/**
* Create a [PagingSource] that pages through results according to queries generated by
* [queryProvider]. Queries returned by [queryProvider] should expected to do keyset paging.
* For that reason, queries should be arranged by an non-ambigious `ORDER BY` clause. [Key] must
* be a unique clause that rows are ordered by. For performance reasons, an index should be present
* on [Key].
*
* [pageBoundariesProvider] is a callback that produces a query containing [Key] items that specifies
* where each page boundary exists within the full dataset. For example:
*
* The dataset `[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]` ordered ascending with a page size of 2 would produce
* page boundaries `[0, 2, 4, 6, 8]`.
*
* The dataset `[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]` ordered ascending with a page size of 3 would produce
* page boundaries `[0, 3, 6, 9]`.
*
* Callbacks made from [pageBoundariesProvider] may include an `anchor` key which, if present,
* should appear in the resulting query.
* Because page boundaries are computed ahead of time, [PagingConfig.initialLoadSize] should match
* [PagingConfig.pageSize]. Failing to do so will result in unexpected page sizes, as
* [pageBoundariesProvider] is called a single time during the first call to [PagingSource.load]
* on this source.
*
* Generally, it's only feasible to produce page boundaries using SQLite window functions. An example
* query to generate page boundaries like shown above would look like the following.
*
* ```sql
* SELECT value
* FROM (
* SELECT
* value,
* CASE
* WHEN ((row_number() OVER(ORDER BY value ASC) - 1) % :limit) = 0 THEN 1
* WHEN value = :anchor THEN 1
* ELSE 0
* END page_boundary
* FROM numbers
* ORDER BY value ASC
* )
* WHERE page_boundary = 1;
* ```
*
* SQLite window queries became available as of version 3.25.0. For this reason, consuming
* applications will likely need a minSdk of 30 set _or_ bundle a SQLite module separate from the OS
* provided module.
*
* An example query returned by [queryProvider] could look like:
*
* ```sql
* SELECT value FROM numbers
* WHERE value >= :beginInclusive AND (value < :endExclusive OR :endExclusive IS NULL)
* ORDER BY value ASC;
* ```
*
* Queries will be executed on [dispatcher].
*
* This [PagingSource] _does not_ support jumping. If your use case requires jumping, use the
* offset based [QueryPagingSource] function.
*/
@Suppress("FunctionName")
fun <Key : Any, RowType : Any> QueryPagingSource(
transacter: Transacter,
dispatcher: CoroutineDispatcher = Dispatchers.IO,
pageBoundariesProvider: (anchor: Key?, limit: Long) -> Query<Key>,
queryProvider: (beginInclusive: Key, endExclusive: Key?) -> Query<RowType>,
): PagingSource<Key, RowType> = KeyedQueryPagingSource(
queryProvider,
pageBoundariesProvider,
transacter,
dispatcher,
)