╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ event.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
81 repo_events: []RepoEvent(hash_kind), 82 history_count: u64, 83 84 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 85 const moment = try DB.HashMap(.read_write).init(cursor.*); 86 87 // the map with all of haxy's state and materialized views 88 const haxy_cursor = try moment.putCursor(hash.hashInt(hash_kind, "haxy")); 89 const haxy = try DB.HashMap(.read_write).init(haxy_cursor); 90 91 // try reading the last object id that was consumed 92 var last_object_id_maybe: ?[hash.byteLen(hash_kind)]u8 = null; 93 if (try haxy.getCursor(hash.hashInt(hash_kind, "last-object-id"))) |last_object_id_cursor| { 94 var last_object_id_buffer: [hash.byteLen(hash_kind)]u8 = undefined; 95 _ = try last_object_id_cursor.readBytes(&last_object_id_buffer); 96 last_object_id_maybe = last_object_id_buffer; 97 } 98 99 // for each event we want to consume... 100 for (ctx.repo_events) |repo_event| { 101 // map that associates this oid with the current transaction id. 102 // this will be important for reverting things when a rebase occurrs. 103 var object_id_to_tx_id_cursor = try haxy.putCursor(hash.hashInt(hash_kind, "object-id->tx-id")); 104 105 // map with the views as they appeared when each event was consumed. 106 // we can use this to see (and revert) the views to any previous state. 107 var object_id_to_views_cursor = try haxy.putCursor(hash.hashInt(hash_kind, "object-id->views")); 108 109 // if this object id has already been consumed, skip it 110 { 111 const object_id_to_views = try DB.HashMap(.read_only).init(object_id_to_views_cursor.readOnly()); 112 if (null != try object_id_to_views.getCursor(hash.bytesToInt(hash_kind, &repo_event.oid))) { 113 continue; 114 } 115 } 116 117 // compare last-object-id to the event's parent. this is important 118 // in situations where the branch was rebased, because in that case 119 // the last-object-id may no longer be valid. 120 if (last_object_id_maybe) |*last_object_id| { 121 if (repo_event.parent_oid) |*parent_oid| { 122 if (!std.mem.eql(u8, last_object_id, parent_oid)) { 123 // the last-object-id does not match the current event's parent id. 124 // this means that a rebase occurred. we just need to revert the 125 // object-id maps to the state they were in when parent-oid was consumed. 126 127 const object_id_to_tx_id = try DB.HashMap(.read_only).init(object_id_to_tx_id_cursor.readOnly()); 128 const tx_id_cursor = try object_id_to_tx_id.getCursor(hash.bytesToInt(hash_kind, parent_oid)) orelse return error.ObjectNotFound; 129 const tx_id = try tx_id_cursor.readUint(); 130 const history = try DB.ArrayList(.read_only).init(cursor.db.rootCursor().readOnly()); 131 132 const old_moment_cursor = try history.getCursor(tx_id) orelse return error.TransactionNotFound; 133 const old_moment = try DB.HashMap(.read_only).init(old_moment_cursor); 134 135 const old_haxy_cursor = try old_moment.getCursor(hash.hashInt(hash_kind, "haxy")) orelse return error.CursorNotFound; 136 const old_haxy = try DB.HashMap(.read_only).init(old_haxy_cursor); 137 138 const old_object_id_to_tx_id_cursor = try old_haxy.getCursor(hash.hashInt(hash_kind, "object-id->tx-id")) orelse return error.CursorNotFound; 139 const old_object_id_to_views_cursor = try old_haxy.getCursor(hash.hashInt(hash_kind, "object-id->views")) orelse return error.CursorNotFound; 140 141 try object_id_to_tx_id_cursor.write(.{ .slot = old_object_id_to_tx_id_cursor.slot() }); 142 try object_id_to_views_cursor.write(.{ .slot = old_object_id_to_views_cursor.slot() }); 143 144 last_object_id_maybe = parent_oid.*; 145 } 146 } else { 147 // the branch was rebased all the way to the very beginning. 148 // we have a repo event with no parent, which means it is now 149 // the very first event. all we need to do is set the 150 // object-id maps to be empty. 151 try object_id_to_tx_id_cursor.write(.{ .slot = null }); 152 try object_id_to_views_cursor.write(.{ .slot = null }); 153 } 154 } else { 155 if (repo_event.parent_oid) |_| { 156 // there is no last-object-id, but this event has a parent. 157 // this is an invalid state. if the event has a parent, that 158 // implies that an event has already been processed, but 159 // if that's true then there would be a last-object-id. 160 return error.UnexpectedParent; 161 } 162 } 163 164 // associate this object id with this transaction id 165 const object_id_to_tx_id = try DB.HashMap(.read_write).init(object_id_to_tx_id_cursor); 166 try object_id_to_tx_id.put(hash.bytesToInt(hash_kind, &repo_event.oid), .{ .uint = ctx.history_count }); 167 168 // init the object-id->views map 169 const object_id_to_views = try DB.HashMap(.read_write).init(object_id_to_views_cursor); 170 171 // create a new views map for the current event we are consuming 172 var views_cursor = try object_id_to_views.putCursor(hash.bytesToInt(hash_kind, &repo_event.oid)); 173 174 // if there was a previous event, set the views map to have the same value as it. 175 // this efficiently "clones" the map so we make further modifications based on it. 176 if (last_object_id_maybe) |*last_object_id| { 177 if (try object_id_to_views.getCursor(hash.bytesToInt(hash_kind, last_object_id))) |last_view_cursor| { 178 try views_cursor.write(.{ .slot = last_view_cursor.slot() }); 179 } 180 } 181 182 // consume the event into the views map 183 { 184 const event = repo_event.event; 185 186 // get the id of the current event as bytes 187 var current_event_id: [event_id_size]u8 = undefined; 188 _ = try std.fmt.hexToBytes(&current_event_id, &event.id); 189 190 const views = try DB.HashMap(.read_write).init(views_cursor); 191 192 switch (event.data) { 193 .issue => |data| { 194 const event_id_to_issue_cursor = try views.putCursor(hash.hashInt(hash_kind, "event-id->issue")); 195 const event_id_to_issue = try DB.HashMap(.read_write).init(event_id_to_issue_cursor); 196 197 const issue_cursor = try event_id_to_issue.putCursor(hash.hashInt(hash_kind, &current_event_id)); 198 const issue = try DB.HashMap(.read_write).init(issue_cursor); 199 200 try upsert(DB, hash_kind, issue, @TypeOf(data), data); 201 }, 202 } 203 } 204 205 // the current object id is now the last one 206 last_object_id_maybe = repo_event.oid; 207 208 // prevent any of the data created above from being mutated by future iterations of this loop 209 try cursor.db.freeze(); 210 } 211 212 if (last_object_id_maybe) |*last_object_id| { 213 try haxy.put(hash.hashInt(hash_kind, "last-object-id"), .{ .bytes = last_object_id }); 214 } 215 } 216 }; 217 218 try db_file.lock(io, .exclusive); 219 defer db_file.unlock(io); 220 221 // create a new transaction in the database that runs the above-defined Ctx function 222 const history = try DB.ArrayList(.read_write).init(db.rootCursor()); 223 try history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ 224 .repo_events = repo_events, 225 .history_count = try history.count(), 226 }); 227 } 228 229 fn upsert( 230 comptime DB: type, 231 comptime hash_kind: hash.HashKind, 232 map: DB.HashMap(.read_write), 233 comptime Data: type, 234 data: Data, 235 ) !void { 236 switch (@typeInfo(Data)) { 237 .@"struct" => |struct_info| { 238 inline for (struct_info.fields) |field| { 239 try upsertField(DB, hash_kind, map, field.name, field.type, @field(data, field.name)); 240 } 241 }, 242 else => @compileError("upsert expects a struct"), 243 } 244 } 245 246 fn upsertField( 247 comptime DB: type, 248 comptime hash_kind: hash.HashKind, 249 map: DB.HashMap(.read_write), 250 comptime field_name: []const u8, 251 comptime Field: type, 252 value: Field, 253 ) !void { 254 const key = hash.hashInt(hash_kind, field_name); 255 256 switch (@typeInfo(Field)) { 257 .pointer => |pointer_info| { 258 if (pointer_info.size == .slice and pointer_info.child == u8) { 259 try upsertBytes(DB, hash_kind, map, key, value); 260 } else { 261 @compileError("unsupported upsert field type: " ++ @typeName(Field)); 262 } 263 }, 264 .array => |array_info| { 265 if (array_info.child == u8) { 266 try upsertBytes(DB, hash_kind, map, key, &value); 267 } else { 268 @compileError("unsupported upsert field type: " ++ @typeName(Field)); 269 } 270 }, 271 .int => |int_info| switch (int_info.signedness) { 272 .unsigned => { 273 if (try map.getCursor(key)) |value_cursor| { 274 if (try value_cursor.readUint() == value) { 275 return; 276 } 277 } 278 279 try map.put(key, .{ .uint = value }); 280 }, 281 .signed => { 282 if (try map.getCursor(key)) |value_cursor| { 283 if (try value_cursor.readInt() == value) { 284 return; 285 } 286 } 287 288 try map.put(key, .{ .int = value }); 289 }, 290 }, 291 else => @compileError("unsupported upsert field type: " ++ @typeName(Field)), 292 } 293 } 294 295 fn upsertBytes( 296 comptime DB: type, 297 comptime hash_kind: hash.HashKind, 298 map: DB.HashMap(.read_write), 299 key: hash.HashInt(hash_kind), 300 value: []const u8, 301 ) !void { 302 var existing_cursor_maybe = try map.getCursor(key); 303 if (existing_cursor_maybe) |*existing_cursor| { 304 if (try bytesEqual(DB, existing_cursor, value)) { 305 return; 306 } 307 } 308 309 var value_cursor = try map.putCursor(key); 310 var write_buffer: [1024]u8 = undefined; 311 var writer = try value_cursor.writer(&write_buffer); 312 try writer.interface.writeAll(value); 313 try writer.finish(); 314 } 315 316 fn bytesEqual( 317 comptime DB: type, 318 cursor: *DB.Cursor(.read_only), 319 value: []const u8, 320 ) !bool { 321 var read_buffer: [1024]u8 = undefined; 322 var reader = try cursor.reader(&read_buffer); 323 if (reader.size != value.len) { 324 return false; 325 } 326 327 var chunk_buffer: [1024]u8 = undefined; 328 var offset: usize = 0; 329 while (offset < value.len) { 330 const chunk_len = @min(chunk_buffer.len, value.len - offset); 331 try reader.interface.readSliceAll(chunk_buffer[0..chunk_len]); 332 if (!std.mem.eql(u8, chunk_buffer[0..chunk_len], value[offset .. offset + chunk_len])) { 333 return false; 334 } 335 offset += chunk_len; 336 } 337 338 return true; 339 } 340