1use std::{
2 collections::{HashMap, HashSet},
3 fs,
4 hash::Hash,
5 path::{Path, PathBuf},
6 sync::{
7 Mutex, RwLock,
8 atomic::{AtomicBool, Ordering},
9 },
10 time::{Duration, Instant},
11};
12
13pub use kcode_k1_invites::UserId;
14use kcode_k1_transaction::{SubsystemId, Transaction};
15use kcode_k1_txn_ordering::K1TxnOrdering;
16pub use kcode_k1_txn_ordering::TxId;
17use rusqlite::{Connection, Error as SqlError, TransactionBehavior, params};
18
19const DATABASE_NAME: &str = "groups.sqlite3";
20const SUBSYSTEM: &str = "k1-groups-subsystem";
21const CREATE_METADATA: &str = "CREATE TABLE metadata (singleton INTEGER PRIMARY KEY CHECK (singleton = 1), schema_version INTEGER NOT NULL, last_applied_txid BLOB)";
22const CREATE_GROUPS: &str =
23 "CREATE TABLE groups (group_id BLOB PRIMARY KEY, revision BLOB NOT NULL)";
24const CREATE_USERS: &str = "CREATE TABLE user_memberships (group_id BLOB NOT NULL, user_id BLOB NOT NULL, role INTEGER NOT NULL, PRIMARY KEY (group_id, user_id), FOREIGN KEY (group_id) REFERENCES groups(group_id) ON DELETE CASCADE)";
25const CREATE_MODELS: &str = "CREATE TABLE model_memberships (group_id BLOB NOT NULL, model_id BLOB NOT NULL, PRIMARY KEY (group_id, model_id), FOREIGN KEY (group_id) REFERENCES groups(group_id) ON DELETE CASCADE)";
26const CREATE_USER_INDEX: &str =
27 "CREATE INDEX user_memberships_by_user ON user_memberships (user_id, group_id)";
28const CREATE_MODEL_INDEX: &str =
29 "CREATE INDEX model_memberships_by_model ON model_memberships (model_id, group_id)";
30
31#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
32pub struct GroupId(TxId);
33
34#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
35pub enum SentinelGroup {
36 AllUsers,
37 AllModels,
38 LocalModels,
39}
40
41pub const ALL_USERS: GroupId = GroupId::new(TxId::from_bytes([
42 255, 75, 49, 71, 82, 80, 0, 0, 0, 0, 0, 1,
43]));
44pub const ALL_MODELS: GroupId = GroupId::new(TxId::from_bytes([
45 255, 75, 49, 71, 82, 80, 0, 0, 0, 0, 0, 2,
46]));
47pub const LOCAL_MODELS: GroupId = GroupId::new(TxId::from_bytes([
48 255, 75, 49, 71, 82, 80, 0, 0, 0, 0, 0, 3,
49]));
50
51impl GroupId {
52 pub const fn new(txid: TxId) -> Self {
53 Self(txid)
54 }
55
56 pub const fn txid(self) -> TxId {
57 self.0
58 }
59
60 pub const fn sentinel(self) -> Option<SentinelGroup> {
61 match self {
62 ALL_USERS => Some(SentinelGroup::AllUsers),
63 ALL_MODELS => Some(SentinelGroup::AllModels),
64 LOCAL_MODELS => Some(SentinelGroup::LocalModels),
65 _ => None,
66 }
67 }
68}
69
70#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
71pub struct ModelId([u8; 32]);
72
73impl ModelId {
74 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
75 Self(bytes)
76 }
77
78 pub const fn as_bytes(&self) -> &[u8; 32] {
79 &self.0
80 }
81
82 pub const fn into_bytes(self) -> [u8; 32] {
83 self.0
84 }
85}
86
87#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
88pub enum GroupRole {
89 User,
90 Admin,
91 Owner,
92}
93
94impl GroupRole {
95 fn stored(self) -> i64 {
96 match self {
97 Self::User => 0,
98 Self::Admin => 1,
99 Self::Owner => 2,
100 }
101 }
102
103 fn from_stored(value: i64) -> Option<Self> {
104 match value {
105 0 => Some(Self::User),
106 1 => Some(Self::Admin),
107 2 => Some(Self::Owner),
108 _ => None,
109 }
110 }
111}
112
113#[derive(Clone, Debug, Eq, PartialEq)]
114pub struct GroupUser {
115 user_id: UserId,
116 role: GroupRole,
117}
118
119impl GroupUser {
120 pub fn user_id(&self) -> UserId {
121 self.user_id
122 }
123
124 pub fn role(&self) -> GroupRole {
125 self.role
126 }
127}
128
129#[derive(Clone, Debug, Eq, PartialEq)]
130pub struct GroupRevision {
131 group_id: GroupId,
132 txid: TxId,
133}
134
135impl GroupRevision {
136 pub fn group_id(&self) -> GroupId {
137 self.group_id
138 }
139
140 pub fn txid(&self) -> TxId {
141 self.txid
142 }
143}
144
145#[derive(Clone, Debug, Eq, PartialEq)]
146pub struct Group {
147 id: GroupId,
148 revision: GroupRevision,
149 users: Vec<GroupUser>,
150 models: Vec<ModelId>,
151}
152
153impl Group {
154 pub fn id(&self) -> GroupId {
155 self.id
156 }
157
158 pub fn revision(&self) -> &GroupRevision {
159 &self.revision
160 }
161
162 pub fn users(&self) -> &[GroupUser] {
163 &self.users
164 }
165
166 pub fn models(&self) -> &[ModelId] {
167 &self.models
168 }
169}
170
171#[derive(Clone, Debug, Eq, PartialEq)]
172pub struct GroupMemberships {
173 revision: Option<TxId>,
174 user_groups: Vec<GroupId>,
175 model_groups: Vec<GroupId>,
176 shared_groups: Vec<GroupId>,
177}
178
179impl GroupMemberships {
180 pub fn revision(&self) -> Option<TxId> {
181 self.revision
182 }
183
184 pub fn user_groups(&self) -> &[GroupId] {
185 &self.user_groups
186 }
187
188 pub fn model_groups(&self) -> &[GroupId] {
189 &self.model_groups
190 }
191
192 pub fn shared_groups(&self) -> &[GroupId] {
193 &self.shared_groups
194 }
195}
196
197#[derive(Clone, Debug, Eq, PartialEq)]
198pub enum GroupAction {
199 Create {
200 owner: UserId,
201 },
202 SetUserRole {
203 group: GroupId,
204 actor: UserId,
205 user: UserId,
206 role: Option<GroupRole>,
207 },
208 SetModelMembership {
209 group: GroupId,
210 actor: UserId,
211 model: ModelId,
212 present: bool,
213 },
214}
215
216#[derive(Clone, Debug, Eq, PartialEq)]
217pub enum ApplyOutcome {
218 Applied(GroupRevision),
219 Unchanged(GroupRevision),
220 Rejected(String),
221}
222
223#[derive(Default)]
224struct State {
225 cursor: Option<TxId>,
226 groups: HashMap<GroupId, GroupState>,
227 user_groups: HashMap<UserId, HashSet<GroupId>>,
228 model_groups: HashMap<ModelId, HashSet<GroupId>>,
229}
230
231struct GroupState {
232 revision: TxId,
233 users: HashMap<UserId, GroupRole>,
234 models: HashSet<ModelId>,
235 owners: usize,
236}
237
238pub struct Projection {
239 state: RwLock<State>,
240 apply_lane: Mutex<()>,
241 connection: Mutex<Connection>,
242 unavailable: AtomicBool,
243}
244
245impl Projection {
246 pub fn open(root: &Path, ordering: &K1TxnOrdering) -> Result<(Self, Option<TxId>), String> {
247 let started = Instant::now();
248 let result = Self::open_inner(root, ordering);
249 let elapsed = started.elapsed();
250 if elapsed > Duration::from_millis(100) {
251 let outcome = if result.is_ok() { "ready" } else { "error" };
252 eprintln!(
253 "level=warn module=kcode-k1-groups-projection operation=open elapsed_us={} outcome={outcome}",
254 elapsed.as_micros()
255 );
256 }
257 result
258 }
259
260 fn open_inner(root: &Path, ordering: &K1TxnOrdering) -> Result<(Self, Option<TxId>), String> {
261 fs::create_dir_all(root)
262 .map_err(|error| format!("create projection root {}: {error}", root.display()))?;
263 let database = root.join(DATABASE_NAME);
264 let (connection, state) = load_or_rebuild(&database, ordering)?;
265 let cursor = state.cursor;
266 Ok((
267 Self {
268 state: RwLock::new(state),
269 apply_lane: Mutex::new(()),
270 connection: Mutex::new(connection),
271 unavailable: AtomicBool::new(false),
272 },
273 cursor,
274 ))
275 }
276
277 pub fn apply(&self, callback_txid: TxId, action: GroupAction) -> Result<ApplyOutcome, String> {
278 self.ensure_available()?;
279 let _lane = self.apply_lane.lock().map_err(|_| {
280 self.mark_unavailable();
281 "projection apply lane is unavailable until reopen".to_owned()
282 })?;
283 self.ensure_available()?;
284 let prepared = self.prepare(callback_txid, action)?;
285 {
286 let mut connection = self.connection.lock().map_err(|_| {
287 self.mark_unavailable();
288 "projection database lane is unavailable until reopen".to_owned()
289 })?;
290 if let Err(error) = persist_prepared(&mut connection, callback_txid, &prepared) {
291 self.mark_unavailable();
292 return Err(error);
293 }
294 }
295 self.publish(callback_txid, prepared)
296 }
297
298 pub fn get(&self, group: GroupId) -> Result<Option<Group>, String> {
299 self.ensure_available()?;
300 if group.sentinel().is_some() {
301 return Ok(None);
302 }
303 let state = self.state.read().map_err(|_| {
304 self.mark_unavailable();
305 "projection state is unavailable until reopen".to_owned()
306 })?;
307 self.ensure_available()?;
308 let Some(stored) = state.groups.get(&group) else {
309 return Ok(None);
310 };
311 let mut users = Vec::new();
312 users
313 .try_reserve(stored.users.len())
314 .map_err(|error| format!("reserve group users: {error}"))?;
315 users.extend(stored.users.iter().map(|(user_id, role)| GroupUser {
316 user_id: *user_id,
317 role: *role,
318 }));
319 let mut models = Vec::new();
320 models
321 .try_reserve(stored.models.len())
322 .map_err(|error| format!("reserve group models: {error}"))?;
323 models.extend(stored.models.iter().copied());
324 Ok(Some(Group {
325 id: group,
326 revision: GroupRevision {
327 group_id: group,
328 txid: stored.revision,
329 },
330 users,
331 models,
332 }))
333 }
334
335 pub fn groups_for_user(&self, user: UserId) -> Result<Vec<GroupId>, String> {
336 self.ensure_available()?;
337 let state = self.state.read().map_err(|_| {
338 self.mark_unavailable();
339 "projection state is unavailable until reopen".to_owned()
340 })?;
341 self.ensure_available()?;
342 copy_group_set_with_sentinel(state.user_groups.get(&user), ALL_USERS, "user groups")
343 }
344
345 pub fn groups_for_model(&self, model: ModelId) -> Result<Vec<GroupId>, String> {
346 self.ensure_available()?;
347 let state = self.state.read().map_err(|_| {
348 self.mark_unavailable();
349 "projection state is unavailable until reopen".to_owned()
350 })?;
351 self.ensure_available()?;
352 copy_group_set_with_sentinel(state.model_groups.get(&model), ALL_MODELS, "model groups")
353 }
354
355 pub fn memberships(&self, user: UserId, model: ModelId) -> Result<GroupMemberships, String> {
356 self.ensure_available()?;
357 let state = self.state.read().map_err(|_| {
358 self.mark_unavailable();
359 "projection state is unavailable until reopen".to_owned()
360 })?;
361 self.ensure_available()?;
362 let user_set = state.user_groups.get(&user);
363 let model_set = state.model_groups.get(&model);
364 let user_groups =
365 copy_group_set_with_sentinel(user_set, ALL_USERS, "membership user groups")?;
366 let model_groups =
367 copy_group_set_with_sentinel(model_set, ALL_MODELS, "membership model groups")?;
368 let mut shared_groups = Vec::new();
369 let shared_capacity = user_set
370 .map(HashSet::len)
371 .unwrap_or_default()
372 .min(model_set.map(HashSet::len).unwrap_or_default());
373 shared_groups
374 .try_reserve(shared_capacity)
375 .map_err(|error| format!("reserve shared groups: {error}"))?;
376 if let (Some(users), Some(models)) = (user_set, model_set) {
377 let (smaller, larger) = if users.len() <= models.len() {
378 (users, models)
379 } else {
380 (models, users)
381 };
382 shared_groups.extend(
383 smaller
384 .iter()
385 .filter(|group| larger.contains(group))
386 .copied(),
387 );
388 }
389 Ok(GroupMemberships {
390 revision: state.cursor,
391 user_groups,
392 model_groups,
393 shared_groups,
394 })
395 }
396
397 pub fn clear(&self) -> Result<(), String> {
398 self.ensure_available()?;
399 let _lane = self.apply_lane.lock().map_err(|_| {
400 self.mark_unavailable();
401 "projection apply lane is unavailable until reopen".to_owned()
402 })?;
403 self.ensure_available()?;
404 {
405 let mut connection = self.connection.lock().map_err(|_| {
406 self.mark_unavailable();
407 "projection database lane is unavailable until reopen".to_owned()
408 })?;
409 if let Err(error) = persist_clear(&mut connection) {
410 self.mark_unavailable();
411 return Err(error);
412 }
413 }
414 let old_state = {
415 let mut state = self.state.write().map_err(|_| {
416 self.mark_unavailable();
417 "projection state is unavailable until reopen".to_owned()
418 })?;
419 std::mem::take(&mut *state)
420 };
421 drop(old_state);
422 Ok(())
423 }
424
425 fn prepare(&self, callback_txid: TxId, action: GroupAction) -> Result<Prepared, String> {
426 let mut state = self.state.write().map_err(|_| {
427 self.mark_unavailable();
428 "projection state is unavailable until reopen".to_owned()
429 })?;
430 self.ensure_available()?;
431 match action {
432 GroupAction::Create { owner } => {
433 let group = GroupId::new(callback_txid);
434 if group.sentinel().is_some() {
435 return Ok(Prepared::rejected("reserved sentinel group ID"));
436 }
437 if state.groups.contains_key(&group) {
438 return Ok(Prepared::rejected("group already exists"));
439 }
440 state
441 .groups
442 .try_reserve(1)
443 .map_err(|error| format!("reserve group index: {error}"))?;
444 let new_reverse = if let Some(reverse) = state.user_groups.get_mut(&owner) {
445 reverse
446 .try_reserve(1)
447 .map_err(|error| format!("reserve owner reverse membership: {error}"))?;
448 None
449 } else {
450 state
451 .user_groups
452 .try_reserve(1)
453 .map_err(|error| format!("reserve user reverse index: {error}"))?;
454 let mut reverse = HashSet::new();
455 reverse
456 .try_reserve(1)
457 .map_err(|error| format!("reserve owner reverse membership: {error}"))?;
458 reverse.insert(group);
459 Some(reverse)
460 };
461 let mut users = HashMap::new();
462 users
463 .try_reserve(1)
464 .map_err(|error| format!("reserve owner membership: {error}"))?;
465 users.insert(owner, GroupRole::Owner);
466 Ok(Prepared {
467 result: PreparedResult::Applied(GroupRevision {
468 group_id: group,
469 txid: callback_txid,
470 }),
471 mutation: Mutation::Create {
472 group,
473 owner,
474 stored: GroupState {
475 revision: callback_txid,
476 users,
477 models: HashSet::new(),
478 owners: 1,
479 },
480 new_reverse,
481 },
482 })
483 }
484 GroupAction::SetUserRole {
485 group,
486 actor,
487 user,
488 role,
489 } => prepare_user_change(&mut state, group, actor, user, role, callback_txid),
490 GroupAction::SetModelMembership {
491 group,
492 actor,
493 model,
494 present,
495 } => prepare_model_change(&mut state, group, actor, model, present, callback_txid),
496 }
497 }
498
499 fn publish(&self, callback_txid: TxId, prepared: Prepared) -> Result<ApplyOutcome, String> {
500 let mut state = self.state.write().map_err(|_| {
501 self.mark_unavailable();
502 "projection state is unavailable until reopen".to_owned()
503 })?;
504 if !mutation_matches(&state, &prepared.mutation) {
505 self.mark_unavailable();
506 return Err(
507 "projection state contradicted committed database state; reopen required"
508 .to_owned(),
509 );
510 }
511 match prepared.mutation {
512 Mutation::None => {}
513 Mutation::Create {
514 group,
515 owner,
516 stored,
517 new_reverse,
518 } => {
519 state.groups.insert(group, stored);
520 if let Some(reverse) = new_reverse {
521 state.user_groups.insert(owner, reverse);
522 } else {
523 state
524 .user_groups
525 .get_mut(&owner)
526 .expect("validated owner reverse index")
527 .insert(group);
528 }
529 }
530 Mutation::User {
531 group,
532 user,
533 from,
534 to,
535 previous_owners,
536 new_reverse,
537 ..
538 } => {
539 let stored = state.groups.get_mut(&group).expect("validated group");
540 match to {
541 Some(role) => {
542 stored.users.insert(user, role);
543 }
544 None => {
545 stored.users.remove(&user);
546 }
547 }
548 stored.owners = owner_count_after(previous_owners, from, to);
549 stored.revision = callback_txid;
550 match (from, to) {
551 (None, Some(_)) => {
552 if let Some(reverse) = new_reverse {
553 state.user_groups.insert(user, reverse);
554 } else {
555 state
556 .user_groups
557 .get_mut(&user)
558 .expect("validated user reverse index")
559 .insert(group);
560 }
561 }
562 (Some(_), None) => {
563 let reverse = state
564 .user_groups
565 .get_mut(&user)
566 .expect("validated user reverse index");
567 reverse.remove(&group);
568 if reverse.is_empty() {
569 state.user_groups.remove(&user);
570 }
571 }
572 _ => {}
573 }
574 }
575 Mutation::Model {
576 group,
577 model,
578 from,
579 to,
580 new_reverse,
581 ..
582 } => {
583 let stored = state.groups.get_mut(&group).expect("validated group");
584 if to {
585 stored.models.insert(model);
586 } else {
587 stored.models.remove(&model);
588 }
589 stored.revision = callback_txid;
590 match (from, to) {
591 (false, true) => {
592 if let Some(reverse) = new_reverse {
593 state.model_groups.insert(model, reverse);
594 } else {
595 state
596 .model_groups
597 .get_mut(&model)
598 .expect("validated model reverse index")
599 .insert(group);
600 }
601 }
602 (true, false) => {
603 let reverse = state
604 .model_groups
605 .get_mut(&model)
606 .expect("validated model reverse index");
607 reverse.remove(&group);
608 if reverse.is_empty() {
609 state.model_groups.remove(&model);
610 }
611 }
612 _ => {}
613 }
614 }
615 }
616 state.cursor = Some(callback_txid);
617 Ok(prepared.result.into_public())
618 }
619
620 fn ensure_available(&self) -> Result<(), String> {
621 if self.unavailable.load(Ordering::SeqCst) {
622 Err("projection is unavailable until reopen".to_owned())
623 } else {
624 Ok(())
625 }
626 }
627
628 fn mark_unavailable(&self) {
629 self.unavailable.store(true, Ordering::SeqCst);
630 }
631}
632
633fn prepare_user_change(
634 state: &mut State,
635 group: GroupId,
636 actor: UserId,
637 user: UserId,
638 to: Option<GroupRole>,
639 callback_txid: TxId,
640) -> Result<Prepared, String> {
641 if group.sentinel().is_some() {
642 return Ok(Prepared::rejected("sentinel groups are immutable"));
643 }
644 let Some(stored) = state.groups.get(&group) else {
645 return Ok(Prepared::rejected("group does not exist"));
646 };
647 let Some(actor_role) = stored.users.get(&actor).copied() else {
648 return Ok(Prepared::rejected("actor is not authorized"));
649 };
650 let from = stored.users.get(&user).copied();
651 let permitted = match actor_role {
652 GroupRole::User => false,
653 GroupRole::Admin => matches!(
654 (from, to),
655 (None, Some(GroupRole::User))
656 | (Some(GroupRole::User), None)
657 | (Some(GroupRole::User), Some(GroupRole::User))
658 ),
659 GroupRole::Owner => true,
660 };
661 if !permitted {
662 return Ok(Prepared::rejected(match actor_role {
663 GroupRole::Admin => "administrator transition is not permitted",
664 _ => "actor is not authorized",
665 }));
666 }
667 if actor_role == GroupRole::Owner
668 && from == Some(GroupRole::Owner)
669 && to != Some(GroupRole::Owner)
670 && stored.owners == 1
671 {
672 return Ok(Prepared::rejected(
673 "final owner cannot be removed or demoted",
674 ));
675 }
676 let previous_revision = stored.revision;
677 let previous_owners = stored.owners;
678 if from == to {
679 return Ok(Prepared::unchanged(group, previous_revision));
680 }
681 let mut new_reverse = None;
682 if from.is_none() && to.is_some() {
683 state
684 .groups
685 .get_mut(&group)
686 .expect("existing group")
687 .users
688 .try_reserve(1)
689 .map_err(|error| format!("reserve group user membership: {error}"))?;
690 if let Some(reverse) = state.user_groups.get_mut(&user) {
691 reverse
692 .try_reserve(1)
693 .map_err(|error| format!("reserve user reverse membership: {error}"))?;
694 } else {
695 state
696 .user_groups
697 .try_reserve(1)
698 .map_err(|error| format!("reserve user reverse index: {error}"))?;
699 let mut reverse = HashSet::new();
700 reverse
701 .try_reserve(1)
702 .map_err(|error| format!("reserve user reverse membership: {error}"))?;
703 reverse.insert(group);
704 new_reverse = Some(reverse);
705 }
706 }
707 Ok(Prepared {
708 result: PreparedResult::Applied(GroupRevision {
709 group_id: group,
710 txid: callback_txid,
711 }),
712 mutation: Mutation::User {
713 group,
714 user,
715 from,
716 to,
717 previous_revision,
718 previous_owners,
719 new_reverse,
720 },
721 })
722}
723
724fn prepare_model_change(
725 state: &mut State,
726 group: GroupId,
727 actor: UserId,
728 model: ModelId,
729 to: bool,
730 callback_txid: TxId,
731) -> Result<Prepared, String> {
732 if group.sentinel().is_some() {
733 return Ok(Prepared::rejected("sentinel groups are immutable"));
734 }
735 let Some(stored) = state.groups.get(&group) else {
736 return Ok(Prepared::rejected("group does not exist"));
737 };
738 if stored.users.get(&actor) != Some(&GroupRole::Owner) {
739 return Ok(Prepared::rejected("actor is not authorized"));
740 }
741 let from = stored.models.contains(&model);
742 let previous_revision = stored.revision;
743 if from == to {
744 return Ok(Prepared::unchanged(group, previous_revision));
745 }
746 let mut new_reverse = None;
747 if to {
748 state
749 .groups
750 .get_mut(&group)
751 .expect("existing group")
752 .models
753 .try_reserve(1)
754 .map_err(|error| format!("reserve group model membership: {error}"))?;
755 if let Some(reverse) = state.model_groups.get_mut(&model) {
756 reverse
757 .try_reserve(1)
758 .map_err(|error| format!("reserve model reverse membership: {error}"))?;
759 } else {
760 state
761 .model_groups
762 .try_reserve(1)
763 .map_err(|error| format!("reserve model reverse index: {error}"))?;
764 let mut reverse = HashSet::new();
765 reverse
766 .try_reserve(1)
767 .map_err(|error| format!("reserve model reverse membership: {error}"))?;
768 reverse.insert(group);
769 new_reverse = Some(reverse);
770 }
771 }
772 Ok(Prepared {
773 result: PreparedResult::Applied(GroupRevision {
774 group_id: group,
775 txid: callback_txid,
776 }),
777 mutation: Mutation::Model {
778 group,
779 model,
780 from,
781 to,
782 previous_revision,
783 new_reverse,
784 },
785 })
786}
787
788struct Prepared {
789 result: PreparedResult,
790 mutation: Mutation,
791}
792
793impl Prepared {
794 fn rejected(reason: &str) -> Self {
795 Self {
796 result: PreparedResult::Rejected(reason.to_owned()),
797 mutation: Mutation::None,
798 }
799 }
800
801 fn unchanged(group_id: GroupId, txid: TxId) -> Self {
802 Self {
803 result: PreparedResult::Unchanged(GroupRevision { group_id, txid }),
804 mutation: Mutation::None,
805 }
806 }
807}
808
809enum PreparedResult {
810 Applied(GroupRevision),
811 Unchanged(GroupRevision),
812 Rejected(String),
813}
814
815impl PreparedResult {
816 fn into_public(self) -> ApplyOutcome {
817 match self {
818 Self::Applied(revision) => ApplyOutcome::Applied(revision),
819 Self::Unchanged(revision) => ApplyOutcome::Unchanged(revision),
820 Self::Rejected(reason) => ApplyOutcome::Rejected(reason),
821 }
822 }
823}
824
825enum Mutation {
826 None,
827 Create {
828 group: GroupId,
829 owner: UserId,
830 stored: GroupState,
831 new_reverse: Option<HashSet<GroupId>>,
832 },
833 User {
834 group: GroupId,
835 user: UserId,
836 from: Option<GroupRole>,
837 to: Option<GroupRole>,
838 previous_revision: TxId,
839 previous_owners: usize,
840 new_reverse: Option<HashSet<GroupId>>,
841 },
842 Model {
843 group: GroupId,
844 model: ModelId,
845 from: bool,
846 to: bool,
847 previous_revision: TxId,
848 new_reverse: Option<HashSet<GroupId>>,
849 },
850}
851
852fn mutation_matches(state: &State, mutation: &Mutation) -> bool {
853 match mutation {
854 Mutation::None => true,
855 Mutation::Create {
856 group,
857 owner,
858 new_reverse,
859 ..
860 } => {
861 if state.groups.contains_key(group) {
862 return false;
863 }
864 match new_reverse {
865 Some(reverse) => !state.user_groups.contains_key(owner) && reverse.contains(group),
866 None => state
867 .user_groups
868 .get(owner)
869 .is_some_and(|groups| !groups.contains(group)),
870 }
871 }
872 Mutation::User {
873 group,
874 user,
875 from,
876 to,
877 previous_revision,
878 previous_owners,
879 new_reverse,
880 } => {
881 let Some(stored) = state.groups.get(group) else {
882 return false;
883 };
884 if stored.revision != *previous_revision
885 || stored.owners != *previous_owners
886 || stored.users.get(user).copied() != *from
887 {
888 return false;
889 }
890 match (from, to) {
891 (None, Some(_)) => match new_reverse {
892 Some(reverse) => {
893 !state.user_groups.contains_key(user) && reverse.contains(group)
894 }
895 None => state
896 .user_groups
897 .get(user)
898 .is_some_and(|groups| !groups.contains(group)),
899 },
900 (Some(_), None) => state
901 .user_groups
902 .get(user)
903 .is_some_and(|groups| groups.contains(group)),
904 _ => true,
905 }
906 }
907 Mutation::Model {
908 group,
909 model,
910 from,
911 to,
912 previous_revision,
913 new_reverse,
914 } => {
915 let Some(stored) = state.groups.get(group) else {
916 return false;
917 };
918 if stored.revision != *previous_revision || stored.models.contains(model) != *from {
919 return false;
920 }
921 match (from, to) {
922 (false, true) => match new_reverse {
923 Some(reverse) => {
924 !state.model_groups.contains_key(model) && reverse.contains(group)
925 }
926 None => state
927 .model_groups
928 .get(model)
929 .is_some_and(|groups| !groups.contains(group)),
930 },
931 (true, false) => state
932 .model_groups
933 .get(model)
934 .is_some_and(|groups| groups.contains(group)),
935 _ => true,
936 }
937 }
938 }
939}
940
941fn owner_count_after(previous: usize, from: Option<GroupRole>, to: Option<GroupRole>) -> usize {
942 match (from == Some(GroupRole::Owner), to == Some(GroupRole::Owner)) {
943 (false, true) => previous + 1,
944 (true, false) => previous - 1,
945 _ => previous,
946 }
947}
948
949fn persist_prepared(
950 connection: &mut Connection,
951 callback_txid: TxId,
952 prepared: &Prepared,
953) -> Result<(), String> {
954 let transaction = connection
955 .transaction_with_behavior(TransactionBehavior::Immediate)
956 .map_err(|error| format!("begin projection apply transaction: {error}"))?;
957 match &prepared.mutation {
958 Mutation::None => {}
959 Mutation::Create {
960 group,
961 owner,
962 stored,
963 ..
964 } => {
965 expect_one(
966 transaction.execute(
967 "INSERT INTO groups (group_id, revision) VALUES (?1, ?2)",
968 params![
969 group.txid().as_bytes().as_slice(),
970 stored.revision.as_bytes().as_slice()
971 ],
972 ),
973 "insert group",
974 )?;
975 let owner_txid = owner.as_tx_id();
976 expect_one(
977 transaction.execute(
978 "INSERT INTO user_memberships (group_id, user_id, role) VALUES (?1, ?2, ?3)",
979 params![
980 group.txid().as_bytes().as_slice(),
981 owner_txid.as_bytes().as_slice(),
982 GroupRole::Owner.stored()
983 ],
984 ),
985 "insert owner membership",
986 )?;
987 }
988 Mutation::User {
989 group,
990 user,
991 from,
992 to,
993 ..
994 } => {
995 let user_txid = user.as_tx_id();
996 match (from, to) {
997 (None, Some(role)) => expect_one(
998 transaction.execute(
999 "INSERT INTO user_memberships (group_id, user_id, role) VALUES (?1, ?2, ?3)",
1000 params![
1001 group.txid().as_bytes().as_slice(),
1002 user_txid.as_bytes().as_slice(),
1003 role.stored()
1004 ],
1005 ),
1006 "insert user membership",
1007 )?,
1008 (Some(_), Some(role)) => expect_one(
1009 transaction.execute(
1010 "UPDATE user_memberships SET role = ?3 WHERE group_id = ?1 AND user_id = ?2",
1011 params![
1012 group.txid().as_bytes().as_slice(),
1013 user_txid.as_bytes().as_slice(),
1014 role.stored()
1015 ],
1016 ),
1017 "update user membership",
1018 )?,
1019 (Some(_), None) => expect_one(
1020 transaction.execute(
1021 "DELETE FROM user_memberships WHERE group_id = ?1 AND user_id = ?2",
1022 params![
1023 group.txid().as_bytes().as_slice(),
1024 user_txid.as_bytes().as_slice()
1025 ],
1026 ),
1027 "delete user membership",
1028 )?,
1029 (None, None) => {
1030 return Err("prepared user mutation has no state change".to_owned());
1031 }
1032 }
1033 update_group_revision(&transaction, *group, callback_txid)?;
1034 }
1035 Mutation::Model {
1036 group,
1037 model,
1038 from,
1039 to,
1040 ..
1041 } => {
1042 match (from, to) {
1043 (false, true) => expect_one(
1044 transaction.execute(
1045 "INSERT INTO model_memberships (group_id, model_id) VALUES (?1, ?2)",
1046 params![
1047 group.txid().as_bytes().as_slice(),
1048 model.as_bytes().as_slice()
1049 ],
1050 ),
1051 "insert model membership",
1052 )?,
1053 (true, false) => expect_one(
1054 transaction.execute(
1055 "DELETE FROM model_memberships WHERE group_id = ?1 AND model_id = ?2",
1056 params![
1057 group.txid().as_bytes().as_slice(),
1058 model.as_bytes().as_slice()
1059 ],
1060 ),
1061 "delete model membership",
1062 )?,
1063 _ => return Err("prepared model mutation has no state change".to_owned()),
1064 }
1065 update_group_revision(&transaction, *group, callback_txid)?;
1066 }
1067 }
1068 expect_one(
1069 transaction.execute(
1070 "UPDATE metadata SET last_applied_txid = ?1 WHERE singleton = 1",
1071 params![callback_txid.as_bytes().as_slice()],
1072 ),
1073 "advance projection cursor",
1074 )?;
1075 transaction
1076 .commit()
1077 .map_err(|error| format!("commit projection apply transaction: {error}"))
1078}
1079
1080fn update_group_revision(
1081 transaction: &rusqlite::Transaction<'_>,
1082 group: GroupId,
1083 callback_txid: TxId,
1084) -> Result<(), String> {
1085 expect_one(
1086 transaction.execute(
1087 "UPDATE groups SET revision = ?2 WHERE group_id = ?1",
1088 params![
1089 group.txid().as_bytes().as_slice(),
1090 callback_txid.as_bytes().as_slice()
1091 ],
1092 ),
1093 "update group revision",
1094 )
1095}
1096
1097fn expect_one(result: Result<usize, SqlError>, operation: &str) -> Result<(), String> {
1098 let changed = result.map_err(|error| format!("{operation}: {error}"))?;
1099 if changed == 1 {
1100 Ok(())
1101 } else {
1102 Err(format!("{operation}: expected one row, changed {changed}"))
1103 }
1104}
1105
1106fn persist_clear(connection: &mut Connection) -> Result<(), String> {
1107 let transaction = connection
1108 .transaction_with_behavior(TransactionBehavior::Immediate)
1109 .map_err(|error| format!("begin projection clear transaction: {error}"))?;
1110 transaction
1111 .execute("DELETE FROM user_memberships", [])
1112 .map_err(|error| format!("clear user memberships: {error}"))?;
1113 transaction
1114 .execute("DELETE FROM model_memberships", [])
1115 .map_err(|error| format!("clear model memberships: {error}"))?;
1116 transaction
1117 .execute("DELETE FROM groups", [])
1118 .map_err(|error| format!("clear groups: {error}"))?;
1119 expect_one(
1120 transaction.execute(
1121 "UPDATE metadata SET last_applied_txid = NULL WHERE singleton = 1",
1122 [],
1123 ),
1124 "clear projection cursor",
1125 )?;
1126 transaction
1127 .commit()
1128 .map_err(|error| format!("commit projection clear transaction: {error}"))
1129}
1130
1131fn copy_group_set_with_sentinel(
1132 set: Option<&HashSet<GroupId>>,
1133 sentinel: GroupId,
1134 operation: &str,
1135) -> Result<Vec<GroupId>, String> {
1136 let ordinary = set.map(HashSet::len).unwrap_or_default();
1137 let mut values = Vec::new();
1138 values
1139 .try_reserve(
1140 ordinary
1141 .checked_add(1)
1142 .ok_or_else(|| format!("{operation} result size overflow"))?,
1143 )
1144 .map_err(|error| format!("reserve {operation}: {error}"))?;
1145 if let Some(set) = set {
1146 values.extend(set.iter().copied());
1147 }
1148 values.push(sentinel);
1149 Ok(values)
1150}
1151
1152enum LoadError {
1153 Recoverable,
1154 Fatal(String),
1155}
1156
1157fn load_or_rebuild(
1158 database: &Path,
1159 ordering: &K1TxnOrdering,
1160) -> Result<(Connection, State), String> {
1161 let exists = database.try_exists().map_err(|error| {
1162 format!(
1163 "inspect projection database {}: {error}",
1164 database.display()
1165 )
1166 })?;
1167 if !exists {
1168 return initialize_database(database);
1169 }
1170 match load_existing(database, ordering) {
1171 Ok(loaded) => Ok(loaded),
1172 Err(LoadError::Recoverable) => {
1173 remove_database_files(database)?;
1174 initialize_database(database)
1175 }
1176 Err(LoadError::Fatal(error)) => Err(error),
1177 }
1178}
1179
1180fn initialize_database(database: &Path) -> Result<(Connection, State), String> {
1181 let mut connection = Connection::open(database)
1182 .map_err(|error| format!("create projection database {}: {error}", database.display()))?;
1183 let journal: String = connection
1184 .query_row("PRAGMA journal_mode = WAL", [], |row| row.get(0))
1185 .map_err(|error| format!("enable projection WAL: {error}"))?;
1186 if !journal.eq_ignore_ascii_case("wal") {
1187 return Err(format!("enable projection WAL: SQLite selected {journal}"));
1188 }
1189 connection
1190 .execute_batch("PRAGMA synchronous = FULL; PRAGMA foreign_keys = ON;")
1191 .map_err(|error| format!("configure projection database: {error}"))?;
1192 let transaction = connection
1193 .transaction_with_behavior(TransactionBehavior::Immediate)
1194 .map_err(|error| format!("begin projection initialization: {error}"))?;
1195 for (statement, operation) in [
1196 (CREATE_METADATA, "create metadata schema"),
1197 (CREATE_GROUPS, "create groups schema"),
1198 (CREATE_USERS, "create user membership schema"),
1199 (CREATE_MODELS, "create model membership schema"),
1200 (CREATE_USER_INDEX, "create user reverse index"),
1201 (CREATE_MODEL_INDEX, "create model reverse index"),
1202 ] {
1203 transaction
1204 .execute(statement, [])
1205 .map_err(|error| format!("{operation}: {error}"))?;
1206 }
1207 expect_one(
1208 transaction.execute(
1209 "INSERT INTO metadata (singleton, schema_version, last_applied_txid) VALUES (1, 1, NULL)",
1210 [],
1211 ),
1212 "initialize projection metadata",
1213 )?;
1214 transaction
1215 .commit()
1216 .map_err(|error| format!("commit projection initialization: {error}"))?;
1217 Ok((connection, State::default()))
1218}
1219
1220fn load_existing(
1221 database: &Path,
1222 ordering: &K1TxnOrdering,
1223) -> Result<(Connection, State), LoadError> {
1224 let connection = Connection::open(database).map_err(|error| {
1225 classify_validation_error(
1226 error,
1227 &format!("open projection database {}", database.display()),
1228 )
1229 })?;
1230 let journal: String = validate_sql(
1231 connection.query_row("PRAGMA journal_mode", [], |row| row.get(0)),
1232 "read projection journal mode",
1233 )?;
1234 if !journal.eq_ignore_ascii_case("wal") {
1235 return Err(LoadError::Recoverable);
1236 }
1237 validate_sql(
1238 connection.execute_batch("PRAGMA synchronous = FULL; PRAGMA foreign_keys = ON;"),
1239 "configure projection database",
1240 )?;
1241 validate_quick_check(&connection)?;
1242 validate_schema(&connection)?;
1243 validate_foreign_keys(&connection)?;
1244 let state = load_state(&connection, ordering)?;
1245 Ok((connection, state))
1246}
1247
1248fn validate_quick_check(connection: &Connection) -> Result<(), LoadError> {
1249 let mut statement = validate_sql(
1250 connection.prepare("PRAGMA quick_check"),
1251 "prepare projection quick_check",
1252 )?;
1253 let mut rows = validate_sql(statement.query([]), "run projection quick_check")?;
1254 let Some(row) = validate_sql(rows.next(), "read projection quick_check")? else {
1255 return Err(LoadError::Recoverable);
1256 };
1257 let result: String = validate_sql(row.get(0), "decode projection quick_check")?;
1258 if result != "ok" || validate_sql(rows.next(), "finish projection quick_check")?.is_some() {
1259 return Err(LoadError::Recoverable);
1260 }
1261 Ok(())
1262}
1263
1264#[derive(Eq, Ord, PartialEq, PartialOrd)]
1265struct SchemaObject {
1266 name: String,
1267 kind: String,
1268 table: String,
1269 sql: Option<String>,
1270}
1271
1272fn validate_schema(connection: &Connection) -> Result<(), LoadError> {
1273 let mut statement = validate_sql(
1274 connection.prepare(
1275 "SELECT name, type, tbl_name, sql FROM sqlite_master ORDER BY name, type, tbl_name",
1276 ),
1277 "prepare projection schema validation",
1278 )?;
1279 let mut rows = validate_sql(statement.query([]), "query projection schema")?;
1280 let mut actual = Vec::new();
1281 while let Some(row) = validate_sql(rows.next(), "read projection schema")? {
1282 actual.push(SchemaObject {
1283 name: validate_sql(row.get(0), "decode projection schema name")?,
1284 kind: validate_sql(row.get(1), "decode projection schema type")?,
1285 table: validate_sql(row.get(2), "decode projection schema table")?,
1286 sql: validate_sql(row.get(3), "decode projection schema SQL")?,
1287 });
1288 }
1289 let mut expected = vec![
1290 schema("groups", "table", "groups", Some(CREATE_GROUPS)),
1291 schema("metadata", "table", "metadata", Some(CREATE_METADATA)),
1292 schema(
1293 "model_memberships",
1294 "table",
1295 "model_memberships",
1296 Some(CREATE_MODELS),
1297 ),
1298 schema(
1299 "model_memberships_by_model",
1300 "index",
1301 "model_memberships",
1302 Some(CREATE_MODEL_INDEX),
1303 ),
1304 schema("sqlite_autoindex_groups_1", "index", "groups", None),
1305 schema(
1306 "sqlite_autoindex_model_memberships_1",
1307 "index",
1308 "model_memberships",
1309 None,
1310 ),
1311 schema(
1312 "sqlite_autoindex_user_memberships_1",
1313 "index",
1314 "user_memberships",
1315 None,
1316 ),
1317 schema(
1318 "user_memberships",
1319 "table",
1320 "user_memberships",
1321 Some(CREATE_USERS),
1322 ),
1323 schema(
1324 "user_memberships_by_user",
1325 "index",
1326 "user_memberships",
1327 Some(CREATE_USER_INDEX),
1328 ),
1329 ];
1330 expected.sort();
1331 if actual != expected {
1332 return Err(LoadError::Recoverable);
1333 }
1334 Ok(())
1335}
1336
1337fn schema(name: &str, kind: &str, table: &str, sql: Option<&str>) -> SchemaObject {
1338 SchemaObject {
1339 name: name.to_owned(),
1340 kind: kind.to_owned(),
1341 table: table.to_owned(),
1342 sql: sql.map(str::to_owned),
1343 }
1344}
1345
1346fn validate_foreign_keys(connection: &Connection) -> Result<(), LoadError> {
1347 let mut statement = validate_sql(
1348 connection.prepare("PRAGMA foreign_key_check"),
1349 "prepare projection foreign key validation",
1350 )?;
1351 let mut rows = validate_sql(statement.query([]), "run projection foreign key validation")?;
1352 if validate_sql(rows.next(), "read projection foreign key validation")?.is_some() {
1353 return Err(LoadError::Recoverable);
1354 }
1355 Ok(())
1356}
1357
1358fn load_state(connection: &Connection, ordering: &K1TxnOrdering) -> Result<State, LoadError> {
1359 let mut state = State::default();
1360 let mut metadata = validate_sql(
1361 connection.prepare(
1362 "SELECT singleton, schema_version, last_applied_txid FROM metadata ORDER BY singleton",
1363 ),
1364 "prepare projection metadata",
1365 )?;
1366 let mut metadata_rows = validate_sql(metadata.query([]), "query projection metadata")?;
1367 let Some(row) = validate_sql(metadata_rows.next(), "read projection metadata")? else {
1368 return Err(LoadError::Recoverable);
1369 };
1370 let singleton: i64 = validate_sql(row.get(0), "decode metadata singleton")?;
1371 let version: i64 = validate_sql(row.get(1), "decode metadata version")?;
1372 let cursor: Option<Vec<u8>> = validate_sql(row.get(2), "decode metadata cursor")?;
1373 if singleton != 1
1374 || version != 1
1375 || validate_sql(metadata_rows.next(), "finish projection metadata")?.is_some()
1376 {
1377 return Err(LoadError::Recoverable);
1378 }
1379 state.cursor = match cursor {
1380 Some(bytes) => Some(TxId::from_bytes(exact_bytes::<12>(&bytes)?)),
1381 None => None,
1382 };
1383
1384 let group_count = table_count(connection, "groups")?;
1385 state
1386 .groups
1387 .try_reserve(group_count)
1388 .map_err(|error| LoadError::Fatal(format!("reserve group state: {error}")))?;
1389 let mut groups = validate_sql(
1390 connection.prepare("SELECT group_id, revision FROM groups"),
1391 "prepare stored groups",
1392 )?;
1393 let mut group_rows = validate_sql(groups.query([]), "query stored groups")?;
1394 while let Some(row) = validate_sql(group_rows.next(), "read stored group")? {
1395 let group_bytes: Vec<u8> = validate_sql(row.get(0), "decode stored group ID")?;
1396 let revision_bytes: Vec<u8> = validate_sql(row.get(1), "decode stored group revision")?;
1397 let group = GroupId::new(TxId::from_bytes(exact_bytes::<12>(&group_bytes)?));
1398 if group.sentinel().is_some() {
1399 return Err(LoadError::Recoverable);
1400 }
1401 let revision = TxId::from_bytes(exact_bytes::<12>(&revision_bytes)?);
1402 if state
1403 .groups
1404 .insert(
1405 group,
1406 GroupState {
1407 revision,
1408 users: HashMap::new(),
1409 models: HashSet::new(),
1410 owners: 0,
1411 },
1412 )
1413 .is_some()
1414 {
1415 return Err(LoadError::Recoverable);
1416 }
1417 }
1418
1419 let mut users = validate_sql(
1420 connection.prepare("SELECT group_id, user_id, role FROM user_memberships"),
1421 "prepare stored user memberships",
1422 )?;
1423 let mut user_rows = validate_sql(users.query([]), "query stored user memberships")?;
1424 while let Some(row) = validate_sql(user_rows.next(), "read stored user membership")? {
1425 let group_bytes: Vec<u8> = validate_sql(row.get(0), "decode membership group ID")?;
1426 let user_bytes: Vec<u8> = validate_sql(row.get(1), "decode membership user ID")?;
1427 let role_value: i64 = validate_sql(row.get(2), "decode membership role")?;
1428 let group = GroupId::new(TxId::from_bytes(exact_bytes::<12>(&group_bytes)?));
1429 let user = UserId::from_tx_id(TxId::from_bytes(exact_bytes::<12>(&user_bytes)?));
1430 let Some(role) = GroupRole::from_stored(role_value) else {
1431 return Err(LoadError::Recoverable);
1432 };
1433 let Some(stored) = state.groups.get_mut(&group) else {
1434 return Err(LoadError::Recoverable);
1435 };
1436 stored
1437 .users
1438 .try_reserve(1)
1439 .map_err(|error| LoadError::Fatal(format!("reserve stored users: {error}")))?;
1440 if stored.users.insert(user, role).is_some() {
1441 return Err(LoadError::Recoverable);
1442 }
1443 if role == GroupRole::Owner {
1444 stored.owners = stored
1445 .owners
1446 .checked_add(1)
1447 .ok_or_else(|| LoadError::Fatal("stored owner count overflow".to_owned()))?;
1448 }
1449 insert_reverse(
1450 &mut state.user_groups,
1451 user,
1452 group,
1453 "stored user reverse index",
1454 )?;
1455 }
1456
1457 let mut models = validate_sql(
1458 connection.prepare("SELECT group_id, model_id FROM model_memberships"),
1459 "prepare stored model memberships",
1460 )?;
1461 let mut model_rows = validate_sql(models.query([]), "query stored model memberships")?;
1462 while let Some(row) = validate_sql(model_rows.next(), "read stored model membership")? {
1463 let group_bytes: Vec<u8> = validate_sql(row.get(0), "decode model membership group ID")?;
1464 let model_bytes: Vec<u8> = validate_sql(row.get(1), "decode model membership model ID")?;
1465 let group = GroupId::new(TxId::from_bytes(exact_bytes::<12>(&group_bytes)?));
1466 let model = ModelId::from_bytes(exact_bytes::<32>(&model_bytes)?);
1467 let Some(stored) = state.groups.get_mut(&group) else {
1468 return Err(LoadError::Recoverable);
1469 };
1470 stored
1471 .models
1472 .try_reserve(1)
1473 .map_err(|error| LoadError::Fatal(format!("reserve stored models: {error}")))?;
1474 if !stored.models.insert(model) {
1475 return Err(LoadError::Recoverable);
1476 }
1477 insert_reverse(
1478 &mut state.model_groups,
1479 model,
1480 group,
1481 "stored model reverse index",
1482 )?;
1483 }
1484
1485 if state.groups.values().any(|group| group.owners == 0)
1486 || (!state.groups.is_empty() && state.cursor.is_none())
1487 {
1488 return Err(LoadError::Recoverable);
1489 }
1490 validate_canonical_ids(&state, ordering)?;
1491 Ok(state)
1492}
1493
1494fn table_count(connection: &Connection, table: &str) -> Result<usize, LoadError> {
1495 let sql = format!("SELECT COUNT(*) FROM {table}");
1496 let count: i64 = validate_sql(
1497 connection.query_row(&sql, [], |row| row.get(0)),
1498 "count projection rows",
1499 )?;
1500 usize::try_from(count).map_err(|_| LoadError::Recoverable)
1501}
1502
1503fn insert_reverse<K>(
1504 reverse: &mut HashMap<K, HashSet<GroupId>>,
1505 key: K,
1506 group: GroupId,
1507 operation: &str,
1508) -> Result<(), LoadError>
1509where
1510 K: Copy + Eq + Hash,
1511{
1512 if !reverse.contains_key(&key) {
1513 reverse
1514 .try_reserve(1)
1515 .map_err(|error| LoadError::Fatal(format!("reserve {operation}: {error}")))?;
1516 }
1517 let groups = reverse.entry(key).or_default();
1518 groups
1519 .try_reserve(1)
1520 .map_err(|error| LoadError::Fatal(format!("reserve {operation}: {error}")))?;
1521 if !groups.insert(group) {
1522 return Err(LoadError::Recoverable);
1523 }
1524 Ok(())
1525}
1526
1527fn validate_canonical_ids(state: &State, ordering: &K1TxnOrdering) -> Result<(), LoadError> {
1528 let subsystem = SubsystemId::from_str(SUBSYSTEM)
1529 .map_err(|error| LoadError::Fatal(format!("construct groups subsystem ID: {error}")))?;
1530 let mut validated = HashSet::new();
1531 validated
1532 .try_reserve(
1533 state
1534 .groups
1535 .len()
1536 .checked_mul(2)
1537 .and_then(|count| count.checked_add(1))
1538 .ok_or_else(|| LoadError::Fatal("canonical ID count overflow".to_owned()))?,
1539 )
1540 .map_err(|error| LoadError::Fatal(format!("reserve canonical ID validation: {error}")))?;
1541 for txid in state
1542 .groups
1543 .iter()
1544 .flat_map(|(group, stored)| [group.txid(), stored.revision])
1545 .chain(state.cursor)
1546 {
1547 if validated.contains(&txid) {
1548 continue;
1549 }
1550 let bytes = ordering.get_txn(txid).map_err(|error| {
1551 LoadError::Fatal(format!("validate canonical transaction {txid}: {error}"))
1552 })?;
1553 let Some(bytes) = bytes else {
1554 return Err(LoadError::Recoverable);
1555 };
1556 let transaction = Transaction::parse(&bytes).map_err(|_| LoadError::Recoverable)?;
1557 if transaction.subsystem() != subsystem {
1558 return Err(LoadError::Recoverable);
1559 }
1560 validated.insert(txid);
1561 }
1562 Ok(())
1563}
1564
1565fn exact_bytes<const N: usize>(bytes: &[u8]) -> Result<[u8; N], LoadError> {
1566 bytes.try_into().map_err(|_| LoadError::Recoverable)
1567}
1568
1569fn validate_sql<T>(result: Result<T, SqlError>, operation: &str) -> Result<T, LoadError> {
1570 result.map_err(|error| classify_validation_error(error, operation))
1571}
1572
1573fn classify_validation_error(error: SqlError, operation: &str) -> LoadError {
1574 let recoverable = match &error {
1575 SqlError::SqliteFailure(code, _)
1576 if matches!(
1577 code.code,
1578 rusqlite::ffi::ErrorCode::DatabaseCorrupt | rusqlite::ffi::ErrorCode::NotADatabase
1579 ) =>
1580 {
1581 true
1582 }
1583 SqlError::FromSqlConversionFailure(..)
1584 | SqlError::IntegralValueOutOfRange(..)
1585 | SqlError::Utf8Error(..)
1586 | SqlError::InvalidColumnType(..)
1587 | SqlError::QueryReturnedNoRows => true,
1588 _ => false,
1589 };
1590 if recoverable {
1591 LoadError::Recoverable
1592 } else {
1593 LoadError::Fatal(format!("{operation}: {error}"))
1594 }
1595}
1596
1597fn remove_database_files(database: &Path) -> Result<(), String> {
1598 for path in [
1599 sidecar_path(database, "-wal"),
1600 sidecar_path(database, "-shm"),
1601 database.to_path_buf(),
1602 ] {
1603 match fs::remove_file(&path) {
1604 Ok(()) => {}
1605 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
1606 Err(error) => {
1607 return Err(format!(
1608 "remove recoverable projection file {}: {error}",
1609 path.display()
1610 ));
1611 }
1612 }
1613 }
1614 Ok(())
1615}
1616
1617fn sidecar_path(database: &Path, suffix: &str) -> PathBuf {
1618 let mut path = database.as_os_str().to_os_string();
1619 path.push(suffix);
1620 PathBuf::from(path)
1621}
1622
1623#[cfg(test)]
1624mod internal_tests {
1625 use std::{
1626 sync::{Arc, TryLockError, mpsc},
1627 thread,
1628 time::{Duration, Instant},
1629 };
1630
1631 use kcode_k1_txn_ordering::K1TxnOrdering;
1632 use rusqlite::Connection;
1633 use tempfile::TempDir;
1634
1635 use super::{GroupAction, GroupId, Projection, TxId, UserId};
1636
1637 #[test]
1638 fn blocked_apply_does_not_block_queries() {
1639 let temporary = TempDir::new().unwrap();
1640 let ordering = K1TxnOrdering::open(&temporary.path().join("ordering")).unwrap();
1641 let projection_root = temporary.path().join("projection");
1642 let (projection, _) = Projection::open(&projection_root, &ordering).unwrap();
1643 let projection = Arc::new(projection);
1644 let blocker = Connection::open(projection_root.join("groups.sqlite3")).unwrap();
1645 blocker.execute_batch("BEGIN IMMEDIATE").unwrap();
1646 let applying = Arc::clone(&projection);
1647 let (apply_sent, apply_received) = mpsc::channel();
1648 let apply = thread::spawn(move || {
1649 apply_sent
1650 .send(applying.apply(
1651 TxId::from_bytes([1; 12]),
1652 GroupAction::Create {
1653 owner: UserId::from_tx_id(TxId::from_bytes([2; 12])),
1654 },
1655 ))
1656 .unwrap();
1657 });
1658 let waiting_started = Instant::now();
1659 loop {
1660 match projection.apply_lane.try_lock() {
1661 Err(TryLockError::WouldBlock) => break,
1662 Err(TryLockError::Poisoned(_)) => panic!("apply lane poisoned"),
1663 Ok(lane) => drop(lane),
1664 }
1665 assert!(waiting_started.elapsed() < Duration::from_secs(1));
1666 thread::yield_now();
1667 }
1668 assert!(matches!(
1669 apply_received.try_recv(),
1670 Err(mpsc::TryRecvError::Empty)
1671 ));
1672 let querying = Arc::clone(&projection);
1673 let (query_sent, query_received) = mpsc::channel();
1674 let query = thread::spawn(move || {
1675 query_sent
1676 .send(querying.get(GroupId::new(TxId::from_bytes([1; 12]))))
1677 .unwrap();
1678 });
1679 let query_result = query_received.recv_timeout(Duration::from_secs(1));
1680 blocker.execute_batch("ROLLBACK").unwrap();
1681 assert!(query_result.unwrap().unwrap().is_none());
1682 assert!(
1683 apply_received
1684 .recv_timeout(Duration::from_secs(1))
1685 .unwrap()
1686 .is_ok()
1687 );
1688 query.join().unwrap();
1689 apply.join().unwrap();
1690 }
1691}