Auto-indexing service and GraphQL API for AT Protocol Records
quickslice.slices.network/
atproto
gleam
graphql
1import database/queries/pagination
2import database/types.{Record}
3import gleam/option.{None, Some}
4import gleeunit/should
5import test_helpers
6
7/// Test encoding a cursor with no sort fields (just CID)
8pub fn encode_cursor_no_sort_test() {
9 let record =
10 Record(
11 uri: "at://did:plc:test/app.bsky.feed.post/123",
12 cid: "bafytest123",
13 did: "did:plc:test",
14 collection: "app.bsky.feed.post",
15 json: "{\"text\":\"Hello world\",\"createdAt\":\"2025-01-15T12:00:00Z\"}",
16 indexed_at: "2025-01-15 12:00:00",
17 rkey: "123",
18 )
19
20 let result = pagination.generate_cursor_from_record(record, None)
21
22 // Decode the base64 to verify it's just the CID
23 let decoded = pagination.decode_base64(result)
24 should.be_ok(decoded)
25 |> should.equal("bafytest123")
26}
27
28/// Test encoding a cursor with single sort field
29pub fn encode_cursor_single_field_test() {
30 let record =
31 Record(
32 uri: "at://did:plc:test/app.bsky.feed.post/123",
33 cid: "bafytest123",
34 did: "did:plc:test",
35 collection: "app.bsky.feed.post",
36 json: "{\"text\":\"Hello world\",\"createdAt\":\"2025-01-15T12:00:00Z\"}",
37 indexed_at: "2025-01-15 12:00:00",
38 rkey: "123",
39 )
40
41 let sort_by = Some([#("indexed_at", "desc")])
42
43 let result = pagination.generate_cursor_from_record(record, sort_by)
44
45 // Decode the base64 to verify format
46 let decoded = pagination.decode_base64(result)
47 should.be_ok(decoded)
48 |> should.equal("2025-01-15 12:00:00|bafytest123")
49}
50
51/// Test encoding a cursor with JSON field
52pub fn encode_cursor_json_field_test() {
53 let record =
54 Record(
55 uri: "at://did:plc:test/app.bsky.feed.post/123",
56 cid: "bafytest123",
57 did: "did:plc:test",
58 collection: "app.bsky.feed.post",
59 json: "{\"text\":\"Hello world\",\"createdAt\":\"2025-01-15T12:00:00Z\"}",
60 indexed_at: "2025-01-15 12:00:00",
61 rkey: "123",
62 )
63
64 let sort_by = Some([#("text", "desc")])
65
66 let result = pagination.generate_cursor_from_record(record, sort_by)
67
68 let decoded = pagination.decode_base64(result)
69 should.be_ok(decoded)
70 |> should.equal("Hello world|bafytest123")
71}
72
73/// Test encoding a cursor with nested JSON field
74pub fn encode_cursor_nested_json_field_test() {
75 let record =
76 Record(
77 uri: "at://did:plc:test/app.bsky.feed.post/123",
78 cid: "bafytest123",
79 did: "did:plc:test",
80 collection: "app.bsky.feed.post",
81 json: "{\"author\":{\"name\":\"Alice\"},\"createdAt\":\"2025-01-15T12:00:00Z\"}",
82 indexed_at: "2025-01-15 12:00:00",
83 rkey: "123",
84 )
85
86 let sort_by = Some([#("author.name", "asc")])
87
88 let result = pagination.generate_cursor_from_record(record, sort_by)
89
90 let decoded = pagination.decode_base64(result)
91 should.be_ok(decoded)
92 |> should.equal("Alice|bafytest123")
93}
94
95/// Test encoding a cursor with multiple sort fields
96pub fn encode_cursor_multi_field_test() {
97 let record =
98 Record(
99 uri: "at://did:plc:test/app.bsky.feed.post/123",
100 cid: "bafytest123",
101 did: "did:plc:test",
102 collection: "app.bsky.feed.post",
103 json: "{\"text\":\"Hello\",\"createdAt\":\"2025-01-15T12:00:00Z\"}",
104 indexed_at: "2025-01-15 12:00:00",
105 rkey: "123",
106 )
107
108 let sort_by = Some([#("text", "desc"), #("createdAt", "desc")])
109
110 let result = pagination.generate_cursor_from_record(record, sort_by)
111
112 let decoded = pagination.decode_base64(result)
113 should.be_ok(decoded)
114 |> should.equal("Hello|2025-01-15T12:00:00Z|bafytest123")
115}
116
117/// Test decoding a valid cursor
118pub fn decode_cursor_valid_test() {
119 let sort_by = Some([#("indexed_at", "desc")])
120
121 // Create a cursor: "2025-01-15 12:00:00|bafytest123"
122 let cursor_str = pagination.encode_base64("2025-01-15 12:00:00|bafytest123")
123
124 let result = pagination.decode_cursor(cursor_str, sort_by)
125
126 should.be_ok(result)
127 |> fn(decoded) {
128 decoded.field_values
129 |> should.equal(["2025-01-15 12:00:00"])
130
131 decoded.cid
132 |> should.equal("bafytest123")
133 }
134}
135
136/// Test decoding a multi-field cursor
137pub fn decode_cursor_multi_field_test() {
138 let sort_by = Some([#("text", "desc"), #("createdAt", "desc")])
139
140 let cursor_str =
141 pagination.encode_base64("Hello|2025-01-15T12:00:00Z|bafytest123")
142
143 let result = pagination.decode_cursor(cursor_str, sort_by)
144
145 should.be_ok(result)
146 |> fn(decoded) {
147 decoded.field_values
148 |> should.equal(["Hello", "2025-01-15T12:00:00Z"])
149
150 decoded.cid
151 |> should.equal("bafytest123")
152 }
153}
154
155/// Test decoding with mismatched field count fails
156pub fn decode_cursor_mismatch_test() {
157 let sort_by = Some([#("text", "desc")])
158
159 // Cursor has 2 fields but sort_by only has 1
160 let cursor_str =
161 pagination.encode_base64("Hello|2025-01-15T12:00:00Z|bafytest123")
162
163 let result = pagination.decode_cursor(cursor_str, sort_by)
164
165 should.be_error(result)
166}
167
168/// Test decoding invalid base64 fails
169pub fn decode_cursor_invalid_base64_test() {
170 let sort_by = Some([#("text", "desc")])
171
172 let result = pagination.decode_cursor("not-valid-base64!!!", sort_by)
173
174 should.be_error(result)
175}
176
177/// Test extracting table column values
178pub fn extract_field_value_table_column_test() {
179 let record =
180 Record(
181 uri: "at://did:plc:test/app.bsky.feed.post/123",
182 cid: "bafytest123",
183 did: "did:plc:test",
184 collection: "app.bsky.feed.post",
185 json: "{}",
186 indexed_at: "2025-01-15 12:00:00",
187 rkey: "123",
188 )
189
190 pagination.extract_field_value(record, "uri")
191 |> should.equal("at://did:plc:test/app.bsky.feed.post/123")
192
193 pagination.extract_field_value(record, "cid")
194 |> should.equal("bafytest123")
195
196 pagination.extract_field_value(record, "did")
197 |> should.equal("did:plc:test")
198
199 pagination.extract_field_value(record, "collection")
200 |> should.equal("app.bsky.feed.post")
201
202 pagination.extract_field_value(record, "indexed_at")
203 |> should.equal("2025-01-15 12:00:00")
204}
205
206/// Test extracting JSON field values
207pub fn extract_field_value_json_test() {
208 let record =
209 Record(
210 uri: "at://did:plc:test/app.bsky.feed.post/123",
211 cid: "bafytest123",
212 did: "did:plc:test",
213 collection: "app.bsky.feed.post",
214 json: "{\"text\":\"Hello world\",\"createdAt\":\"2025-01-15T12:00:00Z\",\"likeCount\":42}",
215 indexed_at: "2025-01-15 12:00:00",
216 rkey: "123",
217 )
218
219 pagination.extract_field_value(record, "text")
220 |> should.equal("Hello world")
221
222 pagination.extract_field_value(record, "createdAt")
223 |> should.equal("2025-01-15T12:00:00Z")
224
225 pagination.extract_field_value(record, "likeCount")
226 |> should.equal("42")
227}
228
229/// Test extracting nested JSON field values
230pub fn extract_field_value_nested_json_test() {
231 let record =
232 Record(
233 uri: "at://did:plc:test/app.bsky.feed.post/123",
234 cid: "bafytest123",
235 did: "did:plc:test",
236 collection: "app.bsky.feed.post",
237 json: "{\"author\":{\"name\":\"Alice\",\"did\":\"did:plc:alice\"}}",
238 indexed_at: "2025-01-15 12:00:00",
239 rkey: "123",
240 )
241
242 pagination.extract_field_value(record, "author.name")
243 |> should.equal("Alice")
244
245 pagination.extract_field_value(record, "author.did")
246 |> should.equal("did:plc:alice")
247}
248
249/// Test extracting missing JSON field returns NULL
250pub fn extract_field_value_missing_test() {
251 let record =
252 Record(
253 uri: "at://did:plc:test/app.bsky.feed.post/123",
254 cid: "bafytest123",
255 did: "did:plc:test",
256 collection: "app.bsky.feed.post",
257 json: "{\"text\":\"Hello\"}",
258 indexed_at: "2025-01-15 12:00:00",
259 rkey: "123",
260 )
261
262 pagination.extract_field_value(record, "nonexistent")
263 |> should.equal("NULL")
264
265 pagination.extract_field_value(record, "author.name")
266 |> should.equal("NULL")
267}
268
269// WHERE Condition Builder Tests
270
271/// Test building WHERE clause for single field DESC
272pub fn build_where_single_field_desc_test() {
273 let assert Ok(exec) = test_helpers.create_test_db()
274 let decoded =
275 pagination.DecodedCursor(
276 field_values: ["2025-01-15 12:00:00"],
277 cid: "bafytest123",
278 )
279
280 let sort_by = Some([#("indexed_at", "desc")])
281
282 let #(sql, params) =
283 pagination.build_cursor_where_clause(exec, decoded, sort_by, False, 1)
284
285 // For DESC: indexed_at < cursor_value OR (indexed_at = cursor_value AND cid < cursor_cid)
286 sql
287 |> should.equal("((indexed_at < ?) OR (indexed_at = ? AND cid < ?))")
288
289 params
290 |> should.equal([
291 "2025-01-15 12:00:00",
292 "2025-01-15 12:00:00",
293 "bafytest123",
294 ])
295}
296
297/// Test building WHERE clause for single field ASC
298pub fn build_where_single_field_asc_test() {
299 let assert Ok(exec) = test_helpers.create_test_db()
300 let decoded =
301 pagination.DecodedCursor(
302 field_values: ["2025-01-15 12:00:00"],
303 cid: "bafytest123",
304 )
305
306 let sort_by = Some([#("indexed_at", "asc")])
307
308 let #(sql, params) =
309 pagination.build_cursor_where_clause(exec, decoded, sort_by, False, 1)
310
311 // For ASC: indexed_at > cursor_value OR (indexed_at = cursor_value AND cid > cursor_cid)
312 sql
313 |> should.equal("((indexed_at > ?) OR (indexed_at = ? AND cid > ?))")
314
315 params
316 |> should.equal([
317 "2025-01-15 12:00:00",
318 "2025-01-15 12:00:00",
319 "bafytest123",
320 ])
321}
322
323/// Test building WHERE clause for JSON field
324pub fn build_where_json_field_test() {
325 let assert Ok(exec) = test_helpers.create_test_db()
326 let decoded =
327 pagination.DecodedCursor(field_values: ["Hello world"], cid: "bafytest123")
328
329 let sort_by = Some([#("text", "desc")])
330
331 let #(sql, params) =
332 pagination.build_cursor_where_clause(exec, decoded, sort_by, False, 1)
333
334 // JSON fields use json_extract
335 sql
336 |> should.equal(
337 "((json_extract(json, '$.text') < ?) OR (json_extract(json, '$.text') = ? AND cid < ?))",
338 )
339
340 params
341 |> should.equal(["Hello world", "Hello world", "bafytest123"])
342}
343
344/// Test building WHERE clause for nested JSON field
345pub fn build_where_nested_json_field_test() {
346 let assert Ok(exec) = test_helpers.create_test_db()
347 let decoded =
348 pagination.DecodedCursor(field_values: ["Alice"], cid: "bafytest123")
349
350 let sort_by = Some([#("author.name", "asc")])
351
352 let #(sql, params) =
353 pagination.build_cursor_where_clause(exec, decoded, sort_by, False, 1)
354
355 // Nested JSON fields use $.path.to.field
356 sql
357 |> should.equal(
358 "((json_extract(json, '$.author.name') > ?) OR (json_extract(json, '$.author.name') = ? AND cid > ?))",
359 )
360
361 params
362 |> should.equal(["Alice", "Alice", "bafytest123"])
363}
364
365/// Test building WHERE clause for multiple fields
366pub fn build_where_multi_field_test() {
367 let assert Ok(exec) = test_helpers.create_test_db()
368 let decoded =
369 pagination.DecodedCursor(
370 field_values: ["Hello", "2025-01-15T12:00:00Z"],
371 cid: "bafytest123",
372 )
373
374 let sort_by = Some([#("text", "desc"), #("createdAt", "desc")])
375
376 let #(sql, params) =
377 pagination.build_cursor_where_clause(exec, decoded, sort_by, False, 1)
378
379 // Multi-field: progressive equality checks
380 // (text < ?) OR (text = ? AND createdAt < ?) OR (text = ? AND createdAt = ? AND cid < ?)
381 sql
382 |> should.equal(
383 "((json_extract(json, '$.text') < ?) OR (json_extract(json, '$.text') = ? AND json_extract(json, '$.createdAt') < ?) OR (json_extract(json, '$.text') = ? AND json_extract(json, '$.createdAt') = ? AND cid < ?))",
384 )
385
386 params
387 |> should.equal([
388 "Hello",
389 "Hello",
390 "2025-01-15T12:00:00Z",
391 "Hello",
392 "2025-01-15T12:00:00Z",
393 "bafytest123",
394 ])
395}
396
397/// Test building WHERE clause for backward pagination (before)
398pub fn build_where_backward_test() {
399 let assert Ok(exec) = test_helpers.create_test_db()
400 let decoded =
401 pagination.DecodedCursor(
402 field_values: ["2025-01-15 12:00:00"],
403 cid: "bafytest123",
404 )
405
406 let sort_by = Some([#("indexed_at", "desc")])
407
408 // is_before = True reverses the comparison operators
409 let #(sql, params) =
410 pagination.build_cursor_where_clause(exec, decoded, sort_by, True, 1)
411
412 // For before with DESC: indexed_at > cursor_value OR (indexed_at = cursor_value AND cid > cursor_cid)
413 sql
414 |> should.equal("((indexed_at > ?) OR (indexed_at = ? AND cid > ?))")
415
416 params
417 |> should.equal([
418 "2025-01-15 12:00:00",
419 "2025-01-15 12:00:00",
420 "bafytest123",
421 ])
422}