mirror of
https://github.com/ankitects/anki.git
synced 2025-09-22 16:02:23 -04:00
Keep tags in human form and update the tags table structure
See https://github.com/ankitects/anki/pull/900#issuecomment-758284016 - Leave tag names alone and add the collapsed and config columns to the tags table. - Update The DB check code to preserve the collapse state of used tags. - Add a simple test for clearing tags and their children
This commit is contained in:
parent
0b5bb711a1
commit
9a68d84483
11 changed files with 106 additions and 230 deletions
|
@ -1158,7 +1158,6 @@ QTableView {{ gridline-color: {grid} }}
|
||||||
toggle_expand(),
|
toggle_expand(),
|
||||||
not node.collapsed,
|
not node.collapsed,
|
||||||
item_type=SidebarItemType.TAG,
|
item_type=SidebarItemType.TAG,
|
||||||
id=node.tag_id,
|
|
||||||
full_name=head + node.name,
|
full_name=head + node.name,
|
||||||
)
|
)
|
||||||
root.addChild(item)
|
root.addChild(item)
|
||||||
|
|
|
@ -797,15 +797,10 @@ message SetTagCollapsedIn {
|
||||||
bool collapsed = 2;
|
bool collapsed = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message TagConfig {
|
|
||||||
bool browser_collapsed = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Tag {
|
message Tag {
|
||||||
int64 id = 1;
|
string name = 1;
|
||||||
string name = 2;
|
sint32 usn = 2;
|
||||||
sint32 usn = 3;
|
bool collapsed = 3;
|
||||||
TagConfig config = 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetChangedTagsOut {
|
message GetChangedTagsOut {
|
||||||
|
@ -813,10 +808,9 @@ message GetChangedTagsOut {
|
||||||
}
|
}
|
||||||
|
|
||||||
message TagTreeNode {
|
message TagTreeNode {
|
||||||
int64 tag_id = 1;
|
string name = 1;
|
||||||
string name = 2;
|
repeated TagTreeNode children = 2;
|
||||||
repeated TagTreeNode children = 3;
|
uint32 level = 3;
|
||||||
uint32 level = 5;
|
|
||||||
bool collapsed = 4;
|
bool collapsed = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,6 @@ use crate::{
|
||||||
get_remote_sync_meta, sync_abort, sync_login, FullSyncProgress, NormalSyncProgress,
|
get_remote_sync_meta, sync_abort, sync_login, FullSyncProgress, NormalSyncProgress,
|
||||||
SyncActionRequired, SyncAuth, SyncMeta, SyncOutput, SyncStage,
|
SyncActionRequired, SyncAuth, SyncMeta, SyncOutput, SyncStage,
|
||||||
},
|
},
|
||||||
tags::Tag,
|
|
||||||
template::RenderedNode,
|
template::RenderedNode,
|
||||||
text::{extract_av_tags, strip_av_tags, AVTag},
|
text::{extract_av_tags, strip_av_tags, AVTag},
|
||||||
timestamp::TimestampSecs,
|
timestamp::TimestampSecs,
|
||||||
|
@ -1295,28 +1294,24 @@ impl BackendService for Backend {
|
||||||
//-------------------------------------------------------------------
|
//-------------------------------------------------------------------
|
||||||
|
|
||||||
fn all_tags(&self, _input: Empty) -> BackendResult<pb::AllTagsOut> {
|
fn all_tags(&self, _input: Empty) -> BackendResult<pb::AllTagsOut> {
|
||||||
let tags: Vec<pb::Tag> =
|
let tags: Vec<pb::Tag> = self.with_col(|col| {
|
||||||
self.with_col(|col| Ok(col.all_tags()?.into_iter().map(|t| t.into()).collect()))?;
|
Ok(col
|
||||||
|
.storage
|
||||||
|
.all_tags()?
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| t.into())
|
||||||
|
.collect())
|
||||||
|
})?;
|
||||||
Ok(pb::AllTagsOut { tags })
|
Ok(pb::AllTagsOut { tags })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_tag_collapsed(&self, input: pb::SetTagCollapsedIn) -> BackendResult<pb::Bool> {
|
fn set_tag_collapsed(&self, input: pb::SetTagCollapsedIn) -> BackendResult<pb::Bool> {
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
let name = &input.name;
|
col.transact(None, |col| {
|
||||||
let mut tag = if let Some(tag) = col.storage.get_tag(name)? {
|
col.set_tag_collapsed(&input.name, input.collapsed)?;
|
||||||
tag
|
|
||||||
} else {
|
|
||||||
// tag is missing, register it
|
|
||||||
let t = Tag {
|
|
||||||
name: name.to_owned(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
col.register_tag(t)?.0
|
|
||||||
};
|
|
||||||
tag.config.browser_collapsed = input.collapsed;
|
|
||||||
col.update_tag(&tag)?;
|
|
||||||
Ok(pb::Bool { val: true })
|
Ok(pb::Bool { val: true })
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn register_tags(&self, input: pb::RegisterTagsIn) -> BackendResult<pb::Bool> {
|
fn register_tags(&self, input: pb::RegisterTagsIn) -> BackendResult<pb::Bool> {
|
||||||
|
@ -1336,7 +1331,7 @@ impl BackendService for Backend {
|
||||||
fn clear_tag(&self, tag: pb::String) -> BackendResult<pb::Bool> {
|
fn clear_tag(&self, tag: pb::String) -> BackendResult<pb::Bool> {
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
col.transact(None, |col| {
|
col.transact(None, |col| {
|
||||||
col.clear_tag(tag.val.as_str())?;
|
col.storage.clear_tag(tag.val.as_str())?;
|
||||||
Ok(pb::Bool { val: true })
|
Ok(pb::Bool { val: true })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -243,6 +243,7 @@ impl Collection {
|
||||||
let stamp = TimestampMillis::now();
|
let stamp = TimestampMillis::now();
|
||||||
|
|
||||||
// will rebuild tag list below
|
// will rebuild tag list below
|
||||||
|
let old_tags = self.storage.all_tags_sorted()?;
|
||||||
self.storage.clear_tags()?;
|
self.storage.clear_tags()?;
|
||||||
|
|
||||||
let total_notes = self.storage.total_notes()?;
|
let total_notes = self.storage.total_notes()?;
|
||||||
|
@ -294,6 +295,18 @@ impl Collection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let new_tags = self.storage.all_tags_sorted()?;
|
||||||
|
for old in old_tags.into_iter() {
|
||||||
|
for new in new_tags.iter() {
|
||||||
|
if new.name == old.name {
|
||||||
|
self.storage.set_tag_collapsed(&new.name, new.collapsed)?;
|
||||||
|
break;
|
||||||
|
} else if new.name.starts_with(&old.name) {
|
||||||
|
self.set_tag_collapsed(&old.name, old.collapsed)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if the collection is empty and the user has deleted all note types, ensure at least
|
// if the collection is empty and the user has deleted all note types, ensure at least
|
||||||
// one note type exists
|
// one note type exists
|
||||||
if self.storage.get_all_notetype_names()?.is_empty() {
|
if self.storage.get_all_notetype_names()?.is_empty() {
|
||||||
|
|
|
@ -10,7 +10,6 @@ use crate::{
|
||||||
notes::field_checksum,
|
notes::field_checksum,
|
||||||
notetype::NoteTypeID,
|
notetype::NoteTypeID,
|
||||||
storage::ids_to_string,
|
storage::ids_to_string,
|
||||||
tags::human_tag_name_to_native,
|
|
||||||
text::{
|
text::{
|
||||||
is_glob, matches_glob, normalize_to_nfc, strip_html_preserving_media_filenames,
|
is_glob, matches_glob, normalize_to_nfc, strip_html_preserving_media_filenames,
|
||||||
to_custom_re, to_re, to_sql, to_text, without_combining,
|
to_custom_re, to_re, to_sql, to_text, without_combining,
|
||||||
|
@ -204,8 +203,7 @@ impl SqlWriter<'_> {
|
||||||
text => {
|
text => {
|
||||||
write!(self.sql, "n.tags regexp ?").unwrap();
|
write!(self.sql, "n.tags regexp ?").unwrap();
|
||||||
let re = &to_custom_re(text, r"\S");
|
let re = &to_custom_re(text, r"\S");
|
||||||
let native_name = human_tag_name_to_native(re);
|
self.args.push(format!("(?i).* {}(::| ).*", re));
|
||||||
self.args.push(format!("(?i).* {}(\x1f| ).*", native_name));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -681,14 +679,14 @@ mod test {
|
||||||
s(ctx, r"tag:one"),
|
s(ctx, r"tag:one"),
|
||||||
(
|
(
|
||||||
"(n.tags regexp ?)".into(),
|
"(n.tags regexp ?)".into(),
|
||||||
vec!["(?i).* one(\x1f| ).*".into()]
|
vec!["(?i).* one(::| ).*".into()]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
s(ctx, r"tag:foo::bar"),
|
s(ctx, r"tag:foo::bar"),
|
||||||
(
|
(
|
||||||
"(n.tags regexp ?)".into(),
|
"(n.tags regexp ?)".into(),
|
||||||
vec!["(?i).* foo\x1fbar(\x1f| ).*".into()]
|
vec!["(?i).* foo::bar(::| ).*".into()]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -696,7 +694,7 @@ mod test {
|
||||||
s(ctx, r"tag:o*n\*et%w%oth_re\_e"),
|
s(ctx, r"tag:o*n\*et%w%oth_re\_e"),
|
||||||
(
|
(
|
||||||
"(n.tags regexp ?)".into(),
|
"(n.tags regexp ?)".into(),
|
||||||
vec!["(?i).* o\\S*n\\*et%w%oth\\Sre_e(\u{1f}| ).*".into()]
|
vec![r"(?i).* o\S*n\*et%w%oth\Sre_e(::| ).*".into()]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
assert_eq!(s(ctx, "tag:none"), ("(n.tags = '')".into(), vec![]));
|
assert_eq!(s(ctx, "tag:none"), ("(n.tags = '')".into(), vec![]));
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
err::Result,
|
err::Result,
|
||||||
notes::{Note, NoteID},
|
notes::{Note, NoteID},
|
||||||
notetype::NoteTypeID,
|
notetype::NoteTypeID,
|
||||||
tags::{human_tag_name_to_native, join_tags, native_tag_name_to_human, split_tags},
|
tags::{join_tags, split_tags},
|
||||||
timestamp::TimestampMillis,
|
timestamp::TimestampMillis,
|
||||||
};
|
};
|
||||||
use rusqlite::{params, Row, NO_PARAMS};
|
use rusqlite::{params, Row, NO_PARAMS};
|
||||||
|
@ -18,11 +18,6 @@ pub(crate) fn join_fields(fields: &[String]) -> String {
|
||||||
fields.join("\x1f")
|
fields.join("\x1f")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn native_tags_str(tags: &[String]) -> String {
|
|
||||||
let s: Vec<_> = tags.iter().map(|t| human_tag_name_to_native(t)).collect();
|
|
||||||
join_tags(&s)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn row_to_note(row: &Row) -> Result<Note> {
|
fn row_to_note(row: &Row) -> Result<Note> {
|
||||||
Ok(Note {
|
Ok(Note {
|
||||||
id: row.get(0)?,
|
id: row.get(0)?,
|
||||||
|
@ -31,7 +26,7 @@ fn row_to_note(row: &Row) -> Result<Note> {
|
||||||
mtime: row.get(3)?,
|
mtime: row.get(3)?,
|
||||||
usn: row.get(4)?,
|
usn: row.get(4)?,
|
||||||
tags: split_tags(row.get_raw(5).as_str()?)
|
tags: split_tags(row.get_raw(5).as_str()?)
|
||||||
.map(|t| native_tag_name_to_human(t))
|
.map(Into::into)
|
||||||
.collect(),
|
.collect(),
|
||||||
fields: split_fields(row.get_raw(6).as_str()?),
|
fields: split_fields(row.get_raw(6).as_str()?),
|
||||||
sort_field: None,
|
sort_field: None,
|
||||||
|
@ -57,7 +52,7 @@ impl super::SqliteStorage {
|
||||||
note.notetype_id,
|
note.notetype_id,
|
||||||
note.mtime,
|
note.mtime,
|
||||||
note.usn,
|
note.usn,
|
||||||
native_tags_str(¬e.tags),
|
join_tags(¬e.tags),
|
||||||
join_fields(¬e.fields()),
|
join_fields(¬e.fields()),
|
||||||
note.sort_field.as_ref().unwrap(),
|
note.sort_field.as_ref().unwrap(),
|
||||||
note.checksum.unwrap(),
|
note.checksum.unwrap(),
|
||||||
|
@ -75,7 +70,7 @@ impl super::SqliteStorage {
|
||||||
note.notetype_id,
|
note.notetype_id,
|
||||||
note.mtime,
|
note.mtime,
|
||||||
note.usn,
|
note.usn,
|
||||||
native_tags_str(¬e.tags),
|
join_tags(¬e.tags),
|
||||||
join_fields(¬e.fields()),
|
join_fields(¬e.fields()),
|
||||||
note.sort_field.as_ref().unwrap(),
|
note.sort_field.as_ref().unwrap(),
|
||||||
note.checksum.unwrap(),
|
note.checksum.unwrap(),
|
||||||
|
@ -93,7 +88,7 @@ impl super::SqliteStorage {
|
||||||
note.notetype_id,
|
note.notetype_id,
|
||||||
note.mtime,
|
note.mtime,
|
||||||
note.usn,
|
note.usn,
|
||||||
native_tags_str(¬e.tags),
|
join_tags(¬e.tags),
|
||||||
join_fields(¬e.fields()),
|
join_fields(¬e.fields()),
|
||||||
note.sort_field.as_ref().unwrap(),
|
note.sort_field.as_ref().unwrap(),
|
||||||
note.checksum.unwrap(),
|
note.checksum.unwrap(),
|
||||||
|
@ -162,69 +157,18 @@ impl super::SqliteStorage {
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get distinct note tags in human form
|
// get distinct note tags
|
||||||
pub(crate) fn get_note_tags(&self, nids: Vec<NoteID>) -> Result<Vec<String>> {
|
pub(crate) fn get_note_tags(&self, nids: Vec<NoteID>) -> Result<Vec<String>> {
|
||||||
if nids.is_empty() {
|
if nids.is_empty() {
|
||||||
self.db
|
self.db
|
||||||
.prepare_cached("select distinct tags from notes")?
|
.prepare_cached("select distinct tags from notes")?
|
||||||
.query_and_then(NO_PARAMS, |r| {
|
.query_and_then(NO_PARAMS, |r| Ok(r.get_raw(0).as_str()?.to_owned()))?
|
||||||
let t = r.get_raw(0).as_str()?;
|
|
||||||
Ok(native_tag_name_to_human(t))
|
|
||||||
})?
|
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
self.db
|
self.db
|
||||||
.prepare_cached("select distinct tags from notes where id in ?")?
|
.prepare_cached("select distinct tags from notes where id in ?")?
|
||||||
.query_and_then(nids, |r| {
|
.query_and_then(nids, |r| Ok(r.get_raw(0).as_str()?.to_owned()))?
|
||||||
let t = r.get_raw(0).as_str()?;
|
|
||||||
Ok(native_tag_name_to_human(t))
|
|
||||||
})?
|
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn upgrade_notes_to_schema17(&self) -> Result<()> {
|
|
||||||
let notes: Result<Vec<(NoteID, String)>> = self
|
|
||||||
.db
|
|
||||||
.prepare_cached("select id, tags from notes")?
|
|
||||||
.query_and_then(NO_PARAMS, |row| -> Result<(NoteID, String)> {
|
|
||||||
let id = NoteID(row.get_raw(0).as_i64()?);
|
|
||||||
let tags: Vec<String> = split_tags(row.get_raw(1).as_str()?)
|
|
||||||
.map(|t| human_tag_name_to_native(t))
|
|
||||||
.collect();
|
|
||||||
let tags = join_tags(&tags);
|
|
||||||
Ok((id, tags))
|
|
||||||
})?
|
|
||||||
.collect();
|
|
||||||
notes?.into_iter().try_for_each(|(id, tags)| -> Result<_> {
|
|
||||||
self.db
|
|
||||||
.prepare_cached("update notes set tags = ? where id = ?")?
|
|
||||||
.execute(params![tags, id])?;
|
|
||||||
Ok(())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn downgrade_notes_from_schema17(&self) -> Result<()> {
|
|
||||||
let notes: Result<Vec<(NoteID, String)>> = self
|
|
||||||
.db
|
|
||||||
.prepare_cached("select id, tags from notes")?
|
|
||||||
.query_and_then(NO_PARAMS, |row| -> Result<(NoteID, String)> {
|
|
||||||
let id = NoteID(row.get_raw(0).as_i64()?);
|
|
||||||
let tags: Vec<String> = split_tags(row.get_raw(1).as_str()?)
|
|
||||||
.map(|t| native_tag_name_to_human(t))
|
|
||||||
.collect();
|
|
||||||
let tags = join_tags(&tags);
|
|
||||||
Ok((id, tags))
|
|
||||||
})?
|
|
||||||
.collect();
|
|
||||||
notes?.into_iter().try_for_each(|(id, tags)| -> Result<_> {
|
|
||||||
self.db
|
|
||||||
.prepare_cached("update notes set tags = ? where id = ?")?
|
|
||||||
.execute(params![tags, id])?;
|
|
||||||
Ok(())
|
|
||||||
})?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
INSERT
|
INSERT
|
||||||
OR REPLACE INTO tags (id, name, usn, config)
|
OR REPLACE INTO tags (tag, usn, collapsed)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?)
|
|
@ -2,30 +2,23 @@
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
use super::SqliteStorage;
|
use super::SqliteStorage;
|
||||||
use crate::{
|
use crate::{err::Result, tags::Tag, types::Usn};
|
||||||
err::Result,
|
|
||||||
tags::{human_tag_name_to_native, native_tag_name_to_human, Tag, TagConfig, TagID},
|
|
||||||
timestamp::TimestampMillis,
|
|
||||||
types::Usn,
|
|
||||||
};
|
|
||||||
use prost::Message;
|
|
||||||
use rusqlite::{params, Row, NO_PARAMS};
|
use rusqlite::{params, Row, NO_PARAMS};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
fn row_to_tag(row: &Row) -> Result<Tag> {
|
fn row_to_tag(row: &Row) -> Result<Tag> {
|
||||||
let config = TagConfig::decode(row.get_raw(3).as_blob()?)?;
|
|
||||||
Ok(Tag {
|
Ok(Tag {
|
||||||
id: row.get(0)?,
|
name: row.get(0)?,
|
||||||
name: row.get(1)?,
|
usn: row.get(1)?,
|
||||||
usn: row.get(2)?,
|
collapsed: row.get(2)?,
|
||||||
config,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SqliteStorage {
|
impl SqliteStorage {
|
||||||
pub(crate) fn all_tags(&self) -> Result<Vec<Tag>> {
|
pub(crate) fn all_tags(&self) -> Result<Vec<Tag>> {
|
||||||
self.db
|
self.db
|
||||||
.prepare_cached("select id, name, usn, config from tags")?
|
.prepare_cached("select tag, usn, collapsed from tags")?
|
||||||
.query_and_then(NO_PARAMS, row_to_tag)?
|
.query_and_then(NO_PARAMS, row_to_tag)?
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
@ -33,54 +26,30 @@ impl SqliteStorage {
|
||||||
/// Get all tags in human form, sorted by name
|
/// Get all tags in human form, sorted by name
|
||||||
pub(crate) fn all_tags_sorted(&self) -> Result<Vec<Tag>> {
|
pub(crate) fn all_tags_sorted(&self) -> Result<Vec<Tag>> {
|
||||||
self.db
|
self.db
|
||||||
.prepare_cached("select id, name, usn, config from tags order by name")?
|
.prepare_cached("select tag, usn, collapsed from tags order by tag")?
|
||||||
.query_and_then(NO_PARAMS, |row| {
|
.query_and_then(NO_PARAMS, row_to_tag)?
|
||||||
let mut tag = row_to_tag(row)?;
|
|
||||||
tag.name = native_tag_name_to_human(&tag.name);
|
|
||||||
Ok(tag)
|
|
||||||
})?
|
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get tag by human name
|
/// Get tag by human name
|
||||||
pub(crate) fn get_tag(&self, name: &str) -> Result<Option<Tag>> {
|
pub(crate) fn get_tag(&self, name: &str) -> Result<Option<Tag>> {
|
||||||
self.db
|
self.db
|
||||||
.prepare_cached("select id, name, usn, config from tags where name = ?")?
|
.prepare_cached("select tag, usn, collapsed from tags where tag = ?")?
|
||||||
.query_and_then(&[human_tag_name_to_native(name)], |row| {
|
.query_and_then(&[name], row_to_tag)?
|
||||||
let mut tag = row_to_tag(row)?;
|
|
||||||
tag.name = native_tag_name_to_human(&tag.name);
|
|
||||||
Ok(tag)
|
|
||||||
})?
|
|
||||||
.next()
|
.next()
|
||||||
.transpose()
|
.transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn alloc_id(&self) -> rusqlite::Result<TagID> {
|
pub(crate) fn register_tag(&self, tag: &Tag) -> Result<()> {
|
||||||
self.db
|
|
||||||
.prepare_cached(include_str!("alloc_id.sql"))?
|
|
||||||
.query_row(&[TimestampMillis::now()], |r| r.get(0))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn register_tag(&self, tag: &mut Tag) -> Result<()> {
|
|
||||||
let mut config = vec![];
|
|
||||||
tag.config.encode(&mut config)?;
|
|
||||||
tag.id = self.alloc_id()?;
|
|
||||||
self.update_tag(tag)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn update_tag(&self, tag: &Tag) -> Result<()> {
|
|
||||||
let mut config = vec![];
|
|
||||||
tag.config.encode(&mut config)?;
|
|
||||||
self.db
|
self.db
|
||||||
.prepare_cached(include_str!("add.sql"))?
|
.prepare_cached(include_str!("add.sql"))?
|
||||||
.execute(params![tag.id, tag.name, tag.usn, config])?;
|
.execute(params![tag.name, tag.usn, tag.collapsed])?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn preferred_tag_case(&self, tag: &str) -> Result<Option<String>> {
|
pub(crate) fn preferred_tag_case(&self, tag: &str) -> Result<Option<String>> {
|
||||||
self.db
|
self.db
|
||||||
.prepare_cached("select name from tags where name = ?")?
|
.prepare_cached("select tag from tags where tag = ?")?
|
||||||
.query_and_then(params![tag], |row| row.get(0))?
|
.query_and_then(params![tag], |row| row.get(0))?
|
||||||
.next()
|
.next()
|
||||||
.transpose()
|
.transpose()
|
||||||
|
@ -89,8 +58,16 @@ impl SqliteStorage {
|
||||||
|
|
||||||
pub(crate) fn clear_tag(&self, tag: &str) -> Result<()> {
|
pub(crate) fn clear_tag(&self, tag: &str) -> Result<()> {
|
||||||
self.db
|
self.db
|
||||||
.prepare_cached("delete from tags where name regexp ?")?
|
.prepare_cached("delete from tags where tag regexp ?")?
|
||||||
.execute(&[format!("^{}($|\x1f)", regex::escape(tag))])?;
|
.execute(&[format!("^{}($|::)", regex::escape(tag))])?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_tag_collapsed(&self, tag: &str, collapsed: bool) -> Result<()> {
|
||||||
|
self.db
|
||||||
|
.prepare_cached("update tags set collapsed = ? where tag = ?")?
|
||||||
|
.execute(params![collapsed, tag])?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -111,7 +88,7 @@ impl SqliteStorage {
|
||||||
pub(crate) fn tags_pending_sync(&self, usn: Usn) -> Result<Vec<String>> {
|
pub(crate) fn tags_pending_sync(&self, usn: Usn) -> Result<Vec<String>> {
|
||||||
self.db
|
self.db
|
||||||
.prepare_cached(&format!(
|
.prepare_cached(&format!(
|
||||||
"select name from tags where {}",
|
"select tag from tags where {}",
|
||||||
usn.pending_object_clause()
|
usn.pending_object_clause()
|
||||||
))?
|
))?
|
||||||
.query_and_then(&[usn], |r| r.get(0).map_err(Into::into))?
|
.query_and_then(&[usn], |r| r.get(0).map_err(Into::into))?
|
||||||
|
@ -121,7 +98,7 @@ impl SqliteStorage {
|
||||||
pub(crate) fn update_tag_usns(&self, tags: &[String], new_usn: Usn) -> Result<()> {
|
pub(crate) fn update_tag_usns(&self, tags: &[String], new_usn: Usn) -> Result<()> {
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.db
|
.db
|
||||||
.prepare_cached("update tags set usn=? where name=?")?;
|
.prepare_cached("update tags set usn=? where tag=?")?;
|
||||||
for tag in tags {
|
for tag in tags {
|
||||||
stmt.execute(params![new_usn, tag])?;
|
stmt.execute(params![new_usn, tag])?;
|
||||||
}
|
}
|
||||||
|
@ -173,18 +150,7 @@ impl SqliteStorage {
|
||||||
.collect::<Result<Vec<Tag>>>()?;
|
.collect::<Result<Vec<Tag>>>()?;
|
||||||
self.db
|
self.db
|
||||||
.execute_batch(include_str!["../upgrades/schema17_upgrade.sql"])?;
|
.execute_batch(include_str!["../upgrades/schema17_upgrade.sql"])?;
|
||||||
tags.into_iter().try_for_each(|mut tag| -> Result<()> {
|
tags.into_iter()
|
||||||
tag.name = human_tag_name_to_native(&tag.name);
|
.try_for_each(|tag| -> Result<()> { self.register_tag(&tag) })
|
||||||
self.register_tag(&mut tag)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn downgrade_tags_from_schema17(&self) -> Result<()> {
|
|
||||||
let tags = self.all_tags()?;
|
|
||||||
self.clear_tags()?;
|
|
||||||
tags.into_iter().try_for_each(|mut tag| -> Result<()> {
|
|
||||||
tag.name = native_tag_name_to_human(&tag.name);
|
|
||||||
self.register_tag(&mut tag)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,6 @@ impl SqliteStorage {
|
||||||
}
|
}
|
||||||
if ver < 17 {
|
if ver < 17 {
|
||||||
self.upgrade_tags_to_schema17()?;
|
self.upgrade_tags_to_schema17()?;
|
||||||
self.upgrade_notes_to_schema17()?;
|
|
||||||
self.db.execute_batch("update col set ver = 17")?;
|
self.db.execute_batch("update col set ver = 17")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,9 +46,7 @@ impl SqliteStorage {
|
||||||
self.downgrade_decks_from_schema15()?;
|
self.downgrade_decks_from_schema15()?;
|
||||||
self.downgrade_notetypes_from_schema15()?;
|
self.downgrade_notetypes_from_schema15()?;
|
||||||
self.downgrade_config_from_schema14()?;
|
self.downgrade_config_from_schema14()?;
|
||||||
self.downgrade_tags_from_schema17()?;
|
|
||||||
self.downgrade_tags_from_schema14()?;
|
self.downgrade_tags_from_schema14()?;
|
||||||
self.downgrade_notes_from_schema17()?;
|
|
||||||
self.db
|
self.db
|
||||||
.execute_batch(include_str!("schema11_downgrade.sql"))?;
|
.execute_batch(include_str!("schema11_downgrade.sql"))?;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
DROP TABLE tags;
|
DROP TABLE tags;
|
||||||
CREATE TABLE tags (
|
CREATE TABLE tags (
|
||||||
id integer PRIMARY KEY NOT NULL,
|
tag text NOT NULL PRIMARY KEY COLLATE unicase,
|
||||||
name text NOT NULL COLLATE unicase,
|
|
||||||
usn integer NOT NULL,
|
usn integer NOT NULL,
|
||||||
config blob NOT NULL
|
collapsed boolean NOT NULL,
|
||||||
);
|
config blob NULL
|
||||||
|
) without rowid;
|
|
@ -1,11 +1,9 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
pub use crate::backend_proto::TagConfig;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend_proto::{Tag as TagProto, TagTreeNode},
|
backend_proto::{Tag as TagProto, TagTreeNode},
|
||||||
collection::Collection,
|
collection::Collection,
|
||||||
define_newtype,
|
|
||||||
err::{AnkiError, Result},
|
err::{AnkiError, Result},
|
||||||
notes::{NoteID, TransformNoteOutput},
|
notes::{NoteID, TransformNoteOutput},
|
||||||
text::{normalize_to_nfc, to_re},
|
text::{normalize_to_nfc, to_re},
|
||||||
|
@ -21,14 +19,11 @@ use std::{
|
||||||
};
|
};
|
||||||
use unicase::UniCase;
|
use unicase::UniCase;
|
||||||
|
|
||||||
define_newtype!(TagID, i64);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Tag {
|
pub struct Tag {
|
||||||
pub id: TagID,
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub usn: Usn,
|
pub usn: Usn,
|
||||||
pub config: TagConfig,
|
pub collapsed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ord for Tag {
|
impl Ord for Tag {
|
||||||
|
@ -54,10 +49,9 @@ impl Eq for Tag {}
|
||||||
impl Default for Tag {
|
impl Default for Tag {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Tag {
|
Tag {
|
||||||
id: TagID(0),
|
|
||||||
name: "".to_string(),
|
name: "".to_string(),
|
||||||
usn: Usn(-1),
|
usn: Usn(-1),
|
||||||
config: Default::default(),
|
collapsed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,10 +59,9 @@ impl Default for Tag {
|
||||||
impl From<Tag> for TagProto {
|
impl From<Tag> for TagProto {
|
||||||
fn from(t: Tag) -> Self {
|
fn from(t: Tag) -> Self {
|
||||||
TagProto {
|
TagProto {
|
||||||
id: t.id.0,
|
|
||||||
name: t.name,
|
name: t.name,
|
||||||
usn: t.usn.0,
|
usn: t.usn.0,
|
||||||
config: Some(t.config),
|
collapsed: t.collapsed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,10 +69,9 @@ impl From<Tag> for TagProto {
|
||||||
impl From<TagProto> for Tag {
|
impl From<TagProto> for Tag {
|
||||||
fn from(t: TagProto) -> Self {
|
fn from(t: TagProto) -> Self {
|
||||||
Tag {
|
Tag {
|
||||||
id: TagID(t.id),
|
|
||||||
name: t.name,
|
name: t.name,
|
||||||
usn: Usn(t.usn),
|
usn: Usn(t.usn),
|
||||||
config: t.config.unwrap(),
|
collapsed: t.collapsed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,17 +111,13 @@ fn normalized_tag_name_component(comp: &str) -> Cow<str> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn human_tag_name_to_native(name: &str) -> String {
|
fn normalize_tag_name(name: &str) -> String {
|
||||||
let mut out = String::with_capacity(name.len());
|
let mut out = String::with_capacity(name.len());
|
||||||
for comp in name.split("::") {
|
for comp in name.split("::") {
|
||||||
out.push_str(&normalized_tag_name_component(comp));
|
out.push_str(&normalized_tag_name_component(comp));
|
||||||
out.push('\x1f');
|
out.push_str("::");
|
||||||
}
|
}
|
||||||
out.trim_end_matches('\x1f').into()
|
out.trim_end_matches("::").into()
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn native_tag_name_to_human(name: &str) -> String {
|
|
||||||
name.replace('\x1f', "::")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fill_missing_tags(tags: Vec<Tag>) -> Vec<Tag> {
|
fn fill_missing_tags(tags: Vec<Tag>) -> Vec<Tag> {
|
||||||
|
@ -177,11 +165,10 @@ fn add_child_nodes(tags: &mut Peekable<impl Iterator<Item = Tag>>, parent: &mut
|
||||||
l if l == parent.level + 1 => {
|
l if l == parent.level + 1 => {
|
||||||
// next item is an immediate descendent of parent
|
// next item is an immediate descendent of parent
|
||||||
parent.children.push(TagTreeNode {
|
parent.children.push(TagTreeNode {
|
||||||
tag_id: tag.id.0,
|
|
||||||
name: (*split_name.last().unwrap()).into(),
|
name: (*split_name.last().unwrap()).into(),
|
||||||
children: vec![],
|
children: vec![],
|
||||||
level: parent.level + 1,
|
level: parent.level + 1,
|
||||||
collapsed: tag.config.browser_collapsed,
|
collapsed: tag.collapsed,
|
||||||
});
|
});
|
||||||
tags.next();
|
tags.next();
|
||||||
}
|
}
|
||||||
|
@ -199,19 +186,6 @@ fn add_child_nodes(tags: &mut Peekable<impl Iterator<Item = Tag>>, parent: &mut
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
pub fn all_tags(&self) -> Result<Vec<Tag>> {
|
|
||||||
self.storage
|
|
||||||
.all_tags()?
|
|
||||||
.into_iter()
|
|
||||||
.map(|t| {
|
|
||||||
Ok(Tag {
|
|
||||||
name: native_tag_name_to_human(&t.name),
|
|
||||||
..t
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tag_tree(&mut self) -> Result<TagTreeNode> {
|
pub fn tag_tree(&mut self) -> Result<TagTreeNode> {
|
||||||
let tags = self.storage.all_tags_sorted()?;
|
let tags = self.storage.all_tags_sorted()?;
|
||||||
let tree = tags_to_tree(tags);
|
let tree = tags_to_tree(tags);
|
||||||
|
@ -253,20 +227,19 @@ impl Collection {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn register_tag(&self, tag: Tag) -> Result<(Tag, bool)> {
|
pub(crate) fn register_tag(&self, tag: Tag) -> Result<(Tag, bool)> {
|
||||||
let native_name = human_tag_name_to_native(&tag.name);
|
let normalized_name = normalize_tag_name(&tag.name);
|
||||||
let mut t = Tag {
|
let mut t = Tag {
|
||||||
name: native_name.clone(),
|
name: normalized_name.clone(),
|
||||||
..tag
|
..tag
|
||||||
};
|
};
|
||||||
if native_name.is_empty() {
|
if normalized_name.is_empty() {
|
||||||
return Ok((t, false));
|
return Ok((t, false));
|
||||||
}
|
}
|
||||||
if let Some(preferred) = self.storage.preferred_tag_case(&native_name)? {
|
if let Some(preferred) = self.storage.preferred_tag_case(&normalized_name)? {
|
||||||
t.name = native_tag_name_to_human(&preferred);
|
t.name = preferred;
|
||||||
Ok((t, false))
|
Ok((t, false))
|
||||||
} else {
|
} else {
|
||||||
self.storage.register_tag(&mut t)?;
|
self.storage.register_tag(&t)?;
|
||||||
t.name = native_tag_name_to_human(&t.name);
|
|
||||||
Ok((t, true))
|
Ok((t, true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -287,19 +260,16 @@ impl Collection {
|
||||||
Ok(changed)
|
Ok(changed)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn update_tag(&self, tag: &Tag) -> Result<()> {
|
pub(crate) fn set_tag_collapsed(&self, name: &str, collapsed: bool) -> Result<()> {
|
||||||
let native_name = human_tag_name_to_native(&tag.name);
|
if self.storage.get_tag(name)?.is_none() {
|
||||||
self.storage.update_tag(&Tag {
|
// tag is missing, register it
|
||||||
id: tag.id,
|
let t = Tag {
|
||||||
name: native_name,
|
name: name.to_owned(),
|
||||||
usn: tag.usn,
|
..Default::default()
|
||||||
config: tag.config.clone(),
|
};
|
||||||
})
|
self.register_tag(t)?;
|
||||||
}
|
}
|
||||||
|
self.storage.set_tag_collapsed(name, collapsed)
|
||||||
pub(crate) fn clear_tag(&self, tag: &str) -> Result<()> {
|
|
||||||
let native_name = human_tag_name_to_native(tag);
|
|
||||||
self.storage.clear_tag(&native_name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace_tags_for_notes_inner<R: Replacer>(
|
fn replace_tags_for_notes_inner<R: Replacer>(
|
||||||
|
@ -425,11 +395,6 @@ mod test {
|
||||||
col.update_note(&mut note)?;
|
col.update_note(&mut note)?;
|
||||||
assert_eq!(¬e.tags, &["one", "two"]);
|
assert_eq!(¬e.tags, &["one", "two"]);
|
||||||
|
|
||||||
// note.tags is in human form
|
|
||||||
note.tags = vec!["foo::bar".into()];
|
|
||||||
col.update_note(&mut note)?;
|
|
||||||
assert_eq!(¬e.tags, &["foo::bar"]);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -483,6 +448,11 @@ mod test {
|
||||||
let note = col.storage.get_note(note.id)?.unwrap();
|
let note = col.storage.get_note(note.id)?.unwrap();
|
||||||
assert_eq!(¬e.tags, &["foo::bar", "foo::bar::bar", "foo::bar::foo",]);
|
assert_eq!(¬e.tags, &["foo::bar", "foo::bar::bar", "foo::bar::foo",]);
|
||||||
|
|
||||||
|
// tag children are also cleared when clearing their parent
|
||||||
|
col.register_tags("a a::b a::b::c", Usn(-1), true)?;
|
||||||
|
col.storage.clear_tag("a")?;
|
||||||
|
assert_eq!(col.storage.all_tags()?, vec![]);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue