1use crate::{sqlite::DatabaseConnection, DbError};
9use coven_foundation::id_source::FileId;
10
11#[derive(
13 Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
14)]
15pub struct MoveId(pub i64);
16
17impl rusqlite::ToSql for MoveId {
20 fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
21 Ok(self.0.into())
22 }
23}
24
25impl rusqlite::types::FromSql for MoveId {
26 fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
27 i64::column_result(value).map(Self)
28 }
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub(crate) enum MoveState {
34 Moving,
36 Failed,
40 Cancelled,
42 Finished,
44}
45
46impl MoveState {
47 fn text(self) -> &'static str {
48 match self {
49 Self::Moving => "moving",
50 Self::Failed => "failed",
51 Self::Cancelled => "cancelled",
52 Self::Finished => "finished",
53 }
54 }
55}
56
57pub(crate) fn create(db: &DatabaseConnection) -> Result<MoveId, DbError> {
59 db.query_row("INSERT INTO _coven_moves(state) VALUES('moving') RETURNING id", [], |r| r.get(0))
60}
61
62pub(crate) fn state(db: &DatabaseConnection, id: MoveId) -> Result<Option<MoveState>, DbError> {
64 db.query("SELECT state FROM _coven_moves WHERE id=?1", [id], |r| r.get::<_, String>(0))?
65 .pop()
66 .map(|state| match state.as_str() {
67 "moving" => Ok(MoveState::Moving),
68 "failed" => Ok(MoveState::Failed),
69 "cancelled" => Ok(MoveState::Cancelled),
70 "finished" => Ok(MoveState::Finished),
71 _ => Err(DbError::DamagedDatabase),
72 })
73 .transpose()
74}
75
76pub(crate) fn bound(db: &DatabaseConnection, id: MoveId) -> Result<bool, DbError> {
79 db.query_row(
80 "SELECT EXISTS(SELECT 1 FROM _coven_file_uploads WHERE move=?1 AND holds IS NOT NULL)",
81 [id],
82 |r| r.get(0),
83 )
84}
85
86pub(crate) fn of_upload(db: &DatabaseConnection, upload: i64) -> Result<MoveId, DbError> {
88 db.query_row("SELECT move FROM _coven_file_uploads WHERE id=?1", [upload], |r| r.get(0))
89 .map_err(|error| match error {
90 DbError::Sqlite(rusqlite::Error::QueryReturnedNoRows) => {
91 DbError::Invariant { what: "a queued upload belongs to a move" }
92 }
93 error => error,
94 })
95}
96
97pub(crate) fn of_file(db: &DatabaseConnection, file: FileId) -> Result<Option<MoveId>, DbError> {
100 Ok(db.query("SELECT move FROM _coven_move_files WHERE file=?1", [file], |r| r.get(0))?.pop())
101}
102
103pub(crate) fn list_all(db: &DatabaseConnection, files: &[(MoveId, FileId)]) -> Result<(), DbError> {
106 let rows: Vec<_> = files
107 .iter()
108 .map(|(id, file)| {
109 Ok(vec![crate::insert_rows::value(id)?, crate::insert_rows::value(file)?])
110 })
111 .collect::<Result<_, DbError>>()?;
112 crate::insert_rows::insert_rows(
113 db,
114 2,
115 &rows,
116 |values| format!("INSERT INTO _coven_move_files(move,file) VALUES{values}"),
117 |_| Ok(()),
118 )?;
119 Ok(())
120}
121
122pub(crate) fn join(db: &DatabaseConnection, from: FileId, to: FileId) -> Result<(), DbError> {
125 db.internal_execute(
126 "INSERT INTO _coven_move_files(move,file)
127 SELECT u.move,?2 FROM _coven_upload_files AS l
128 JOIN _coven_file_uploads AS u ON u.id=l.upload WHERE l.file=?1",
129 rusqlite::params![from, to],
130 )?;
131 Ok(())
132}
133
134pub(crate) fn forget(db: &DatabaseConnection, file: FileId) -> Result<(), DbError> {
136 db.internal_execute("DELETE FROM _coven_move_files WHERE file=?1", [file])?;
137 Ok(())
138}
139
140fn set(db: &DatabaseConnection, id: MoveId, state: MoveState) -> Result<(), DbError> {
143 match state {
144 MoveState::Cancelled | MoveState::Finished => {
145 db.internal_execute("DELETE FROM _coven_move_files WHERE move=?1", [id])?;
146 }
147 MoveState::Moving | MoveState::Failed => {}
148 }
149 db.internal_execute(
150 "UPDATE _coven_moves SET state=?2 WHERE id=?1",
151 rusqlite::params![id, state.text()],
152 )?;
153 Ok(())
154}
155
156pub(crate) fn end_unswitched(
163 db: &DatabaseConnection,
164 id: MoveId,
165 state: MoveState,
166) -> Result<(), DbError> {
167 crate::file_queue::unswitch_move(db, id)?;
168 set(db, id, state)
169}
170
171pub(crate) fn complete(db: &DatabaseConnection) -> Result<Vec<(MoveId, Vec<i64>)>, DbError> {
175 let moves = db.query(
176 "SELECT m.id FROM _coven_moves AS m WHERE m.state='moving'
177 AND NOT EXISTS(SELECT 1 FROM _coven_file_uploads AS u
178 WHERE u.move=m.id AND u.stored=0
179 AND EXISTS(SELECT 1 FROM _coven_upload_files AS l WHERE l.upload=u.id))",
180 [],
181 |r| r.get(0),
182 )?;
183 moves
184 .into_iter()
185 .map(|id| {
186 let uploads = db.query(
187 "SELECT u.id FROM _coven_file_uploads AS u WHERE u.move=?1
188 AND EXISTS(SELECT 1 FROM _coven_upload_files AS l WHERE l.upload=u.id)
189 ORDER BY u.id",
190 [id],
191 |r| r.get(0),
192 )?;
193 Ok((id, uploads))
194 })
195 .collect()
196}
197
198pub(crate) fn finish(db: &DatabaseConnection, id: MoveId) -> Result<(), DbError> {
200 set(db, id, MoveState::Finished)?;
201 crate::pending::clear_move(db, id)
202}
203
204#[cfg(test)]
205#[path = "file_moves_tests.rs"]
206pub(crate) mod tests;