1use rusqlite::{Connection, OpenFlags, params};
2use std::collections::HashMap;
3use std::path::Path;
4
5use crate::error::{FossilError, Result};
6
7pub struct Database {
8 conn: Connection,
9}
10
11impl Database {
12 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
14 let conn = Connection::open_with_flags(
15 path,
16 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
17 )?;
18
19 let is_fossil: bool = conn.query_row(
21 "SELECT COUNT(*) > 0 FROM sqlite_master WHERE name='blob'",
22 [],
23 |row| row.get(0),
24 )?;
25
26 if !is_fossil {
27 return Err(FossilError::NotARepository);
28 }
29
30 Ok(Self { conn })
31 }
32
33 pub fn open_rw<P: AsRef<Path>>(path: P) -> Result<Self> {
35 let conn = Connection::open_with_flags(
36 path,
37 OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX,
38 )?;
39 Ok(Self { conn })
40 }
41
42 pub fn get_blob_raw(&self, hash: &str) -> Result<(i64, Vec<u8>)> {
44 let result: std::result::Result<(i64, Vec<u8>), _> = self.conn.query_row(
46 "SELECT rid, content FROM blob WHERE uuid = ?1",
47 params![hash],
48 |row| Ok((row.get(0)?, row.get(1)?)),
49 );
50
51 if let Ok(r) = result {
52 return Ok(r);
53 }
54
55 let mut stmt = self
57 .conn
58 .prepare("SELECT rid, content, uuid FROM blob WHERE uuid LIKE ?1 || '%'")?;
59 let mut rows = stmt.query(params![hash])?;
60
61 let first = rows
62 .next()?
63 .ok_or_else(|| FossilError::ArtifactNotFound(hash.to_string()))?;
64 let rid: i64 = first.get(0)?;
65 let content: Vec<u8> = first.get(1)?;
66
67 if rows.next()?.is_some() {
69 return Err(FossilError::AmbiguousHash(hash.to_string()));
70 }
71
72 Ok((rid, content))
73 }
74
75 pub fn get_blob_for_sync(&self, hash: &str) -> Result<(Vec<u8>, i64, Option<String>)> {
78 let (rid, content) = self.get_blob_raw(hash)?;
79
80 let size: i64 = self.conn.query_row(
82 "SELECT size FROM blob WHERE rid = ?1",
83 params![rid],
84 |row| row.get(0),
85 )?;
86
87 let delta_source: Option<String> = if let Some(src_rid) = self.get_delta_source(rid)? {
89 Some(self.get_hash_by_rid(src_rid)?)
90 } else {
91 None
92 };
93
94 Ok((content, size, delta_source))
95 }
96
97 pub fn insert_raw_blob(&self, content: &[u8], hash: &str, size: i64) -> Result<i64> {
99 if let Ok(rid) = self.get_rid_by_hash(hash) {
101 return Ok(rid);
102 }
103
104 self.conn.execute(
105 "INSERT INTO blob(uuid, size, content) VALUES (?1, ?2, ?3)",
106 params![hash, size, content],
107 )?;
108
109 Ok(self.conn.last_insert_rowid())
110 }
111
112 pub fn insert_delta(&self, rid: i64, src_rid: i64) -> Result<()> {
114 self.conn.execute(
115 "INSERT OR REPLACE INTO delta(rid, srcid) VALUES (?1, ?2)",
116 params![rid, src_rid],
117 )?;
118 Ok(())
119 }
120
121 pub fn get_blob_by_rid(&self, rid: i64) -> Result<Vec<u8>> {
123 let content: Vec<u8> = self
124 .conn
125 .query_row(
126 "SELECT content FROM blob WHERE rid = ?1",
127 params![rid],
128 |row| row.get(0),
129 )
130 .map_err(|_| FossilError::ArtifactNotFound(rid.to_string()))?;
131
132 Ok(content)
133 }
134
135 pub fn get_delta_source(&self, rid: i64) -> Result<Option<i64>> {
137 let result: std::result::Result<i64, _> = self.conn.query_row(
138 "SELECT srcid FROM delta WHERE rid = ?1",
139 params![rid],
140 |row| row.get(0),
141 );
142
143 Ok(result.ok())
144 }
145
146 pub fn get_hash_by_rid(&self, rid: i64) -> Result<String> {
148 let hash: String = self
149 .conn
150 .query_row(
151 "SELECT uuid FROM blob WHERE rid = ?1",
152 params![rid],
153 |row| row.get(0),
154 )
155 .map_err(|_| FossilError::ArtifactNotFound(rid.to_string()))?;
156
157 Ok(hash)
158 }
159
160 pub fn get_rid_by_hash(&self, hash: &str) -> Result<i64> {
162 let result: std::result::Result<i64, _> = self.conn.query_row(
164 "SELECT rid FROM blob WHERE uuid = ?1",
165 params![hash],
166 |row| row.get(0),
167 );
168
169 if let Ok(rid) = result {
170 return Ok(rid);
171 }
172
173 let mut stmt = self
175 .conn
176 .prepare("SELECT rid FROM blob WHERE uuid LIKE ?1 || '%'")?;
177 let mut rows = stmt.query(params![hash])?;
178
179 let first = rows
180 .next()?
181 .ok_or_else(|| FossilError::ArtifactNotFound(hash.to_string()))?;
182 let rid: i64 = first.get(0)?;
183
184 if rows.next()?.is_some() {
186 return Err(FossilError::AmbiguousHash(hash.to_string()));
187 }
188
189 Ok(rid)
190 }
191
192 pub fn get_project_code(&self) -> Result<String> {
194 let code: String = self.conn.query_row(
195 "SELECT value FROM config WHERE name = 'project-code'",
196 [],
197 |row| row.get(0),
198 )?;
199 Ok(code)
200 }
201
202 pub fn get_project_name(&self) -> Result<Option<String>> {
204 let result: std::result::Result<String, _> = self.conn.query_row(
205 "SELECT value FROM config WHERE name = 'project-name'",
206 [],
207 |row| row.get(0),
208 );
209 Ok(result.ok())
210 }
211
212 pub fn get_trunk_tip(&self) -> Result<i64> {
214 let rid: i64 = self.conn.query_row(
217 r#"SELECT event.objid FROM event
218 JOIN tagxref ON tagxref.rid = event.objid
219 JOIN tag ON tag.tagid = tagxref.tagid
220 WHERE tag.tagname = 'sym-trunk'
221 AND event.type = 'ci'
222 ORDER BY event.mtime DESC, event.objid DESC
223 LIMIT 1"#,
224 [],
225 |row| row.get(0),
226 )?;
227 Ok(rid)
228 }
229
230 pub fn get_branch_tip(&self, branch: &str) -> Result<i64> {
232 let tag_name = format!("sym-{}", branch);
233 let rid: i64 = self.conn.query_row(
235 r#"SELECT event.objid FROM event
236 JOIN tagxref ON tagxref.rid = event.objid
237 JOIN tag ON tag.tagid = tagxref.tagid
238 WHERE tag.tagname = ?1
239 AND event.type = 'ci'
240 ORDER BY event.mtime DESC, event.objid DESC
241 LIMIT 1"#,
242 params![tag_name],
243 |row| row.get(0),
244 )?;
245 Ok(rid)
246 }
247
248 pub fn list_branches(&self) -> Result<Vec<String>> {
250 let mut stmt = self.conn.prepare(
251 r#"SELECT DISTINCT substr(tagname, 5) FROM tag
252 WHERE tagname LIKE 'sym-%'
253 ORDER BY tagname"#,
254 )?;
255
256 let branches = stmt
257 .query_map([], |row| row.get(0))?
258 .filter_map(|r| r.ok())
259 .collect();
260
261 Ok(branches)
262 }
263
264 pub fn get_recent_checkins(
266 &self,
267 limit: usize,
268 ) -> Result<Vec<(i64, String, f64, String, String)>> {
269 let mut stmt = self.conn.prepare(
270 r#"SELECT objid,
271 (SELECT uuid FROM blob WHERE rid = objid),
272 mtime, user, comment
273 FROM event
274 WHERE type = 'ci'
275 ORDER BY mtime DESC
276 LIMIT ?1"#,
277 )?;
278
279 let checkins = stmt
280 .query_map(params![limit as i64], |row| {
281 Ok((
282 row.get::<_, i64>(0)?,
283 row.get::<_, String>(1)?,
284 row.get::<_, f64>(2)?,
285 row.get::<_, String>(3)?,
286 row.get::<_, String>(4)?,
287 ))
288 })?
289 .filter_map(|r| r.ok())
290 .collect();
291
292 Ok(checkins)
293 }
294
295 pub fn connection(&self) -> &Connection {
296 &self.conn
297 }
298
299 pub fn init<P: AsRef<Path>>(path: P) -> Result<Self> {
301 let conn = Connection::open(&path)?;
302
303 conn.execute_batch(
305 r#"
306 CREATE TABLE IF NOT EXISTS blob(
307 rid INTEGER PRIMARY KEY,
308 rcvid INTEGER,
309 size INTEGER,
310 uuid TEXT UNIQUE NOT NULL,
311 content BLOB,
312 CHECK( length(uuid)>=40 AND rid>0 )
313 );
314 CREATE TABLE IF NOT EXISTS delta(
315 rid INTEGER PRIMARY KEY,
316 srcid INTEGER NOT NULL REFERENCES blob
317 );
318 CREATE TABLE IF NOT EXISTS config(
319 name TEXT PRIMARY KEY NOT NULL,
320 value CLOB,
321 mtime DATE
322 );
323 CREATE TABLE IF NOT EXISTS event(
324 type TEXT,
325 mtime DATETIME,
326 objid INTEGER PRIMARY KEY,
327 tagid INTEGER,
328 uid INTEGER,
329 bgcolor TEXT,
330 euser TEXT,
331 user TEXT,
332 ecomment TEXT,
333 comment TEXT,
334 brief TEXT,
335 omtime DATETIME
336 );
337 CREATE INDEX IF NOT EXISTS event_i1 ON event(mtime);
338 CREATE TABLE IF NOT EXISTS tag(
339 tagid INTEGER PRIMARY KEY,
340 tagname TEXT UNIQUE
341 );
342 CREATE TABLE IF NOT EXISTS tagxref(
343 tagid INTEGER REFERENCES tag,
344 tagtype INTEGER,
345 srcid INTEGER REFERENCES blob,
346 origid INTEGER REFERENCES blob,
347 value TEXT,
348 mtime TIMESTAMP,
349 rid INTEGER REFERENCES blob,
350 UNIQUE(rid, tagid)
351 );
352 CREATE INDEX IF NOT EXISTS tagxref_i1 ON tagxref(tagid, mtime);
353 CREATE TABLE IF NOT EXISTS plink(
354 pid INTEGER REFERENCES blob,
355 cid INTEGER REFERENCES blob,
356 isprim BOOLEAN,
357 mtime DATETIME,
358 baseid INTEGER REFERENCES blob,
359 UNIQUE(pid, cid)
360 );
361 CREATE INDEX IF NOT EXISTS plink_i2 ON plink(cid,pid);
362 CREATE TABLE IF NOT EXISTS filename(
363 fnid INTEGER PRIMARY KEY,
364 name TEXT UNIQUE
365 );
366 CREATE TABLE IF NOT EXISTS mlink(
367 mid INTEGER,
368 fid INTEGER,
369 pmid INTEGER,
370 pid INTEGER,
371 fnid INTEGER REFERENCES filename,
372 pfnid INTEGER,
373 mperm INTEGER,
374 isaux BOOLEAN DEFAULT 0
375 );
376 CREATE INDEX IF NOT EXISTS mlink_i1 ON mlink(mid);
377 CREATE INDEX IF NOT EXISTS mlink_i2 ON mlink(fnid);
378 CREATE INDEX IF NOT EXISTS mlink_i3 ON mlink(fid);
379 CREATE INDEX IF NOT EXISTS mlink_i4 ON mlink(pid);
380 CREATE INDEX IF NOT EXISTS blob_uuid ON blob(uuid);
381 CREATE TABLE IF NOT EXISTS rcvfrom(
382 rcvid INTEGER PRIMARY KEY,
383 uid INTEGER,
384 mtime DATETIME,
385 nonce TEXT UNIQUE,
386 ipaddr TEXT
387 );
388 "#,
389 )?;
390
391 let project_code = Self::generate_hash(uuid::Uuid::new_v4().to_string().as_bytes());
393 let server_code = Self::generate_hash(uuid::Uuid::new_v4().to_string().as_bytes());
394
395 conn.execute(
397 "INSERT INTO config(name, value, mtime) VALUES ('project-code', ?1, julianday('now'))",
398 params![project_code],
399 )?;
400 conn.execute(
401 "INSERT INTO config(name, value, mtime) VALUES ('server-code', ?1, julianday('now'))",
402 params![server_code],
403 )?;
404 conn.execute(
405 "INSERT INTO config(name, value, mtime) VALUES ('content-schema', '2', julianday('now'))",
406 [],
407 )?;
408 conn.execute(
409 "INSERT INTO config(name, value, mtime) VALUES ('aux-schema', '2015-01-24', julianday('now'))",
410 [],
411 )?;
412 conn.execute(
413 "INSERT INTO config(name, value, mtime) VALUES ('hash-policy', '2', julianday('now'))",
414 [],
415 )?;
416
417 conn.execute_batch(
419 r#"
420 CREATE TABLE IF NOT EXISTS user(
421 uid INTEGER PRIMARY KEY,
422 login TEXT UNIQUE,
423 pw TEXT,
424 cap TEXT,
425 cookie TEXT,
426 ipaddr TEXT,
427 cexpire DATETIME,
428 info TEXT,
429 mtime DATE,
430 photo BLOB
431 );
432 CREATE TABLE IF NOT EXISTS leaf(
433 rid INTEGER PRIMARY KEY
434 );
435 CREATE TABLE IF NOT EXISTS phantom(
436 rid INTEGER PRIMARY KEY
437 );
438 CREATE TABLE IF NOT EXISTS orphan(
439 rid INTEGER PRIMARY KEY,
440 baseline INTEGER
441 );
442 CREATE TABLE IF NOT EXISTS unclustered(
443 rid INTEGER PRIMARY KEY
444 );
445 CREATE TABLE IF NOT EXISTS unsent(
446 rid INTEGER PRIMARY KEY
447 );
448 CREATE TABLE IF NOT EXISTS private(
449 rid INTEGER PRIMARY KEY
450 );
451 CREATE TABLE IF NOT EXISTS shun(
452 uuid UNIQUE,
453 mtime DATE,
454 scom TEXT
455 );
456 CREATE TABLE IF NOT EXISTS reportfmt(
457 rn INTEGER PRIMARY KEY,
458 owner TEXT,
459 title TEXT UNIQUE,
460 mtime DATE,
461 cols TEXT,
462 sqlcode TEXT
463 );
464 CREATE TABLE IF NOT EXISTS concealed(
465 hash TEXT PRIMARY KEY,
466 content TEXT
467 );
468 CREATE TABLE IF NOT EXISTS cherrypick(
469 parentid INT,
470 childid INT,
471 isExclude BOOLEAN DEFAULT false,
472 PRIMARY KEY(parentid, childid)
473 ) WITHOUT ROWID;
474 CREATE TABLE IF NOT EXISTS backlink(
475 target TEXT,
476 srctype INT,
477 srcid INT,
478 mtime TIMESTAMP,
479 UNIQUE(target, srctype, srcid)
480 );
481 CREATE TABLE IF NOT EXISTS attachment(
482 attachid INTEGER PRIMARY KEY,
483 isLatest BOOLEAN DEFAULT 0,
484 mtime TIMESTAMP,
485 src TEXT,
486 target TEXT,
487 filename TEXT,
488 comment TEXT,
489 user TEXT
490 );
491 CREATE TABLE IF NOT EXISTS ticket(tkt_id INTEGER PRIMARY KEY, tkt_uuid TEXT UNIQUE, tkt_mtime DATE, tkt_ctime DATE);
492 CREATE TABLE IF NOT EXISTS ticketchng(tkt_id INTEGER PRIMARY KEY, tkt_rid INT, tkt_mtime DATE, tkt_user TEXT);
493 "#,
494 )?;
495
496 let db = Self { conn };
497
498 db.create_user("anonymous", "", "cgjorz")?;
505
506 db.create_user("nobody", "", "")?;
508
509 db.create_user("developer", "", "deghijkmnorstvwz")?;
511
512 Ok(db)
513 }
514
515 pub fn generate_hash(data: &[u8]) -> String {
517 use sha3::{Digest, Sha3_256};
518 let mut hasher = Sha3_256::new();
519 hasher.update(data);
520 hex::encode(hasher.finalize())
521 }
522
523 pub fn insert_blob(&self, content: &[u8], hash: &str, size: i64) -> Result<i64> {
525 self.conn.execute(
527 "INSERT OR IGNORE INTO blob(uuid, size, content) VALUES (?1, ?2, ?3)",
528 params![hash, size, content],
529 )?;
530
531 let rid: i64 = self.conn.query_row(
533 "SELECT rid FROM blob WHERE uuid = ?1",
534 params![hash],
535 |row| row.get(0),
536 )?;
537
538 Ok(rid)
539 }
540
541 pub fn insert_blobs(&self, blobs: &[(&[u8], &str, i64)]) -> Result<HashMap<String, i64>> {
544 {
546 let mut insert_stmt = self.conn.prepare_cached(
547 "INSERT OR IGNORE INTO blob(uuid, size, content) VALUES (?1, ?2, ?3)",
548 )?;
549 for (content, hash, size) in blobs {
550 insert_stmt.execute(params![hash, size, content])?;
551 }
552 }
553
554 let mut result = HashMap::new();
556 {
557 let mut select_stmt = self
558 .conn
559 .prepare_cached("SELECT rid FROM blob WHERE uuid = ?1")?;
560 for (_, hash, _) in blobs {
561 let rid: i64 = select_stmt.query_row(params![hash], |row| row.get(0))?;
562 result.insert(hash.to_string(), rid);
563 }
564 }
565
566 Ok(result)
567 }
568
569 pub fn get_or_create_filename(&self, name: &str) -> Result<i64> {
571 self.conn.execute(
573 "INSERT OR IGNORE INTO filename(name) VALUES (?1)",
574 params![name],
575 )?;
576
577 let fnid: i64 = self.conn.query_row(
578 "SELECT fnid FROM filename WHERE name = ?1",
579 params![name],
580 |row| row.get(0),
581 )?;
582
583 Ok(fnid)
584 }
585
586 pub fn get_or_create_filenames(&self, names: &[&str]) -> Result<HashMap<String, i64>> {
589 {
591 let mut insert_stmt = self
592 .conn
593 .prepare_cached("INSERT OR IGNORE INTO filename(name) VALUES (?1)")?;
594 for name in names {
595 insert_stmt.execute(params![name])?;
596 }
597 }
598
599 let mut result = HashMap::new();
601 {
602 let mut select_stmt = self
603 .conn
604 .prepare_cached("SELECT fnid FROM filename WHERE name = ?1")?;
605 for name in names {
606 let fnid: i64 = select_stmt.query_row(params![name], |row| row.get(0))?;
607 result.insert(name.to_string(), fnid);
608 }
609 }
610
611 Ok(result)
612 }
613
614 pub fn get_or_create_tag(&self, tagname: &str) -> Result<i64> {
616 let existing: std::result::Result<i64, _> = self.conn.query_row(
617 "SELECT tagid FROM tag WHERE tagname = ?1",
618 params![tagname],
619 |row| row.get(0),
620 );
621
622 if let Ok(tagid) = existing {
623 return Ok(tagid);
624 }
625
626 self.conn
627 .execute("INSERT INTO tag(tagname) VALUES (?1)", params![tagname])?;
628
629 Ok(self.conn.last_insert_rowid())
630 }
631
632 pub fn insert_event(
634 &self,
635 event_type: &str,
636 objid: i64,
637 mtime: f64,
638 user: &str,
639 comment: &str,
640 ) -> Result<()> {
641 self.conn.execute(
642 "INSERT INTO event(type, objid, mtime, user, comment) VALUES (?1, ?2, ?3, ?4, ?5)",
643 params![event_type, objid, mtime, user, comment],
644 )?;
645 Ok(())
646 }
647
648 pub fn insert_tagxref(
650 &self,
651 tagid: i64,
652 tagtype: i64,
653 rid: i64,
654 mtime: f64,
655 value: Option<&str>,
656 ) -> Result<()> {
657 self.conn.execute(
658 "INSERT OR REPLACE INTO tagxref(tagid, tagtype, rid, mtime, value, srcid, origid)
659 VALUES (?1, ?2, ?3, ?4, ?5, ?3, ?3)",
660 params![tagid, tagtype, rid, mtime, value],
661 )?;
662 Ok(())
663 }
664
665 pub fn insert_leaf(&self, rid: i64) -> Result<()> {
667 self.conn
668 .execute("INSERT OR IGNORE INTO leaf(rid) VALUES (?1)", params![rid])?;
669 Ok(())
670 }
671
672 pub fn insert_plink(&self, parent_rid: i64, child_rid: i64, mtime: f64) -> Result<()> {
674 self.conn.execute(
675 "INSERT OR IGNORE INTO plink(pid, cid, isprim, mtime) VALUES (?1, ?2, 1, ?3)",
676 params![parent_rid, child_rid, mtime],
677 )?;
678
679 self.conn
681 .execute("DELETE FROM leaf WHERE rid = ?1", params![parent_rid])?;
682
683 self.conn.execute(
685 "INSERT OR IGNORE INTO leaf(rid) VALUES (?1)",
686 params![child_rid],
687 )?;
688
689 Ok(())
690 }
691
692 pub fn get_files_for_manifest(&self, manifest_rid: i64) -> Result<Vec<(String, String)>> {
695 let mut stmt = self.conn.prepare(
696 r#"SELECT f.name, b.uuid
697 FROM mlink m
698 JOIN filename f ON f.fnid = m.fnid
699 JOIN blob b ON b.rid = m.fid
700 WHERE m.mid = ?1 AND m.fid > 0
701 ORDER BY f.name"#,
702 )?;
703
704 let files = stmt
705 .query_map(params![manifest_rid], |row| {
706 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
707 })?
708 .filter_map(|r| r.ok())
709 .collect();
710
711 Ok(files)
712 }
713
714 pub fn get_file_hash_from_manifest(&self, manifest_rid: i64, filename: &str) -> Result<String> {
716 let hash: String = self
717 .conn
718 .query_row(
719 r#"SELECT b.uuid
720 FROM mlink m
721 JOIN filename f ON f.fnid = m.fnid
722 JOIN blob b ON b.rid = m.fid
723 WHERE m.mid = ?1 AND f.name = ?2 AND m.fid > 0"#,
724 params![manifest_rid, filename],
725 |row| row.get(0),
726 )
727 .map_err(|_| FossilError::ArtifactNotFound(format!("file: {}", filename)))?;
728
729 Ok(hash)
730 }
731
732 pub fn insert_mlink(
734 &self,
735 manifest_rid: i64,
736 file_rid: i64,
737 filename_id: i64,
738 parent_file_rid: Option<i64>,
739 ) -> Result<()> {
740 self.conn.execute(
741 "INSERT INTO mlink(mid, fid, fnid, pid) VALUES (?1, ?2, ?3, ?4)",
742 params![manifest_rid, file_rid, filename_id, parent_file_rid],
743 )?;
744 Ok(())
745 }
746
747 pub fn insert_mlinks(
749 &self,
750 manifest_rid: i64,
751 entries: &[(i64, i64)], ) -> Result<()> {
753 let mut stmt = self
754 .conn
755 .prepare_cached("INSERT INTO mlink(mid, fid, fnid) VALUES (?1, ?2, ?3)")?;
756 for (file_rid, filename_id) in entries {
757 stmt.execute(params![manifest_rid, file_rid, filename_id])?;
758 }
759 Ok(())
760 }
761
762 pub fn create_user(&self, login: &str, password: &str, capabilities: &str) -> Result<()> {
764 self.conn.execute(
765 "INSERT OR REPLACE INTO user(login, pw, cap, mtime) VALUES (?1, ?2, ?3, julianday('now'))",
766 params![login, password, capabilities],
767 )?;
768 Ok(())
769 }
770
771 pub fn set_user_capabilities(&self, login: &str, capabilities: &str) -> Result<()> {
773 self.conn.execute(
774 "UPDATE user SET cap = ?2, mtime = julianday('now') WHERE login = ?1",
775 params![login, capabilities],
776 )?;
777 Ok(())
778 }
779
780 pub fn get_user_capabilities(&self, login: &str) -> Result<Option<String>> {
782 let result: std::result::Result<String, _> = self.conn.query_row(
783 "SELECT cap FROM user WHERE login = ?1",
784 params![login],
785 |row| row.get(0),
786 );
787 Ok(result.ok())
788 }
789
790 pub fn list_users(&self) -> Result<Vec<(String, String)>> {
792 let mut stmt = self
793 .conn
794 .prepare("SELECT login, cap FROM user ORDER BY login")?;
795 let users = stmt
796 .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
797 .filter_map(|r| r.ok())
798 .collect();
799 Ok(users)
800 }
801
802 pub fn begin_transaction(&self) -> Result<()> {
804 self.conn.execute("BEGIN IMMEDIATE", [])?;
805 Ok(())
806 }
807
808 pub fn commit_transaction(&self) -> Result<()> {
810 self.conn.execute("COMMIT", [])?;
811 Ok(())
812 }
813
814 pub fn rollback_transaction(&self) -> Result<()> {
816 self.conn.execute("ROLLBACK", [])?;
817 Ok(())
818 }
819}