@@ -171,6 +171,25 @@ impl Db {
171171 "purify_stats" ,
172172 "ALTER TABLE notes ADD COLUMN purify_stats TEXT" ,
173173 ) ?;
174+ // v0.10.0(REQ-004 增强):notes 表补 tags/properties/pin 列
175+ ensure_column (
176+ & conn,
177+ "notes" ,
178+ "tags" ,
179+ "ALTER TABLE notes ADD COLUMN tags TEXT NOT NULL DEFAULT '[]'" ,
180+ ) ?;
181+ ensure_column (
182+ & conn,
183+ "notes" ,
184+ "properties" ,
185+ "ALTER TABLE notes ADD COLUMN properties TEXT" ,
186+ ) ?;
187+ ensure_column (
188+ & conn,
189+ "notes" ,
190+ "pin" ,
191+ "ALTER TABLE notes ADD COLUMN pin INTEGER NOT NULL DEFAULT 0" ,
192+ ) ?;
174193 // 关联查询索引(列表 has_note 子查询 + 笔记页来源跳转共用)
175194 conn. execute_batch ( "CREATE INDEX IF NOT EXISTS idx_notes_session ON notes(session_id)" ) ?;
176195 // v0.7.7(REQ-183):结构图记录表(建表幂等——新库建表/旧库补表)
@@ -187,13 +206,14 @@ impl Db {
187206 /// 新建笔记,返回含 id 与时间戳的完整记录。
188207 pub fn create_note ( & self , new : & NewNote ) -> Result < Note > {
189208 let now = unix_seconds ( ) ;
209+ let tags = new. tags . clone ( ) . unwrap_or_else ( || "[]" . to_string ( ) ) ;
190210 let conn = self . conn . lock ( ) . expect ( "db lock poisoned" ) ;
191211 conn. execute (
192- "INSERT INTO notes (title, content, source, session_id, rule_version, purify_stats, created_at, updated_at)
193- VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?7 )" ,
212+ "INSERT INTO notes (title, content, source, session_id, rule_version, purify_stats, tags, properties, created_at, updated_at)
213+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?9 )" ,
194214 params ! [
195215 new. title, new. content, new. source, new. session_id, new. rule_version,
196- new. purify_stats, now
216+ new. purify_stats, tags , new . properties , now
197217 ] ,
198218 ) ?;
199219 let id = conn. last_insert_rowid ( ) ;
@@ -205,6 +225,9 @@ impl Db {
205225 session_id : new. session_id ,
206226 rule_version : new. rule_version . clone ( ) ,
207227 purify_stats : new. purify_stats . clone ( ) ,
228+ tags,
229+ properties : new. properties . clone ( ) ,
230+ pin : 0 ,
208231 created_at : now,
209232 updated_at : now,
210233 } )
@@ -214,7 +237,7 @@ impl Db {
214237 pub fn get_note ( & self , id : i64 ) -> Result < Option < Note > > {
215238 let conn = self . conn . lock ( ) . expect ( "db lock poisoned" ) ;
216239 let mut stmt = conn. prepare (
217- "SELECT id, title, content, source, session_id, rule_version, purify_stats, created_at, updated_at FROM notes WHERE id = ?1" ,
240+ "SELECT id, title, content, source, session_id, rule_version, purify_stats, created_at, updated_at, tags, properties, pin FROM notes WHERE id = ?1" ,
218241 ) ?;
219242 let mut rows = stmt. query_map ( params ! [ id] , row_to_note) ?;
220243 match rows. next ( ) {
@@ -228,7 +251,7 @@ impl Db {
228251 pub fn list_notes ( & self ) -> Result < Vec < Note > > {
229252 let conn = self . conn . lock ( ) . expect ( "db lock poisoned" ) ;
230253 let mut stmt = conn. prepare (
231- "SELECT id, title, content, source, session_id, rule_version, purify_stats, created_at, updated_at FROM notes ORDER BY updated_at DESC" ,
254+ "SELECT id, title, content, source, session_id, rule_version, purify_stats, created_at, updated_at, tags, properties, pin FROM notes ORDER BY updated_at DESC" ,
232255 ) ?;
233256 let rows = stmt. query_map ( [ ] , row_to_note) ?;
234257 rows. collect :: < rusqlite:: Result < Vec < _ > > > ( ) . map_err ( Into :: into)
@@ -260,7 +283,7 @@ impl Db {
260283 let escaped = escape_like ( keyword) ;
261284 let pattern = format ! ( "%{}%" , escaped) ;
262285 let mut stmt = conn. prepare (
263- "SELECT id, title, content, source, session_id, rule_version, purify_stats, created_at, updated_at FROM notes
286+ "SELECT id, title, content, source, session_id, rule_version, purify_stats, created_at, updated_at, tags, properties, pin FROM notes
264287 WHERE title LIKE ?1 ESCAPE '\\ ' OR content LIKE ?1 ESCAPE '\\ '
265288 ORDER BY updated_at DESC" ,
266289 ) ?;
@@ -275,7 +298,7 @@ impl Db {
275298 pub fn find_note_by_session ( & self , session_id : i64 ) -> Result < Option < Note > > {
276299 let conn = self . conn . lock ( ) . expect ( "db lock poisoned" ) ;
277300 let mut stmt = conn. prepare (
278- "SELECT id, title, content, source, session_id, rule_version, purify_stats, created_at, updated_at FROM notes
301+ "SELECT id, title, content, source, session_id, rule_version, purify_stats, created_at, updated_at, tags, properties, pin FROM notes
279302 WHERE session_id = ?1 ORDER BY created_at DESC, id DESC LIMIT 1" ,
280303 ) ?;
281304 let mut rows = stmt. query_map ( params ! [ session_id] , row_to_note) ?;
@@ -285,6 +308,59 @@ impl Db {
285308 None => Ok ( None ) ,
286309 }
287310 }
311+
312+ /// 更新笔记标签(v0.10.0;幂等覆盖写入 JSON 数组)。
313+ pub fn update_note_tags ( & self , id : i64 , tags : & str ) -> Result < bool > {
314+ let conn = self . conn . lock ( ) . expect ( "db lock poisoned" ) ;
315+ let affected = conn. execute (
316+ "UPDATE notes SET tags = ?1, updated_at = ?2 WHERE id = ?3" ,
317+ params ! [ tags, unix_seconds( ) , id] ,
318+ ) ?;
319+ Ok ( affected > 0 )
320+ }
321+
322+ /// 更新笔记固定状态(v0.10.0;pin=0 取消,=1 固定)。
323+ pub fn update_note_pin ( & self , id : i64 , pin : i64 ) -> Result < bool > {
324+ let conn = self . conn . lock ( ) . expect ( "db lock poisoned" ) ;
325+ let affected = conn. execute (
326+ "UPDATE notes SET pin = ?1, updated_at = ?2 WHERE id = ?3" ,
327+ params ! [ pin, unix_seconds( ) , id] ,
328+ ) ?;
329+ Ok ( affected > 0 )
330+ }
331+
332+ /// 列出笔记(支持排序模式;v0.10.0)。
333+ pub fn list_notes_sorted ( & self , sort_mode : & crate :: types:: NoteSortMode ) -> Result < Vec < Note > > {
334+ let conn = self . conn . lock ( ) . expect ( "db lock poisoned" ) ;
335+ let sql = match sort_mode {
336+ crate :: types:: NoteSortMode :: UpdatedDesc => {
337+ "SELECT id, title, content, source, session_id, rule_version, purify_stats, created_at, updated_at, tags, properties, pin FROM notes ORDER BY updated_at DESC"
338+ }
339+ crate :: types:: NoteSortMode :: PinFirst => {
340+ "SELECT id, title, content, source, session_id, rule_version, purify_stats, created_at, updated_at, tags, properties, pin FROM notes ORDER BY pin DESC, updated_at DESC"
341+ }
342+ crate :: types:: NoteSortMode :: CreatedDesc => {
343+ "SELECT id, title, content, source, session_id, rule_version, purify_stats, created_at, updated_at, tags, properties, pin FROM notes ORDER BY created_at DESC"
344+ }
345+ } ;
346+ let mut stmt = conn. prepare ( sql) ?;
347+ let rows = stmt. query_map ( [ ] , row_to_note) ?;
348+ rows. collect :: < rusqlite:: Result < Vec < _ > > > ( ) . map_err ( Into :: into)
349+ }
350+
351+ /// 按标签过滤笔记(v0.10.0;JSON 数组包含式匹配)。
352+ pub fn search_notes_by_tag ( & self , tag : & str ) -> Result < Vec < Note > > {
353+ let conn = self . conn . lock ( ) . expect ( "db lock poisoned" ) ;
354+ let escaped = escape_like ( tag) ;
355+ let pattern = format ! ( "%\" {}%\" %" , escaped) ;
356+ let mut stmt = conn. prepare (
357+ "SELECT id, title, content, source, session_id, rule_version, purify_stats, created_at, updated_at, tags, properties, pin FROM notes
358+ WHERE tags LIKE ?1 ESCAPE '\\ '
359+ ORDER BY updated_at DESC" ,
360+ ) ?;
361+ let rows = stmt. query_map ( params ! [ pattern] , row_to_note) ?;
362+ rows. collect :: < rusqlite:: Result < Vec < _ > > > ( ) . map_err ( Into :: into)
363+ }
288364}
289365
290366/// 把 rusqlite 行映射为 Note。
@@ -299,6 +375,9 @@ fn row_to_note(row: &rusqlite::Row<'_>) -> rusqlite::Result<Note> {
299375 purify_stats : row. get ( 6 ) ?,
300376 created_at : row. get ( 7 ) ?,
301377 updated_at : row. get ( 8 ) ?,
378+ tags : row. get ( 9 ) ?,
379+ properties : row. get ( 10 ) ?,
380+ pin : row. get ( 11 ) ?,
302381 } )
303382}
304383
@@ -345,7 +424,7 @@ mod tests {
345424 fn create_and_get_note_roundtrip ( ) {
346425 // Arrange
347426 let db = mem_db ( ) ;
348- let new = NewNote { title : "物理" . into ( ) , content : "# 牛顿\n F=ma" . into ( ) , source : "manual" . into ( ) , session_id : None , rule_version : None , purify_stats : None } ;
427+ let new = NewNote { title : "物理" . into ( ) , content : "# 牛顿\n F=ma" . into ( ) , source : "manual" . into ( ) , session_id : None , rule_version : None , purify_stats : None , tags : None , properties : None } ;
349428 // Act
350429 let created = db. create_note ( & new) . expect ( "create" ) ;
351430 let fetched = db. get_note ( created. id ) . expect ( "get" ) . expect ( "exists" ) ;
@@ -359,8 +438,8 @@ mod tests {
359438 fn list_orders_by_updated_desc ( ) {
360439 // Arrange
361440 let db = mem_db ( ) ;
362- db. create_note ( & NewNote { title : "A" . into ( ) , content : "a" . into ( ) , source : "manual" . into ( ) , session_id : None , rule_version : None , purify_stats : None } ) . unwrap ( ) ;
363- db. create_note ( & NewNote { title : "B" . into ( ) , content : "b" . into ( ) , source : "manual" . into ( ) , session_id : None , rule_version : None , purify_stats : None } ) . unwrap ( ) ;
441+ db. create_note ( & NewNote { title : "A" . into ( ) , content : "a" . into ( ) , source : "manual" . into ( ) , session_id : None , rule_version : None , purify_stats : None , tags : None , properties : None } ) . unwrap ( ) ;
442+ db. create_note ( & NewNote { title : "B" . into ( ) , content : "b" . into ( ) , source : "manual" . into ( ) , session_id : None , rule_version : None , purify_stats : None , tags : None , properties : None } ) . unwrap ( ) ;
364443 // Act
365444 let notes = db. list_notes ( ) . expect ( "list" ) ;
366445 // Assert
@@ -371,7 +450,7 @@ mod tests {
371450 fn update_note_changes_content ( ) {
372451 // Arrange
373452 let db = mem_db ( ) ;
374- let created = db. create_note ( & NewNote { title : "旧" . into ( ) , content : "旧内容" . into ( ) , source : "manual" . into ( ) , session_id : None , rule_version : None , purify_stats : None } ) . unwrap ( ) ;
453+ let created = db. create_note ( & NewNote { title : "旧" . into ( ) , content : "旧内容" . into ( ) , source : "manual" . into ( ) , session_id : None , rule_version : None , purify_stats : None , tags : None , properties : None } ) . unwrap ( ) ;
375454 // Act
376455 let ok = db. update_note ( created. id , "新标题" , "新内容" ) . expect ( "update" ) ;
377456 let fetched = db. get_note ( created. id ) . unwrap ( ) . unwrap ( ) ;
@@ -385,7 +464,7 @@ mod tests {
385464 fn delete_note_removes_row ( ) {
386465 // Arrange
387466 let db = mem_db ( ) ;
388- let created = db. create_note ( & NewNote { title : "待删" . into ( ) , content : "x" . into ( ) , source : "manual" . into ( ) , session_id : None , rule_version : None , purify_stats : None } ) . unwrap ( ) ;
467+ let created = db. create_note ( & NewNote { title : "待删" . into ( ) , content : "x" . into ( ) , source : "manual" . into ( ) , session_id : None , rule_version : None , purify_stats : None , tags : None , properties : None } ) . unwrap ( ) ;
389468 // Act
390469 let ok = db. delete_note ( created. id ) . expect ( "delete" ) ;
391470 let fetched = db. get_note ( created. id ) . expect ( "get" ) ;
@@ -398,8 +477,8 @@ mod tests {
398477 fn search_matches_title_and_content ( ) {
399478 // Arrange
400479 let db = mem_db ( ) ;
401- db. create_note ( & NewNote { title : "化学课" . into ( ) , content : "讲分子" . into ( ) , source : "classroom" . into ( ) , session_id : None , rule_version : None , purify_stats : None } ) . unwrap ( ) ;
402- db. create_note ( & NewNote { title : "随笔" . into ( ) , content : "含熵减概念" . into ( ) , source : "manual" . into ( ) , session_id : None , rule_version : None , purify_stats : None } ) . unwrap ( ) ;
480+ db. create_note ( & NewNote { title : "化学课" . into ( ) , content : "讲分子" . into ( ) , source : "classroom" . into ( ) , session_id : None , rule_version : None , purify_stats : None , tags : None , properties : None } ) . unwrap ( ) ;
481+ db. create_note ( & NewNote { title : "随笔" . into ( ) , content : "含熵减概念" . into ( ) , source : "manual" . into ( ) , session_id : None , rule_version : None , purify_stats : None , tags : None , properties : None } ) . unwrap ( ) ;
403482 // Act
404483 let by_title = db. search_notes ( "化学" ) . expect ( "search" ) ;
405484 let by_content = db. search_notes ( "熵减" ) . expect ( "search" ) ;
@@ -413,8 +492,8 @@ mod tests {
413492 fn search_escapes_wildcards ( ) {
414493 // Arrange:用户输入含 % 应作为字面量
415494 let db = mem_db ( ) ;
416- db. create_note ( & NewNote { title : "50%off" . into ( ) , content : "促销" . into ( ) , source : "manual" . into ( ) , session_id : None , rule_version : None , purify_stats : None } ) . unwrap ( ) ;
417- db. create_note ( & NewNote { title : "normal" . into ( ) , content : "普通" . into ( ) , source : "manual" . into ( ) , session_id : None , rule_version : None , purify_stats : None } ) . unwrap ( ) ;
495+ db. create_note ( & NewNote { title : "50%off" . into ( ) , content : "促销" . into ( ) , source : "manual" . into ( ) , session_id : None , rule_version : None , purify_stats : None , tags : None , properties : None } ) . unwrap ( ) ;
496+ db. create_note ( & NewNote { title : "normal" . into ( ) , content : "普通" . into ( ) , source : "manual" . into ( ) , session_id : None , rule_version : None , purify_stats : None , tags : None , properties : None } ) . unwrap ( ) ;
418497 // Act:搜索字面 "%"
419498 let result = db. search_notes ( "%off" ) . expect ( "search" ) ;
420499 // Assert:只命中含字面 %off 的,不应命中所有
@@ -509,6 +588,8 @@ mod tests {
509588 session_id : Some ( session. id ) ,
510589 rule_version : Some ( "note-rules-0.7.5" . into ( ) ) ,
511590 purify_stats : Some ( r#"{"filler":2,"verbal":3}"# . into ( ) ) ,
591+ tags : None ,
592+ properties : None ,
512593 } ;
513594 // Act
514595 let created = db. create_note ( & new) . expect ( "create" ) ;
@@ -534,6 +615,8 @@ mod tests {
534615 session_id : Some ( session. id ) ,
535616 rule_version : None ,
536617 purify_stats : None ,
618+ tags : None ,
619+ properties : None ,
537620 } ) . expect ( "create note" ) ;
538621 // Act:删除会话
539622 let deleted = db. delete_session ( session. id ) . expect ( "delete session" ) ;
@@ -561,6 +644,8 @@ mod tests {
561644 session_id : Some ( session. id ) ,
562645 rule_version : None ,
563646 purify_stats : None ,
647+ tags : None ,
648+ properties : None ,
564649 } ) . expect ( "create older" ) ;
565650 let newer = db. create_note ( & NewNote {
566651 title : "第二版" . into ( ) ,
@@ -569,6 +654,8 @@ mod tests {
569654 session_id : Some ( session. id ) ,
570655 rule_version : None ,
571656 purify_stats : None ,
657+ tags : None ,
658+ properties : None ,
572659 } ) . expect ( "create newer" ) ;
573660 db. create_note ( & NewNote {
574661 title : "手动" . into ( ) ,
@@ -577,6 +664,8 @@ mod tests {
577664 session_id : None ,
578665 rule_version : None ,
579666 purify_stats : None ,
667+ tags : None ,
668+ properties : None ,
580669 } ) . expect ( "create manual" ) ;
581670 // Act
582671 let found = db. find_note_by_session ( session. id ) . expect ( "find" ) ;
0 commit comments