╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ event.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1016 try std.testing.expectEqualStrings("Kept by the edit", kept.event.title); 1017 } 1018 } 1019 1020 // the commit that last set `field_name` on the issue `id` names 1021 fn readOid( 1022 comptime DB: type, 1023 comptime hash_kind: hash.HashKind, 1024 haxy_moment: DB.HashMap(.read_only), 1025 id: *const [evt.event_id_size]u8, 1026 comptime field_name: []const u8, 1027 ) ![hash.byteLen(hash_kind)]u8 { 1028 const id_to_field_to_oid_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, evt.Issue.id_to_field_to_oid_key)) orelse return error.NotFound; 1029 const id_to_field_to_oid = try DB.HashMap(.read_only).init(id_to_field_to_oid_cursor); 1030 const field_oids_cursor = try id_to_field_to_oid.getCursor(hash.hashInt(hash_kind, id)) orelse return error.NotFound; 1031 const field_oids = try DB.SortedMap(.read_only).init(field_oids_cursor); 1032 const cursor = try field_oids.getCursor(field_name) orelse return error.NotFound; 1033 var oid: [hash.byteLen(hash_kind)]u8 = undefined; 1034 _ = try cursor.readBytes(&oid); 1035 return oid; 1036 } 1037 1038 // the issue `id` names, read out of `event-id->issue` 1039 fn readIssue( 1040 comptime DB: type, 1041 comptime hash_kind: hash.HashKind, 1042 haxy_moment: DB.HashMap(.read_only), 1043 arena: *std.heap.ArenaAllocator, 1044 id: *const [evt.event_id_size]u8, 1045 ) !evt.Issue.Record { 1046 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "event-id->issue")) orelse return error.NotFound; 1047 const event_id_to_issue = try DB.HashMap(.read_only).init(event_id_to_issue_cursor); 1048 const issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(hash_kind, id)) orelse return error.NotFound; 1049 const issue_map = try DB.HashMap(.read_only).init(issue_cursor); 1050 return try evt.read(evt.Issue.Record, DB, hash_kind, arena, issue_map); 1051 } 1052 1053 test "user and repo" { 1054 const io = std.testing.io; 1055 const allocator = std.testing.allocator; 1056 const temp_dir_name = "temp-event-user-and-repo"; 1057 1058 // create the temp dir 1059 const cwd = std.Io.Dir.cwd(); 1060 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 1061 if (temp_dir_or_err) |*temp_dir| { 1062 temp_dir.close(io); 1063 try cwd.deleteTree(io, temp_dir_name); 1064 } else |_| {} 1065 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 1066 defer cwd.deleteTree(io, temp_dir_name) catch {}; 1067 defer temp_dir.close(io); 1068 1069 const cwd_path = try std.process.currentPathAlloc(io, allocator); 1070 defer allocator.free(cwd_path); 1071 1072 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 1073 defer allocator.free(work_path); 1074 1075 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 1076 const Repo = rp.Repo(.xit, repo_opts); 1077 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 1078 defer repo.deinit(io, allocator); 1079 1080 var arena = std.heap.ArenaAllocator.init(allocator); 1081 defer arena.deinit(); 1082 1083 // 1084 // define test events 1085 // 1086 1087 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 1088 const user_event_id = evt.EventWithId.randomId(prng.random()); 1089 const repo_event_id = evt.EventWithId.randomId(prng.random()); 1090 1091 var first_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 1092 const first_password_hash = try evt.User.hashPassword("correct horse battery staple", &first_password_hash_buf, io); 1093 1094 var second_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 1095 const second_password_hash = try evt.User.hashPassword("Tr0ub4dor&3", &second_password_hash_buf, io); 1096 1097 const events_to_consume = [_]evt.EventWithId{ 1098 .{ 1099 .id = std.fmt.bytesToHex(user_event_id, .lower), 1100 .author_email = author_email, 1101 .event = .{ 1102 .user = .{ 1103 .name = "alice", 1104 .display_name = "Alice Example", 1105 .email = "alice@example.test", 1106 .password_hash = first_password_hash, 1107 }, 1108 }, 1109 }, 1110 // this event edits the previous one because it has the same id 1111 .{ 1112 .id = std.fmt.bytesToHex(user_event_id, .lower), 1113 .author_email = author_email, 1114 .event = .{ 1115 .user = .{ 1116 .name = "alice", 1117 .display_name = "Alice Example", 1118 .email = "alice@example.test", 1119 .password_hash = second_password_hash, 1120 }, 1121 }, 1122 }, 1123 .{ 1124 .id = std.fmt.bytesToHex(repo_event_id, .lower), 1125 .author_email = author_email, 1126 .event = .{ 1127 .repo = .{ 1128 .user_id = &user_event_id, 1129 .name = "ziglings", 1130 .description = "Learn the Zig programming language by fixing tiny broken programs", 1131 }, 1132 }, 1133 }, 1134 }; 1135 1136 // commit and consume the seed events 1137 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume); 1138 1139 { 1140 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 1141 1142 // get the map of users 1143 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 1144 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 1145 1146 // get the user out of the map that was edited 1147 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 1148 const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); 1149 const user_event = try evt.read(evt.User.Record, Repo.DB, repo_opts.hash, &arena, user_map); 1150 1151 // the password was correctly edited 1152 try std.testing.expectEqualStrings(events_to_consume[1].event.user.?.password_hash, user_event.event.password_hash); 1153 1154 // get the map of repos 1155 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 1156 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 1157 1158 // get the repo out of the map 1159 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 1160 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 1161 const repo_event = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); 1162 1163 try std.testing.expectEqualSlices(u8, &user_event_id, repo_event.event.user_id); 1164 try std.testing.expectEqualStrings("ziglings", repo_event.event.name); 1165 1166 // get the repos created by the user 1167 const user_id_to_repo_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repo-id-set")) orelse return error.NotFound; 1168 const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); 1169 1170 const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 1171 const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); 1172 1173 try std.testing.expectEqual(1, try user_repos.count()); 1174 1175 // the set is keyed by orderKeyDesc([created-ts][event-id]) 1176 const order_key = evt.orderKeyDesc(repo_event.created_ts, &repo_event_id); 1177 try std.testing.expect(try user_repos.contains(&order_key)); 1178 } 1179 1180 // 1181 // remove the repo 1182 // 1183 1184 const events_to_consume2 = [_]evt.EventWithId{ 1185 .{ 1186 .id = std.fmt.bytesToHex(repo_event_id, .lower), 1187 .author_email = author_email, 1188 .event = .{ .repo = null }, 1189 }, 1190 }; 1191 1192 // commit and consume the removal 1193 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume2); 1194 1195 { 1196 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 1197 1198 // get the map of repos 1199 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 1200 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 1201 1202 try std.testing.expect(null == try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id))); 1203 1204 // get the repos created by the user 1205 const user_id_to_repo_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repo-id-set")) orelse return error.NotFound; 1206 const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); 1207 1208 const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 1209 const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); 1210 1211 // removing the repo emptied the user's set 1212 try std.testing.expectEqual(0, try user_repos.count()); 1213 } 1214 1215 // 1216 // remove the user 1217 // 1218 1219 const events_to_consume3 = [_]evt.EventWithId{ 1220 .{ 1221 .id = std.fmt.bytesToHex(user_event_id, .lower), 1222 .author_email = author_email, 1223 .event = .{ .user = null }, 1224 }, 1225 }; 1226 1227 // commit and consume the removal 1228 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume3); 1229 1230 { 1231 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 1232 1233 // get the map of users 1234 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 1235 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 1236 1237 try std.testing.expect(null == try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id))); 1238 } 1239 } 1240 1241 test "repos and users paginate newest first" { 1242 const io = std.testing.io; 1243 const allocator = std.testing.allocator; 1244 const temp_dir_name = "temp-event-order"; 1245 1246 const cwd = std.Io.Dir.cwd(); 1247 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 1248 if (temp_dir_or_err) |*temp_dir| { 1249 temp_dir.close(io); 1250 try cwd.deleteTree(io, temp_dir_name); 1251 } else |_| {} 1252 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 1253 defer cwd.deleteTree(io, temp_dir_name) catch {}; 1254 defer temp_dir.close(io); 1255 1256 const cwd_path = try std.process.currentPathAlloc(io, allocator); 1257 defer allocator.free(cwd_path); 1258 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 1259 defer allocator.free(work_path); 1260 1261 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 1262 const Repo = rp.Repo(.xit, repo_opts); 1263 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 1264 defer repo.deinit(io, allocator); 1265 1266 var arena = std.heap.ArenaAllocator.init(allocator); 1267 defer arena.deinit(); 1268 1269 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 1270 const user_id = evt.EventWithId.randomId(prng.random()); 1271 var repo_ids: [4][evt.event_id_size]u8 = undefined; 1272 for (&repo_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 1273 1274 var pw_buf: [evt.User.password_hash_max_len]u8 = undefined; 1275 const pw = try evt.User.hashPassword("pw", &pw_buf, io); 1276 1277 // asserts the repo-id-set holds exactly `expected` ids in that order 1278 const Check = struct { 1279 fn order( 1280 comptime DB: type, 1281 comptime hash_kind: hash.HashKind, 1282 moment: DB.HashMap(.read_only), 1283 expected: []const *const [evt.event_id_size]u8, 1284 ) !void { 1285 const cursor = try moment.getCursor(hash.hashInt(hash_kind, "repo-id-set")) orelse return error.NotFound; 1286 const set = try DB.SortedSet(.read_only).init(cursor); 1287 try std.testing.expectEqual(expected.len, try set.count()); 1288 for (expected, 0..) |id, i| { 1289 const kv = (try set.getIndexKeyValuePair(@intCast(i))) orelse return error.NotFound; 1290 // the key is orderKeyDesc ([timestamp][event-id]); its trailing bytes are the id 1291 var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; 1292 _ = try kv.key_cursor.readBytes(&order_key); 1293 try std.testing.expectEqualSlices(u8, id, order_key[@sizeOf(u64)..]); 1294 } 1295 } 1296 }; 1297 1298 // one user, then four repos, each with its own creation timestamp (user@100, 1299 // repos@101..104) 1300 const events = [_]evt.EventWithId{ 1301 .{ .id = std.fmt.bytesToHex(user_id, .lower), .author_email = author_email, .timestamp = 100, .event = .{ .user = .{ .name = "alice", .display_name = "Alice", .email = "alice@example.test", .password_hash = pw } } }, 1302 .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .author_email = author_email, .timestamp = 101, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo0", .description = "d0" } } }, 1303 .{ .id = std.fmt.bytesToHex(repo_ids[1], .lower), .author_email = author_email, .timestamp = 102, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo1", .description = "d1" } } }, 1304 .{ .id = std.fmt.bytesToHex(repo_ids[2], .lower), .author_email = author_email, .timestamp = 103, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo2", .description = "d2" } } }, 1305 .{ .id = std.fmt.bytesToHex(repo_ids[3], .lower), .author_email = author_email, .timestamp = 104, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo3", .description = "d3" } } }, 1306 }; 1307 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &events); 1308 1309 { 1310 const moment = try evt.currentMoment(repo_opts, &repo); 1311 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[1], &repo_ids[0] }); 1312 1313 // the single user shows up in its own ordered set 1314 const ucur = try moment.getCursor(hash.hashInt(repo_opts.hash, "user-id-set")) orelse return error.NotFound; 1315 const uset = try Repo.DB.SortedSet(.read_only).init(ucur); 1316 try std.testing.expectEqual(1, try uset.count()); 1317 } 1318 1319 // delete repo1 -> dense order (no tombstone) 1320 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ 1321 .{ .id = std.fmt.bytesToHex(repo_ids[1], .lower), .author_email = author_email, .timestamp = 200, .event = .{ .repo = null } }, 1322 }); 1323 { 1324 const moment = try evt.currentMoment(repo_opts, &repo); 1325 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[0] }); 1326 } 1327 1328 // update repo0 at a later timestamp -> keeps its original place 1329 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ 1330 .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .author_email = author_email, .timestamp = 300, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo0", .description = "updated" } } }, 1331 }); 1332 { 1333 const moment = try evt.currentMoment(repo_opts, &repo); 1334 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[0] }); 1335 1336 // the value really was updated 1337 const e2r_cur = try moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 1338 const e2r = try Repo.DB.HashMap(.read_only).init(e2r_cur); 1339 const rc = try e2r.getCursor(hash.hashInt(repo_opts.hash, &repo_ids[0])) orelse return error.NotFound; 1340 const rm = try Repo.DB.HashMap(.read_only).init(rc); 1341 const re = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, rm); 1342 try std.testing.expectEqualStrings("updated", re.event.description); 1343 } 1344 } 1345