notion_into_sqlite/
sqlite.rs1use std::{collections::HashMap, fs, path::Path};
2
3use crate::{
4 notion_database::{NotionDatabaseSchema, NotionPropertyType},
5 notion_pages::{NotionPage, NotionPropertyValue},
6};
7use anyhow::{anyhow, Result};
8use rusqlite::{params, params_from_iter, Connection};
9
10pub static PAGE_METADATA_TABLE: &str = "page_metadata";
11pub static PAGE_PROPERTIES_TABLE: &str = "pages";
12pub static PAGE_ID_COLUMN: &str = "page_id";
13
14struct ColumnNames {
16 hash: HashMap<String, String>,
17}
18impl ColumnNames {
19 fn new(schema: &NotionDatabaseSchema) -> ColumnNames {
20 let mut hash = HashMap::new();
21 for property in schema.properties.values() {
22 let column = property.name.replace('"', "\"\"");
23 hash.insert(property.name.to_string(), column);
24 }
25 ColumnNames { hash }
26 }
27
28 fn resolve(&self, notion_property_name: &str) -> &str {
30 self.hash.get(notion_property_name).unwrap()
31 }
32}
33
34pub struct Sqlite<'a> {
35 pub conn: Connection,
36 pub schema: &'a NotionDatabaseSchema,
37 column_names: ColumnNames,
38}
39impl Sqlite<'_> {
40 pub fn new<'a>(path: &str, schema: &'a NotionDatabaseSchema) -> Result<Sqlite<'a>> {
41 let conn = Connection::open(path)?;
42 let column_names = ColumnNames::new(schema);
43 Ok(Sqlite {
44 conn,
45 schema,
46 column_names,
47 })
48 }
49
50 pub fn validate_database_path(path: &str) -> Result<()> {
52 if Path::new(path).exists() {
53 return Err(anyhow!("{} already exists", path));
54 }
55
56 let conn = Connection::open(path)?;
57 match conn.close() {
58 Ok(_) => {
59 fs::remove_file(path).ok();
61 Ok(())
62 }
63 Err((_, err)) => Err(anyhow!(err.to_string())),
64 }
65 }
66
67 pub fn create_tables(&self) -> Result<()> {
68 let table_definition = self.table_definitin_from();
70 let sql = format!(
71 "CREATE TABLE {table_name} (
72 {id_column} TEXT PRIMARY KEY,
73 {definition}
74 )",
75 table_name = PAGE_PROPERTIES_TABLE,
76 id_column = PAGE_ID_COLUMN,
77 definition = table_definition,
78 );
79 debug!("{}", sql);
80 self.conn.execute(&sql, [])?;
81
82 let sql = format!(
84 "CREATE TABLE {table_name} (
85 id TEXT PRIMARY KEY,
86 url TEXT,
87 created_time TEXT,
88 created_by JSON,
89 last_edited_time TEXT,
90 last_edited_by JSON,
91 archived BOOLEAN
92 )",
93 table_name = PAGE_METADATA_TABLE,
94 );
95 debug!("{}", sql);
96 self.conn.execute(&sql, [])?;
97 Ok(())
98 }
99
100 pub fn insert(&self, page: &NotionPage) -> Result<()> {
101 let mut property_names = vec![PAGE_ID_COLUMN];
103 for name in page.properties.keys() {
104 property_names.push(name);
105 }
106 let sql = self.create_insert_sql_for(&property_names);
107 debug!("{}", sql);
108 let page_id = NotionPropertyValue::Text(page.id.clone());
109 let sql_params = params_from_iter(property_names.iter().map(|&column| {
110 if column == PAGE_ID_COLUMN {
111 &page_id
112 } else {
113 page.properties.get(column).unwrap()
114 }
115 }));
116 debug!("Parameters: {:?}", sql_params);
117 self.conn.execute(&sql, sql_params)?;
118
119 let sql = format!(
121 "INSERT INTO {table_name} (
122 id,
123 url,
124 created_time,
125 created_by,
126 last_edited_time,
127 last_edited_by,
128 archived
129 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
130 table_name = PAGE_METADATA_TABLE,
131 );
132 let sql_params = params![
133 page.id,
134 page.url,
135 page.created_time,
136 page.created_by.to_string(),
137 page.last_edited_time,
138 page.last_edited_by.to_string(),
139 page.archived
140 ];
141 self.conn.execute(&sql, sql_params)?;
142
143 Ok(())
144 }
145
146 fn table_definitin_from(&self) -> String {
150 self.schema
151 .properties
152 .iter()
153 .map(|(_, property)| {
154 let column = self.column_names.resolve(&property.name);
155 let data_type = match property.property_type {
156 NotionPropertyType::Title => "TEXT",
157 NotionPropertyType::Number => "REAL",
158 NotionPropertyType::Select => "TEXT",
159 NotionPropertyType::Checkbox => "BOOLEAN",
160 NotionPropertyType::Other => "TEXT",
161 _ => "TEXT",
162 };
163 format!(r#""{column}" {data_type}"#)
164 })
165 .collect::<Vec<_>>()
166 .join(", ")
167 }
168
169 fn create_insert_sql_for(&self, properties: &[&str]) -> String {
171 let columns_formatted = properties
172 .iter()
173 .map(|&property_name| {
174 if property_name == PAGE_ID_COLUMN {
175 property_name.to_string()
176 } else {
177 let column = self.column_names.resolve(property_name);
178 format!(r#""{column}""#)
179 }
180 })
181 .collect::<Vec<_>>();
182 let placeholders = (1..(columns_formatted.len() + 1))
183 .map(|index| format!("?{}", index))
184 .collect::<Vec<_>>();
185
186 format!(
187 "INSERT INTO {table_name} ({columns}) VALUES ({values})",
188 table_name = &PAGE_PROPERTIES_TABLE,
189 columns = columns_formatted.join(", "),
190 values = placeholders.join(", ")
191 )
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use std::fs;
199
200 #[test]
201 fn validate_database_path() {
202 fs::create_dir("tmp").ok();
203
204 let valid_path = "./tmp/a.db";
205 let result = Sqlite::validate_database_path(valid_path);
206 assert!(result.is_ok());
207 let invalid_path = "tmp/foo/bar/a.db";
208 let result = Sqlite::validate_database_path(invalid_path);
209 assert!(result.is_err());
210 }
211}