collie_core/repository/
database.rs1use rusqlite::Connection as RusqliteConnection;
2use sea_query::{
3 ColumnDef, Expr, ForeignKey, ForeignKeyAction, Iden, Index, SqliteQueryBuilder, Table,
4 TableStatement,
5};
6use std::{
7 path::Path,
8 sync::{Arc, Mutex},
9};
10
11use crate::error::Result;
12
13pub type DbConnection = Arc<Mutex<RusqliteConnection>>;
14
15#[derive(Iden)]
16pub enum Feeds {
17 Table,
18 Id,
19 Title,
20 Link,
21 Status,
22 CheckedAt,
23 FetchOldItems,
24}
25
26#[derive(Iden)]
27pub enum Items {
28 Table,
29 Id,
30 Fingerprint,
31 Author,
32 Title,
33 Description,
34 Link,
35 Status,
36 IsSaved,
37 PublishedAt,
38 Feed,
39}
40
41pub struct Migration {
42 tables: Vec<Vec<TableStatement>>,
43}
44
45impl Default for Migration {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl Migration {
52 pub fn new() -> Self {
53 Self { tables: Vec::new() }
54 }
55
56 pub fn table(mut self, stmts: Vec<TableStatement>) -> Self {
57 self.tables.push(stmts);
58 self
59 }
60
61 pub fn migrate(&self, db: &RusqliteConnection) -> Result<()> {
62 let sql = self
63 .tables
64 .iter()
65 .map(|stmts| {
66 stmts
67 .iter()
68 .map(|stmt| stmt.build(SqliteQueryBuilder))
69 .collect::<Vec<_>>()
70 .join(";")
71 })
72 .collect::<Vec<_>>();
73
74 for stmt in sql {
75 let _ = db.execute_batch(&stmt);
76 }
77
78 Ok(())
79 }
80}
81
82pub fn open_connection(path: &Path) -> Result<RusqliteConnection> {
83 Ok(RusqliteConnection::open(path)?)
84}
85
86pub fn feeds_table() -> Vec<TableStatement> {
87 let create_stmt = Table::create()
88 .table(Feeds::Table)
89 .if_not_exists()
90 .col(
91 ColumnDef::new(Feeds::Id)
92 .integer()
93 .not_null()
94 .auto_increment()
95 .primary_key(),
96 )
97 .col(ColumnDef::new(Feeds::Title).text().not_null())
98 .col(ColumnDef::new(Feeds::Link).text().not_null())
99 .col(
100 ColumnDef::new(Feeds::Status)
101 .text()
102 .check(Expr::col(Feeds::Status).is_in(["subscribed", "unsubscribed"]))
103 .not_null()
104 .default("subscribed"),
105 )
106 .col(ColumnDef::new(Feeds::CheckedAt).date_time().not_null())
107 .col(
108 ColumnDef::new(Feeds::FetchOldItems)
109 .boolean()
110 .not_null()
111 .default(true),
112 )
113 .index(
114 Index::create()
115 .unique()
116 .name("uk_feeds_title_link")
117 .col(Feeds::Title)
118 .col(Feeds::Link),
119 )
120 .to_owned();
121
122 let alter_stmt = Table::alter()
123 .table(Feeds::Table)
124 .add_column_if_not_exists(
125 ColumnDef::new(Feeds::FetchOldItems)
126 .boolean()
127 .not_null()
128 .default(true),
129 )
130 .to_owned();
131
132 vec![
133 TableStatement::Create(create_stmt),
134 TableStatement::Alter(alter_stmt),
135 ]
136}
137
138pub fn items_table() -> Vec<TableStatement> {
139 let create_stmt = Table::create()
140 .table(Items::Table)
141 .if_not_exists()
142 .col(
143 ColumnDef::new(Items::Id)
144 .integer()
145 .not_null()
146 .auto_increment()
147 .primary_key(),
148 )
149 .col(
150 ColumnDef::new(Items::Fingerprint)
151 .text()
152 .not_null()
153 .unique_key(),
154 )
155 .col(ColumnDef::new(Items::Author).text())
156 .col(ColumnDef::new(Items::Title).text().not_null())
157 .col(ColumnDef::new(Items::Description).text().not_null())
158 .col(ColumnDef::new(Items::Link).text().not_null())
159 .col(
160 ColumnDef::new(Items::Status)
161 .text()
162 .check(Expr::col(Items::Status).is_in(["unread", "read"]))
163 .not_null()
164 .default("unread"),
165 )
166 .col(
167 ColumnDef::new(Items::IsSaved)
168 .integer()
169 .check(Expr::col(Items::IsSaved).is_in([0, 1]))
170 .not_null()
171 .default(0),
172 )
173 .col(ColumnDef::new(Items::PublishedAt).date_time().not_null())
174 .col(ColumnDef::new(Items::Feed).integer().not_null())
175 .foreign_key(
176 ForeignKey::create()
177 .name("fk_items_feeds")
178 .from(Items::Table, Items::Feed)
179 .to(Feeds::Table, Feeds::Id)
180 .on_delete(ForeignKeyAction::Cascade)
181 .on_update(ForeignKeyAction::Cascade),
182 )
183 .to_owned();
184
185 vec![TableStatement::Create(create_stmt)]
186}