This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-migrate / src / source.rs
11 kB 317 lines
1use std::collections::BTreeSet; 2use std::path::Path; 3 4use rusqlite::{Connection, OpenFlags, OptionalExtension, Row}; 5 6#[derive(Debug, thiserror::Error)] 7pub enum SourceError { 8 #[error("source database query failed: {0}")] 9 Sqlite(#[from] rusqlite::Error), 10 #[error( 11 "source database predates DID-keyed repos. Upgrade tangled-knot to its latest release, let it finish its own migrations, then run knot-migrate again." 12 )] 13 PreDid, 14 #[error("source database has no acl table for the casbin cross-check")] 15 MissingAcl, 16 #[error( 17 "source database has a collaborators table but no knot_members table. Upgrade tangled-knot to its latest release, let it finish its own migrations, then run knot-migrate again." 18 )] 19 CollaboratorsWithoutMembers, 20 #[error( 21 "source table `{table}` is missing expected columns {}. This tangled-knot predates the schema knot-migrate reads. Upgrade tangled-knot to its latest release, let it finish its own migrations, then run knot-migrate again.", 22 .missing.join(", ") 23 )] 24 SchemaMismatch { table: String, missing: Vec<String> }, 25} 26 27knot_types::text_newtype! { 28 pub struct SourceRepoDid(String) => verbatim as from_column; 29 pub struct SourceDid(String) => verbatim as from_column; 30 pub struct SourceRkey(String) => verbatim as from_column; 31 pub struct SourceRepoName(String) => verbatim as from_column; 32 pub struct SourceRepoObject(String) => verbatim as from_column; 33 pub struct SourceTimestamp(String) => verbatim as from_column; 34} 35 36#[derive(Debug, Clone, PartialEq, Eq)] 37pub enum SourceKeyType { 38 K256, 39 Other(String), 40} 41 42impl SourceKeyType { 43 pub fn from_column(value: impl Into<String>) -> Self { 44 let value = value.into(); 45 match value.as_str() { 46 "k256" => Self::K256, 47 _ => Self::Other(value), 48 } 49 } 50 51 pub fn is_k256(&self) -> bool { 52 matches!(self, Self::K256) 53 } 54} 55 56impl ::std::fmt::Display for SourceKeyType { 57 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { 58 match self { 59 Self::K256 => f.write_str("k256"), 60 Self::Other(value) => f.write_str(value), 61 } 62 } 63} 64 65#[derive(Debug, Clone, Copy, PartialEq, Eq)] 66pub enum SourceSchema { 67 Tables, 68 PreFlip, 69} 70 71#[derive(Clone, PartialEq, Eq, zeroize::Zeroize, zeroize::ZeroizeOnDrop)] 72// Every repo's private key, straight outta the old knot's db. 73// `Debug` doesn't prints any bytes on purpose, 74// as to not compromise a migration. 75pub struct SourceSigningKey(Vec<u8>); 76 77impl SourceSigningKey { 78 pub fn from_column(bytes: Vec<u8>) -> Self { 79 Self(bytes) 80 } 81 82 pub fn as_bytes(&self) -> &[u8] { 83 &self.0 84 } 85} 86 87impl std::fmt::Debug for SourceSigningKey { 88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 89 f.debug_tuple("SourceSigningKey").finish_non_exhaustive() 90 } 91} 92 93#[derive(Debug, Clone, PartialEq, Eq)] 94pub struct RepoRow { 95 pub repo_did: SourceRepoDid, 96 pub owner_did: SourceDid, 97 pub repo_name: SourceRepoName, 98 pub signing_key: SourceSigningKey, 99 pub key_type: SourceKeyType, 100 pub created_at: SourceTimestamp, 101} 102 103#[derive(Debug, Clone, PartialEq, Eq)] 104pub struct MemberRow { 105 pub did: SourceDid, 106 pub subject: SourceDid, 107 pub created: SourceTimestamp, 108} 109 110#[derive(Debug, Clone, PartialEq, Eq)] 111pub struct CollabRow { 112 pub repo_did: SourceRepoDid, 113 pub subject_did: SourceDid, 114 pub added_by_did: SourceDid, 115 pub created: SourceTimestamp, 116} 117 118#[derive(Debug, Clone, PartialEq, Eq)] 119pub struct AclRow { 120 pub p_type: String, 121 pub v0: String, 122 pub v1: String, 123 pub v2: String, 124 pub v3: String, 125} 126 127pub struct SourceDb { 128 conn: Connection, 129} 130 131impl SourceDb { 132 pub fn open(path: &Path) -> Result<Self, SourceError> { 133 let conn = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY)?; 134 Ok(Self { conn }) 135 } 136 137 pub fn schema(&self) -> Result<SourceSchema, SourceError> { 138 let variant = match ( 139 self.has_table("repo_keys")?, 140 self.has_table("repo_aliases")?, 141 self.has_table("knot_members")?, 142 self.has_table("collaborators")?, 143 ) { 144 (true, true, true, true) => SourceSchema::Tables, 145 (true, true, false, true) => return Err(SourceError::CollaboratorsWithoutMembers), 146 (true, true, _, false) => SourceSchema::PreFlip, 147 _ => return Err(SourceError::PreDid), 148 }; 149 let checks: Vec<(&str, &[&str])> = [ 150 Some(( 151 "repo_keys", 152 &[ 153 "repo_did", 154 "owner_did", 155 "repo_name", 156 "signing_key", 157 "key_type", 158 "created_at", 159 ][..], 160 )), 161 Some(("repo_aliases", &["rkey", "repo_did", "rev"][..])), 162 self.has_table("acl")? 163 .then_some(("acl", &["p_type", "v0", "v1", "v2", "v3"][..])), 164 self.has_table("knot_members")? 165 .then_some(("knot_members", &["id", "did", "subject", "created"][..])), 166 (variant == SourceSchema::Tables).then_some(( 167 "collaborators", 168 &["id", "repo_did", "subject_did", "added_by_did", "created"][..], 169 )), 170 ] 171 .into_iter() 172 .flatten() 173 .collect(); 174 checks 175 .into_iter() 176 .try_for_each(|(table, columns)| self.require_columns(table, columns))?; 177 Ok(variant) 178 } 179 180 fn has_table(&self, name: &str) -> Result<bool, SourceError> { 181 let count: i64 = self.conn.query_row( 182 "select count(*) from sqlite_master where type = 'table' and name = ?1", 183 [name], 184 |row| row.get(0), 185 )?; 186 Ok(count > 0) 187 } 188 189 fn require_columns(&self, table: &str, required: &[&str]) -> Result<(), SourceError> { 190 let present: BTreeSet<String> = self 191 .conn 192 .prepare("select name from pragma_table_info(?1)")? 193 .query_map([table], |row| row.get::<_, String>(0))? 194 .collect::<rusqlite::Result<_>>()?; 195 let missing: Vec<String> = required 196 .iter() 197 .filter(|column| !present.contains(**column)) 198 .map(|column| (*column).to_string()) 199 .collect(); 200 missing 201 .is_empty() 202 .then_some(()) 203 .ok_or(SourceError::SchemaMismatch { 204 table: table.to_string(), 205 missing, 206 }) 207 } 208 209 pub fn repos(&self) -> Result<Vec<RepoRow>, SourceError> { 210 self.collect( 211 "select repo_did, owner_did, repo_name, signing_key, key_type, created_at 212 from repo_keys order by created_at, repo_did", 213 |row| { 214 Ok(RepoRow { 215 repo_did: SourceRepoDid::from_column(row.get::<_, String>(0)?), 216 owner_did: SourceDid::from_column(row.get::<_, String>(1)?), 217 repo_name: SourceRepoName::from_column(row.get::<_, String>(2)?), 218 signing_key: SourceSigningKey::from_column(row.get(3)?), 219 key_type: SourceKeyType::from_column(row.get::<_, String>(4)?), 220 created_at: SourceTimestamp::from_column(row.get::<_, String>(5)?), 221 }) 222 }, 223 ) 224 } 225 226 pub fn members(&self) -> Result<Vec<MemberRow>, SourceError> { 227 if !self.has_table("knot_members")? { 228 return Ok(Vec::new()); 229 } 230 self.collect( 231 "select did, subject, created from knot_members 232 where id in (select min(id) from knot_members group by subject) 233 order by id", 234 |row| { 235 Ok(MemberRow { 236 did: SourceDid::from_column(row.get::<_, String>(0)?), 237 subject: SourceDid::from_column(row.get::<_, String>(1)?), 238 created: SourceTimestamp::from_column(row.get::<_, String>(2)?), 239 }) 240 }, 241 ) 242 } 243 244 pub fn collaborators(&self) -> Result<Vec<CollabRow>, SourceError> { 245 if !self.has_table("collaborators")? { 246 return Ok(Vec::new()); 247 } 248 self.collect( 249 "select repo_did, subject_did, added_by_did, created from collaborators order by id", 250 |row| { 251 Ok(CollabRow { 252 repo_did: SourceRepoDid::from_column(row.get::<_, String>(0)?), 253 subject_did: SourceDid::from_column(row.get::<_, String>(1)?), 254 added_by_did: SourceDid::from_column(row.get::<_, String>(2)?), 255 created: SourceTimestamp::from_column(row.get::<_, String>(3)?), 256 }) 257 }, 258 ) 259 } 260 261 pub fn acl(&self) -> Result<Vec<AclRow>, SourceError> { 262 if !self.has_table("acl")? { 263 return Err(SourceError::MissingAcl); 264 } 265 self.collect( 266 "select p_type, v0, v1, v2, v3 from acl order by rowid", 267 |row| { 268 Ok(AclRow { 269 p_type: row.get(0)?, 270 v0: row.get(1)?, 271 v1: row.get(2)?, 272 v2: row.get(3)?, 273 v3: row.get(4)?, 274 }) 275 }, 276 ) 277 } 278 279 pub fn current_rkey( 280 &self, 281 repo_did: &SourceRepoDid, 282 ) -> Result<Option<SourceRkey>, SourceError> { 283 self.conn 284 .query_row( 285 "select rkey from repo_aliases 286 where repo_did = ? 287 order by rev desc 288 limit 1", 289 [repo_did.as_str()], 290 |row| row.get::<_, String>(0).map(SourceRkey::from_column), 291 ) 292 .optional() 293 .map_err(Into::into) 294 } 295 296 pub fn orphan_alias_count(&self) -> Result<u64, SourceError> { 297 let count: i64 = self.conn.query_row( 298 "select count(*) from repo_aliases ra 299 where not exists (select 1 from repo_keys rk where rk.repo_did = ra.repo_did)", 300 [], 301 |row| row.get(0), 302 )?; 303 Ok(count as u64) 304 } 305 306 fn collect<T>( 307 &self, 308 sql: &str, 309 map: impl Fn(&Row<'_>) -> rusqlite::Result<T>, 310 ) -> Result<Vec<T>, SourceError> { 311 let mut statement = self.conn.prepare(sql)?; 312 let rows = statement 313 .query_map([], map)? 314 .collect::<rusqlite::Result<Vec<T>>>()?; 315 Ok(rows) 316 } 317}