Auto-indexing service and GraphQL API for AT Protocol Records
at main 403 lines 9.6 kB view raw
1import database/executor.{type Executor, Text} 2import database/queries/aggregates 3import database/queries/where_clause 4import database/types 5import gleam/dict 6import gleam/list 7import gleam/option.{None, Some} 8import gleeunit 9import gleeunit/should 10import test_helpers 11 12pub fn main() { 13 gleeunit.main() 14} 15 16// Helper to create an in-memory test database using existing database functions 17fn setup_test_db() -> Executor { 18 let assert Ok(exec) = test_helpers.create_test_db() 19 20 // Use existing database creation functions 21 let assert Ok(_) = test_helpers.create_record_table(exec) 22 let assert Ok(_) = test_helpers.create_actor_table(exec) 23 24 exec 25} 26 27// Helper to insert test records 28fn insert_test_record( 29 exec: Executor, 30 uri: String, 31 collection: String, 32 json: String, 33 did: String, 34) { 35 let assert Ok(_) = 36 executor.exec( 37 exec, 38 "INSERT INTO record (uri, cid, did, collection, json, indexed_at) 39 VALUES (?, 'cid123', ?, ?, ?, datetime('now'))", 40 [Text(uri), Text(did), Text(collection), Text(json)], 41 ) 42 Nil 43} 44 45pub fn test_simple_group_by_single_field() { 46 let exec = setup_test_db() 47 48 // Insert test data with different statuses 49 insert_test_record( 50 exec, 51 "at://did1/xyz.statusphere.status/1", 52 "xyz.statusphere.status", 53 "{\"status\": \"👍\", \"text\": \"Great!\"}", 54 "did:plc:user1", 55 ) 56 insert_test_record( 57 exec, 58 "at://did2/xyz.statusphere.status/2", 59 "xyz.statusphere.status", 60 "{\"status\": \"👍\", \"text\": \"Awesome!\"}", 61 "did:plc:user2", 62 ) 63 insert_test_record( 64 exec, 65 "at://did3/xyz.statusphere.status/3", 66 "xyz.statusphere.status", 67 "{\"status\": \"👎\", \"text\": \"Not good\"}", 68 "did:plc:user3", 69 ) 70 71 // Aggregate by status field 72 let assert Ok(results) = 73 aggregates.get_aggregated_records( 74 exec, 75 "xyz.statusphere.status", 76 [types.SimpleField("status")], 77 None, 78 True, 79 // order by count desc 80 10, 81 ) 82 83 // Should have 2 groups (👍 and 👎) 84 list.length(results) |> should.equal(2) 85 86 // First group should be 👍 with count 2 87 let assert [first, second] = results 88 first.count |> should.equal(2) 89 second.count |> should.equal(1) 90} 91 92pub fn test_group_by_multiple_fields() { 93 let exec = setup_test_db() 94 95 // Insert test data 96 insert_test_record( 97 exec, 98 "at://did1/xyz.statusphere.status/1", 99 "xyz.statusphere.status", 100 "{\"status\": \"👍\", \"category\": \"work\"}", 101 "did:plc:user1", 102 ) 103 insert_test_record( 104 exec, 105 "at://did2/xyz.statusphere.status/2", 106 "xyz.statusphere.status", 107 "{\"status\": \"👍\", \"category\": \"personal\"}", 108 "did:plc:user2", 109 ) 110 insert_test_record( 111 exec, 112 "at://did3/xyz.statusphere.status/3", 113 "xyz.statusphere.status", 114 "{\"status\": \"👍\", \"category\": \"work\"}", 115 "did:plc:user3", 116 ) 117 118 // Aggregate by status and category 119 let assert Ok(results) = 120 aggregates.get_aggregated_records( 121 exec, 122 "xyz.statusphere.status", 123 [types.SimpleField("status"), types.SimpleField("category")], 124 None, 125 True, 126 10, 127 ) 128 129 // Should have 2 groups 130 list.length(results) |> should.equal(2) 131 132 // Check that we get the expected counts 133 let assert [first, ..] = results 134 first.count |> should.equal(2) 135} 136 137pub fn test_group_by_with_where_filter() { 138 let exec = setup_test_db() 139 140 // Insert test data 141 insert_test_record( 142 exec, 143 "at://did1/xyz.statusphere.status/1", 144 "xyz.statusphere.status", 145 "{\"status\": \"👍\", \"active\": \"true\"}", 146 "did:plc:user1", 147 ) 148 insert_test_record( 149 exec, 150 "at://did2/xyz.statusphere.status/2", 151 "xyz.statusphere.status", 152 "{\"status\": \"👍\", \"active\": \"false\"}", 153 "did:plc:user2", 154 ) 155 insert_test_record( 156 exec, 157 "at://did3/xyz.statusphere.status/3", 158 "xyz.statusphere.status", 159 "{\"status\": \"👎\", \"active\": \"true\"}", 160 "did:plc:user3", 161 ) 162 163 // Create WHERE clause to filter only active records 164 let where_condition = 165 where_clause.WhereCondition( 166 eq: Some(Text("true")), 167 in_values: None, 168 contains: None, 169 gt: None, 170 gte: None, 171 lt: None, 172 lte: None, 173 is_null: None, 174 is_numeric: False, 175 ) 176 177 let where_clause = 178 where_clause.WhereClause( 179 conditions: dict.from_list([#("active", where_condition)]), 180 and: None, 181 or: None, 182 ) 183 184 // Aggregate by status with WHERE filter 185 let assert Ok(results) = 186 aggregates.get_aggregated_records( 187 exec, 188 "xyz.statusphere.status", 189 [types.SimpleField("status")], 190 Some(where_clause), 191 True, 192 10, 193 ) 194 195 // Should have 2 groups, each with count 1 (only active records) 196 list.length(results) |> should.equal(2) 197 list.all(results, fn(r) { r.count == 1 }) |> should.be_true() 198} 199 200pub fn test_group_by_table_column() { 201 let exec = setup_test_db() 202 203 // Insert test data with different DIDs 204 insert_test_record( 205 exec, 206 "at://did1/xyz.statusphere.status/1", 207 "xyz.statusphere.status", 208 "{\"status\": \"👍\"}", 209 "did:plc:user1", 210 ) 211 insert_test_record( 212 exec, 213 "at://did1/xyz.statusphere.status/2", 214 "xyz.statusphere.status", 215 "{\"status\": \"👍\"}", 216 "did:plc:user1", 217 ) 218 insert_test_record( 219 exec, 220 "at://did2/xyz.statusphere.status/3", 221 "xyz.statusphere.status", 222 "{\"status\": \"👍\"}", 223 "did:plc:user2", 224 ) 225 226 // Aggregate by DID (table column) 227 let assert Ok(results) = 228 aggregates.get_aggregated_records( 229 exec, 230 "xyz.statusphere.status", 231 [types.SimpleField("did")], 232 None, 233 True, 234 10, 235 ) 236 237 // Should have 2 groups (2 different DIDs) 238 list.length(results) |> should.equal(2) 239 240 // First group should have count 2 241 let assert [first, ..] = results 242 first.count |> should.equal(2) 243} 244 245pub fn test_order_by_count_ascending() { 246 let exec = setup_test_db() 247 248 // Insert test data 249 insert_test_record( 250 exec, 251 "at://did1/xyz.statusphere.status/1", 252 "xyz.statusphere.status", 253 "{\"status\": \"👍\"}", 254 "did:plc:user1", 255 ) 256 insert_test_record( 257 exec, 258 "at://did2/xyz.statusphere.status/2", 259 "xyz.statusphere.status", 260 "{\"status\": \"👎\"}", 261 "did:plc:user2", 262 ) 263 insert_test_record( 264 exec, 265 "at://did3/xyz.statusphere.status/3", 266 "xyz.statusphere.status", 267 "{\"status\": \"👎\"}", 268 "did:plc:user3", 269 ) 270 271 // Aggregate with ascending order 272 let assert Ok(results) = 273 aggregates.get_aggregated_records( 274 exec, 275 "xyz.statusphere.status", 276 [types.SimpleField("status")], 277 None, 278 False, 279 // order by count asc 280 10, 281 ) 282 283 list.length(results) |> should.equal(2) 284 285 // First result should have count 1 (ascending order) 286 let assert [first, ..] = results 287 first.count |> should.equal(1) 288} 289 290pub fn test_limit() { 291 let exec = setup_test_db() 292 293 // Insert test data with many different statuses 294 insert_test_record( 295 exec, 296 "at://did1/xyz.statusphere.status/1", 297 "xyz.statusphere.status", 298 "{\"status\": \"A\"}", 299 "did:plc:user1", 300 ) 301 insert_test_record( 302 exec, 303 "at://did2/xyz.statusphere.status/2", 304 "xyz.statusphere.status", 305 "{\"status\": \"B\"}", 306 "did:plc:user2", 307 ) 308 insert_test_record( 309 exec, 310 "at://did3/xyz.statusphere.status/3", 311 "xyz.statusphere.status", 312 "{\"status\": \"C\"}", 313 "did:plc:user3", 314 ) 315 insert_test_record( 316 exec, 317 "at://did4/xyz.statusphere.status/4", 318 "xyz.statusphere.status", 319 "{\"status\": \"D\"}", 320 "did:plc:user4", 321 ) 322 323 // Aggregate with limit of 2 324 let assert Ok(results) = 325 aggregates.get_aggregated_records( 326 exec, 327 "xyz.statusphere.status", 328 [types.SimpleField("status")], 329 None, 330 True, 331 2, 332 ) 333 334 // Should only return 2 results due to limit 335 list.length(results) |> should.equal(2) 336} 337 338pub fn test_date_truncation_day() { 339 let exec = setup_test_db() 340 341 // Insert records with indexed_at timestamps on different days 342 let assert Ok(_) = 343 executor.exec( 344 exec, 345 "INSERT INTO record (uri, cid, did, collection, json, indexed_at) 346 VALUES (?, 'cid1', 'did1', 'xyz.statusphere.status', '{}', '2024-01-15 10:30:00')", 347 [Text("at://did1/xyz.statusphere.status/1")], 348 ) 349 350 let assert Ok(_) = 351 executor.exec( 352 exec, 353 "INSERT INTO record (uri, cid, did, collection, json, indexed_at) 354 VALUES (?, 'cid2', 'did2', 'xyz.statusphere.status', '{}', '2024-01-15 15:45:00')", 355 [Text("at://did2/xyz.statusphere.status/2")], 356 ) 357 358 let assert Ok(_) = 359 executor.exec( 360 exec, 361 "INSERT INTO record (uri, cid, did, collection, json, indexed_at) 362 VALUES (?, 'cid3', 'did3', 'xyz.statusphere.status', '{}', '2024-01-16 09:00:00')", 363 [Text("at://did3/xyz.statusphere.status/3")], 364 ) 365 366 // Aggregate by indexed_at truncated to day 367 let assert Ok(results) = 368 aggregates.get_aggregated_records( 369 exec, 370 "xyz.statusphere.status", 371 [types.TruncatedField("indexed_at", types.Day)], 372 None, 373 True, 374 10, 375 ) 376 377 // Should have 2 groups (2 different days) 378 list.length(results) |> should.equal(2) 379 380 // One day should have 2 records 381 let assert [first, ..] = results 382 first.count |> should.equal(2) 383} 384 385pub fn test_empty_result() { 386 let exec = setup_test_db() 387 388 // No records inserted 389 390 // Try to aggregate 391 let assert Ok(results) = 392 aggregates.get_aggregated_records( 393 exec, 394 "xyz.statusphere.status", 395 [types.SimpleField("status")], 396 None, 397 True, 398 10, 399 ) 400 401 // Should return empty list 402 list.length(results) |> should.equal(0) 403}