1use rusqlite::{params, Connection, Result as SqlResult};
7use std::path::PathBuf;
8use std::sync::Mutex;
9
10fn db_path() -> PathBuf {
12 let home = std::env::var("HOME")
13 .or_else(|_| std::env::var("USERPROFILE"))
14 .unwrap_or_else(|_| ".".to_string());
15 let mut path = PathBuf::from(home);
16 path.push(".futu-opend-rs");
17 path.push("stock_data.db");
18 path
19}
20
21#[derive(Debug, Clone)]
23pub struct DbStockItem {
24 pub stock_id: u64,
25 pub code: String,
26 pub name_sc: String,
27 pub market_code: u32,
28 pub instrument_type: u32,
29 pub lot_size: u32,
30 pub delisting: bool,
31 pub warrnt_stock_owner: u64,
32 pub no_search: bool,
33 pub listing_date: u32,
34}
35
36pub struct StockDb {
38 conn: Mutex<Connection>,
39}
40
41unsafe impl Send for StockDb {}
43unsafe impl Sync for StockDb {}
44
45impl StockDb {
46 pub fn open() -> SqlResult<Self> {
48 let path = db_path();
49 if let Some(parent) = path.parent() {
50 let _ = std::fs::create_dir_all(parent);
51 }
52 let conn = Connection::open(&path)?;
53 conn.execute_batch(
54 "PRAGMA journal_mode=WAL;
55 PRAGMA synchronous=NORMAL;
56 PRAGMA cache_size=-8000;",
57 )?;
58 conn.execute_batch(
59 "CREATE TABLE IF NOT EXISTS config (
60 key TEXT PRIMARY KEY,
61 value TEXT NOT NULL
62 );
63 CREATE TABLE IF NOT EXISTS securities (
64 stock_id INTEGER PRIMARY KEY,
65 code TEXT NOT NULL,
66 name_sc TEXT NOT NULL DEFAULT '',
67 market_code INTEGER NOT NULL DEFAULT 0,
68 instrument_type INTEGER NOT NULL DEFAULT 0,
69 lot_size INTEGER NOT NULL DEFAULT 0,
70 delisting INTEGER NOT NULL DEFAULT 0,
71 warrnt_stock_owner INTEGER NOT NULL DEFAULT 0,
72 no_search INTEGER NOT NULL DEFAULT 0,
73 listing_date INTEGER NOT NULL DEFAULT 0
74 );",
75 )?;
76 let _ = conn.execute_batch(
78 "ALTER TABLE securities ADD COLUMN listing_date INTEGER NOT NULL DEFAULT 0;",
79 );
80 Ok(Self {
81 conn: Mutex::new(conn),
82 })
83 }
84
85 pub fn get_version(&self) -> u64 {
87 let conn = self.conn.lock().unwrap();
88 conn.query_row(
89 "SELECT value FROM config WHERE key = 'stock_list_version'",
90 [],
91 |row| {
92 let s: String = row.get(0)?;
93 Ok(s.parse::<u64>().unwrap_or(0))
94 },
95 )
96 .unwrap_or(0)
97 }
98
99 pub fn set_version(&self, version: u64) -> SqlResult<()> {
101 let conn = self.conn.lock().unwrap();
102 conn.execute(
103 "INSERT OR REPLACE INTO config (key, value) VALUES ('stock_list_version', ?1)",
104 params![version.to_string()],
105 )?;
106 Ok(())
107 }
108
109 pub fn load_all(&self) -> SqlResult<Vec<DbStockItem>> {
111 let conn = self.conn.lock().unwrap();
112 let mut stmt = conn.prepare(
113 "SELECT stock_id, code, name_sc, market_code, instrument_type, lot_size,
114 delisting, warrnt_stock_owner, no_search, listing_date
115 FROM securities",
116 )?;
117 let items = stmt
118 .query_map([], |row| {
119 Ok(DbStockItem {
120 stock_id: row.get::<_, i64>(0)? as u64,
121 code: row.get(1)?,
122 name_sc: row.get(2)?,
123 market_code: row.get::<_, i32>(3)? as u32,
124 instrument_type: row.get::<_, i32>(4)? as u32,
125 lot_size: row.get::<_, i32>(5)? as u32,
126 delisting: row.get::<_, i32>(6)? != 0,
127 warrnt_stock_owner: row.get::<_, i64>(7)? as u64,
128 no_search: row.get::<_, i32>(8)? != 0,
129 listing_date: row.get::<_, i32>(9)? as u32,
130 })
131 })?
132 .filter_map(|r| r.ok())
133 .collect();
134 Ok(items)
135 }
136
137 pub fn begin_batch(&self) -> SqlResult<()> {
139 let conn = self.conn.lock().unwrap();
140 conn.execute_batch("BEGIN TRANSACTION")?;
141 Ok(())
142 }
143
144 pub fn commit_batch(&self) -> SqlResult<()> {
146 let conn = self.conn.lock().unwrap();
147 conn.execute_batch("COMMIT")?;
148 Ok(())
149 }
150
151 pub fn upsert(&self, item: &DbStockItem) -> SqlResult<()> {
153 let conn = self.conn.lock().unwrap();
154 conn.execute(
155 "INSERT OR REPLACE INTO securities
156 (stock_id, code, name_sc, market_code, instrument_type, lot_size,
157 delisting, warrnt_stock_owner, no_search, listing_date)
158 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
159 params![
160 item.stock_id as i64,
161 item.code,
162 item.name_sc,
163 item.market_code as i32,
164 item.instrument_type as i32,
165 item.lot_size as i32,
166 item.delisting as i32,
167 item.warrnt_stock_owner as i64,
168 item.no_search as i32,
169 item.listing_date as i32,
170 ],
171 )?;
172 Ok(())
173 }
174
175 pub fn delete(&self, stock_id: u64) -> SqlResult<()> {
177 let conn = self.conn.lock().unwrap();
178 conn.execute(
179 "DELETE FROM securities WHERE stock_id = ?1",
180 params![stock_id as i64],
181 )?;
182 Ok(())
183 }
184
185 pub fn count(&self) -> u64 {
187 let conn = self.conn.lock().unwrap();
188 conn.query_row("SELECT COUNT(*) FROM securities", [], |row| {
189 row.get::<_, i64>(0)
190 })
191 .unwrap_or(0) as u64
192 }
193}