2
2
3
3
use diesel:: dsl:: * ;
4
4
use diesel_full_text_search:: * ;
5
+ use indexmap:: IndexMap ;
5
6
6
7
use crate :: controllers:: cargo_prelude:: * ;
7
8
use crate :: controllers:: helpers:: Paginate ;
@@ -12,7 +13,7 @@ use crate::schema::*;
12
13
use crate :: util:: errors:: { bad_request, ChainError } ;
13
14
use crate :: views:: EncodableCrate ;
14
15
15
- use crate :: controllers:: helpers:: pagination:: { Paginated , PaginationOptions } ;
16
+ use crate :: controllers:: helpers:: pagination:: { Page , Paginated , PaginationOptions } ;
16
17
use crate :: models:: krate:: { canon_crate_name, ALL_COLUMNS } ;
17
18
18
19
/// Handles the `GET /crates` route.
@@ -56,7 +57,13 @@ pub fn search(req: &mut dyn RequestExt) -> EndpointResult {
56
57
. select ( selection)
57
58
. into_boxed ( ) ;
58
59
60
+ let mut supports_seek = true ;
61
+
59
62
if let Some ( q_string) = params. get ( "q" ) {
63
+ // Searching with a query string always puts the exact match at the start of the results,
64
+ // so we can't support seek-based pagination with it.
65
+ supports_seek = false ;
66
+
60
67
if !q_string. is_empty ( ) {
61
68
let sort = params. get ( "sort" ) . map ( |s| & * * s) . unwrap_or ( "relevance" ) ;
62
69
@@ -84,6 +91,9 @@ pub fn search(req: &mut dyn RequestExt) -> EndpointResult {
84
91
}
85
92
86
93
if let Some ( cat) = params. get ( "category" ) {
94
+ // Calculating the total number of results with filters is not supported yet.
95
+ supports_seek = false ;
96
+
87
97
query = query. filter (
88
98
crates:: id. eq_any (
89
99
crates_categories:: table
@@ -102,6 +112,9 @@ pub fn search(req: &mut dyn RequestExt) -> EndpointResult {
102
112
use diesel:: sql_types:: Array ;
103
113
sql_function ! ( #[ aggregate] fn array_agg<T >( x: T ) -> Array <T >) ;
104
114
115
+ // Calculating the total number of results with filters is not supported yet.
116
+ supports_seek = false ;
117
+
105
118
let names: Vec < _ > = kws
106
119
. split_whitespace ( )
107
120
. map ( |name| name. to_lowercase ( ) )
@@ -120,6 +133,9 @@ pub fn search(req: &mut dyn RequestExt) -> EndpointResult {
120
133
) ,
121
134
) ;
122
135
} else if let Some ( kw) = params. get ( "keyword" ) {
136
+ // Calculating the total number of results with filters is not supported yet.
137
+ supports_seek = false ;
138
+
123
139
query = query. filter (
124
140
crates:: id. eq_any (
125
141
crates_keywords:: table
@@ -129,6 +145,9 @@ pub fn search(req: &mut dyn RequestExt) -> EndpointResult {
129
145
) ,
130
146
) ;
131
147
} else if let Some ( letter) = params. get ( "letter" ) {
148
+ // Calculating the total number of results with filters is not supported yet.
149
+ supports_seek = false ;
150
+
132
151
let pattern = format ! (
133
152
"{}%" ,
134
153
letter
@@ -140,6 +159,9 @@ pub fn search(req: &mut dyn RequestExt) -> EndpointResult {
140
159
) ;
141
160
query = query. filter ( canon_crate_name ( crates:: name) . like ( pattern) ) ;
142
161
} else if let Some ( user_id) = params. get ( "user_id" ) . and_then ( |s| s. parse :: < i32 > ( ) . ok ( ) ) {
162
+ // Calculating the total number of results with filters is not supported yet.
163
+ supports_seek = false ;
164
+
143
165
query = query. filter (
144
166
crates:: id. eq_any (
145
167
CrateOwner :: by_owner_kind ( OwnerKind :: User )
@@ -148,6 +170,9 @@ pub fn search(req: &mut dyn RequestExt) -> EndpointResult {
148
170
) ,
149
171
) ;
150
172
} else if let Some ( team_id) = params. get ( "team_id" ) . and_then ( |s| s. parse :: < i32 > ( ) . ok ( ) ) {
173
+ // Calculating the total number of results with filters is not supported yet.
174
+ supports_seek = false ;
175
+
151
176
query = query. filter (
152
177
crates:: id. eq_any (
153
178
CrateOwner :: by_owner_kind ( OwnerKind :: Team )
@@ -156,6 +181,9 @@ pub fn search(req: &mut dyn RequestExt) -> EndpointResult {
156
181
) ,
157
182
) ;
158
183
} else if params. get ( "following" ) . is_some ( ) {
184
+ // Calculating the total number of results with filters is not supported yet.
185
+ supports_seek = false ;
186
+
159
187
let user_id = req. authenticate ( ) ?. user_id ( ) ;
160
188
query = query. filter (
161
189
crates:: id. eq_any (
@@ -165,6 +193,9 @@ pub fn search(req: &mut dyn RequestExt) -> EndpointResult {
165
193
) ,
166
194
) ;
167
195
} else if params. get ( "ids[]" ) . is_some ( ) {
196
+ // Calculating the total number of results with filters is not supported yet.
197
+ supports_seek = false ;
198
+
168
199
let query_bytes = req. query_string ( ) . unwrap_or ( "" ) . as_bytes ( ) ;
169
200
let ids: Vec < _ > = url:: form_urlencoded:: parse ( query_bytes)
170
201
. filter ( |( key, _) | key == "ids[]" )
@@ -175,6 +206,9 @@ pub fn search(req: &mut dyn RequestExt) -> EndpointResult {
175
206
}
176
207
177
208
if !include_yanked {
209
+ // Calculating the total number of results with filters is not supported yet.
210
+ supports_seek = false ;
211
+
178
212
query = query. filter ( exists (
179
213
versions:: table
180
214
. filter ( versions:: crate_id. eq ( crates:: id) )
@@ -183,28 +217,89 @@ pub fn search(req: &mut dyn RequestExt) -> EndpointResult {
183
217
}
184
218
185
219
if sort == Some ( "downloads" ) {
220
+ // Custom sorting is not supported yet with seek.
221
+ supports_seek = false ;
222
+
186
223
query = query. then_order_by ( crates:: downloads. desc ( ) )
187
224
} else if sort == Some ( "recent-downloads" ) {
225
+ // Custom sorting is not supported yet with seek.
226
+ supports_seek = false ;
227
+
188
228
query = query. then_order_by ( recent_crate_downloads:: downloads. desc ( ) . nulls_last ( ) )
189
229
} else if sort == Some ( "recent-updates" ) {
230
+ // Custom sorting is not supported yet with seek.
231
+ supports_seek = false ;
232
+
190
233
query = query. order ( crates:: updated_at. desc ( ) ) ;
191
234
} else if sort == Some ( "new" ) {
235
+ // Custom sorting is not supported yet with seek.
236
+ supports_seek = false ;
237
+
192
238
query = query. order ( crates:: created_at. desc ( ) ) ;
193
239
} else {
194
240
query = query. then_order_by ( crates:: name. asc ( ) )
195
241
}
196
242
197
- let query = query. pages_pagination (
198
- PaginationOptions :: builder ( )
199
- . limit_page_numbers ( req. app ( ) . clone ( ) )
200
- . gather ( req) ?,
201
- ) ;
243
+ let pagination: PaginationOptions = PaginationOptions :: builder ( )
244
+ . limit_page_numbers ( req. app ( ) . clone ( ) )
245
+ . enable_seek ( supports_seek)
246
+ . gather ( req) ?;
202
247
let conn = req. db_read_only ( ) ?;
203
- let data: Paginated < ( Crate , bool , Option < i64 > ) > = query. load ( & * conn) ?;
204
- let total = data. total ( ) ;
205
248
206
- let next_page = data. next_page_params ( ) . map ( |p| req. query_with_params ( p) ) ;
207
- let prev_page = data. prev_page_params ( ) . map ( |p| req. query_with_params ( p) ) ;
249
+ let ( explicit_page, seek) = match pagination. page . clone ( ) {
250
+ Page :: Numeric ( _) => ( true , None ) ,
251
+ Page :: Seek ( s) => ( false , Some ( s. decode :: < i32 > ( ) ?) ) ,
252
+ Page :: Unspecified => ( false , None ) ,
253
+ } ;
254
+
255
+ // To avoid breaking existing users, seek-based pagination is only used if an explicit page has
256
+ // not been provided. This way clients relying on meta.next_page will use the faster seek-based
257
+ // paginations, while client hardcoding pages handling will use the slower offset-based code.
258
+ let ( total, next_page, prev_page, data, conn) = if supports_seek && !explicit_page {
259
+ // Equivalent of:
260
+ // `WHERE name > (SELECT name FROM crates WHERE id = $1) LIMIT $2`
261
+ query = query. limit ( pagination. per_page as i64 ) ;
262
+ if let Some ( seek) = seek {
263
+ let crate_name: String = crates:: table
264
+ . find ( seek)
265
+ . select ( crates:: name)
266
+ . get_result ( & * conn) ?;
267
+ query = query. filter ( crates:: name. gt ( crate_name) ) ;
268
+ }
269
+
270
+ // This does a full index-only scan over the crates table to gather how many crates were
271
+ // published. Unfortunately on PostgreSQL counting the rows in a table requires scanning
272
+ // the table, and the `total` field is part of the stable registries API.
273
+ //
274
+ // If this becomes a problem in the future the crates count could be denormalized, at least
275
+ // for the filterless happy path.
276
+ let total: i64 = crates:: table. count ( ) . get_result ( & * conn) ?;
277
+
278
+ let results: Vec < ( Crate , bool , Option < i64 > ) > = query. load ( & * conn) ?;
279
+
280
+ let next_page = if let Some ( last) = results. last ( ) {
281
+ let mut params = IndexMap :: new ( ) ;
282
+ params. insert (
283
+ "seek" . into ( ) ,
284
+ crate :: controllers:: helpers:: pagination:: encode_seek ( last. 0 . id ) ?,
285
+ ) ;
286
+ Some ( req. query_with_params ( params) )
287
+ } else {
288
+ None
289
+ } ;
290
+
291
+ ( total, next_page, None , results, conn)
292
+ } else {
293
+ let query = query. pages_pagination ( pagination) ;
294
+ let data: Paginated < ( Crate , bool , Option < i64 > ) > = query. load ( & * conn) ?;
295
+ (
296
+ data. total ( ) ,
297
+ data. next_page_params ( ) . map ( |p| req. query_with_params ( p) ) ,
298
+ data. prev_page_params ( ) . map ( |p| req. query_with_params ( p) ) ,
299
+ data. into_iter ( ) . collect :: < Vec < _ > > ( ) ,
300
+ conn,
301
+ )
302
+ } ;
208
303
209
304
let perfect_matches = data. iter ( ) . map ( |& ( _, b, _) | b) . collect :: < Vec < _ > > ( ) ;
210
305
let recent_downloads = data
0 commit comments