start on exposing notes and individual note type methods

changes to note:

- add_note() now takes a provided deck id instead of looking it up
in the notetype
- note type use counts fetched using a single table scan
- make sure note type changes are persisted
- expose optionalness of ords in templates and fields json
This commit is contained in:
Damien Elmes 2020-04-23 10:10:28 +10:00
parent 09db596009
commit 6cc2bdbf87
10 changed files with 346 additions and 65 deletions

View file

@ -69,11 +69,20 @@ message BackendInput {
SetConfigJson set_config_json = 53;
bytes set_all_config = 54;
Empty get_all_config = 55;
Empty get_all_notetypes = 56;
bytes set_all_notetypes = 57;
int32 get_changed_notetypes = 56;
AddOrUpdateNotetypeIn add_or_update_notetype = 57;
Empty get_all_decks = 58;
bytes set_all_decks = 59;
Empty all_stock_notetypes = 60;
int64 get_notetype_legacy = 61;
Empty get_notetype_names = 62;
Empty get_notetype_names_and_counts = 63;
string get_notetype_id_by_name = 64;
int64 remove_notetype = 65;
int64 new_note = 66;
AddNoteIn add_note = 67;
Note update_note = 68;
int64 get_note = 69;
}
}
@ -123,10 +132,19 @@ message BackendOutput {
Empty set_config_json = 53;
Empty set_all_config = 54;
bytes get_all_config = 55;
bytes get_all_notetypes = 56;
Empty set_all_notetypes = 57;
bytes get_changed_notetypes = 56;
int64 add_or_update_notetype = 57;
bytes get_all_decks = 58;
Empty set_all_decks = 59;
bytes get_notetype_legacy = 61;
NoteTypeNames get_notetype_names = 62;
NoteTypeUseCounts get_notetype_names_and_counts = 63;
int64 get_notetype_id_by_name = 64;
Empty remove_notetype = 65;
Note new_note = 66;
int64 add_note = 67;
Empty update_note = 68;
Note get_note = 69;
BackendError error = 2047;
}
@ -147,6 +165,7 @@ message BackendError {
Empty interrupted = 8;
string json_error = 9;
string proto_error = 10;
Empty not_found_error = 11;
}
}
@ -569,3 +588,43 @@ enum StockNoteType {
message AllStockNotetypesOut {
repeated NoteType notetypes = 1;
}
message NoteTypeNames {
repeated NoteTypeNameID entries = 1;
}
message NoteTypeUseCounts {
repeated NoteTypeNameIDUseCount entries = 1;
}
message NoteTypeNameID {
int64 id = 1;
string name = 2;
}
message NoteTypeNameIDUseCount {
int64 id = 1;
string name = 2;
uint32 use_count = 3;
}
message AddOrUpdateNotetypeIn {
bytes json = 1;
bool preserve_usn_and_mtime = 2;
}
message Note {
int64 id = 1;
string guid = 2;
int64 ntid = 3;
uint32 mtime_secs = 4;
int32 usn = 5;
repeated string tags = 6;
repeated string fields = 7;
}
message AddNoteIn {
Note note = 1;
int64 deck_id = 2;
}

View file

@ -20,8 +20,8 @@ use crate::{
media::check::MediaChecker,
media::sync::MediaSyncProgress,
media::MediaManager,
notes::NoteID,
notetype::{all_stock_notetypes, NoteTypeID, NoteTypeSchema11},
notes::{Note, NoteID},
notetype::{all_stock_notetypes, NoteType, NoteTypeID, NoteTypeSchema11},
sched::cutoff::{local_minutes_west_for_stamp, sched_timing_today},
sched::timespan::{answer_button_time, learning_congrats, studied_today, time_span},
search::SortMode,
@ -77,6 +77,7 @@ fn anki_error_to_proto_error(err: AnkiError, i18n: &I18n) -> pb::BackendError {
AnkiError::CollectionAlreadyOpen => V::InvalidInput(pb::Empty {}),
AnkiError::JSONError { info } => V::JsonError(info),
AnkiError::ProtoError { info } => V::ProtoError(info),
AnkiError::NotFound => V::NotFoundError(Empty {}),
};
pb::BackendError {
@ -304,10 +305,8 @@ impl Backend {
pb::Empty {}
}),
Value::GetAllConfig(_) => OValue::GetAllConfig(self.get_all_config()?),
Value::GetAllNotetypes(_) => OValue::GetAllNotetypes(self.get_all_notetypes()?),
Value::SetAllNotetypes(bytes) => {
self.set_all_notetypes(&bytes)?;
OValue::SetAllNotetypes(pb::Empty {})
Value::GetChangedNotetypes(_) => {
OValue::GetChangedNotetypes(self.get_changed_notetypes()?)
}
Value::GetAllDecks(_) => OValue::GetAllDecks(self.get_all_decks()?),
Value::SetAllDecks(bytes) => {
@ -320,6 +319,31 @@ impl Backend {
.map(Into::into)
.collect(),
}),
Value::GetNotetypeLegacy(id) => {
OValue::GetNotetypeLegacy(self.get_notetype_legacy(id)?)
}
Value::GetNotetypeNames(_) => OValue::GetNotetypeNames(self.get_notetype_names()?),
Value::GetNotetypeNamesAndCounts(_) => {
OValue::GetNotetypeNamesAndCounts(self.get_notetype_use_counts()?)
}
Value::GetNotetypeIdByName(name) => {
OValue::GetNotetypeIdByName(self.get_notetype_id_by_name(name)?)
}
Value::AddOrUpdateNotetype(input) => {
OValue::AddOrUpdateNotetype(self.add_or_update_notetype_legacy(input)?)
}
Value::RemoveNotetype(id) => {
self.remove_notetype(id)?;
OValue::RemoveNotetype(pb::Empty {})
}
Value::NewNote(ntid) => OValue::NewNote(self.new_note(ntid)?),
Value::AddNote(input) => OValue::AddNote(self.add_note(input)?),
Value::UpdateNote(note) => {
self.update_note(note)?;
OValue::UpdateNote(pb::Empty {})
}
Value::GetNote(nid) => OValue::GetNote(self.get_note(nid)?),
})
}
@ -856,21 +880,12 @@ impl Backend {
})
}
fn set_all_notetypes(&self, json: &[u8]) -> Result<()> {
let val: HashMap<NoteTypeID, NoteTypeSchema11> = serde_json::from_slice(json)?;
self.with_col(|col| {
col.transact(None, |col| {
col.storage.set_schema11_notetypes(val)?;
col.storage.upgrade_notetypes_to_schema15()
})
})
}
fn get_all_notetypes(&self) -> Result<Vec<u8>> {
self.with_col(|col| {
let nts = col.storage.get_all_notetypes_as_schema11()?;
serde_json::to_vec(&nts).map_err(Into::into)
})
fn get_changed_notetypes(&self) -> Result<Vec<u8>> {
todo!("filter by usn");
// self.with_col(|col| {
// let nts = col.storage.get_all_notetypes_as_schema11()?;
// serde_json::to_vec(&nts).map_err(Into::into)
// })
}
fn set_all_decks(&self, json: &[u8]) -> Result<()> {
@ -884,6 +899,104 @@ impl Backend {
serde_json::to_vec(&decks).map_err(Into::into)
})
}
fn get_notetype_names(&self) -> Result<pb::NoteTypeNames> {
self.with_col(|col| {
let entries: Vec<_> = col
.storage
.get_all_notetype_names()?
.into_iter()
.map(|(id, name)| pb::NoteTypeNameId { id: id.0, name })
.collect();
Ok(pb::NoteTypeNames { entries })
})
}
fn get_notetype_use_counts(&self) -> Result<pb::NoteTypeUseCounts> {
self.with_col(|col| {
let entries: Vec<_> = col
.storage
.get_notetype_use_counts()?
.into_iter()
.map(|(id, name, use_count)| pb::NoteTypeNameIdUseCount {
id: id.0,
name,
use_count,
})
.collect();
Ok(pb::NoteTypeUseCounts { entries })
})
}
fn get_notetype_legacy(&self, id: i64) -> Result<Vec<u8>> {
self.with_col(|col| {
let schema11: NoteTypeSchema11 = col
.storage
.get_notetype(NoteTypeID(id))?
.ok_or(AnkiError::NotFound)?
.into();
Ok(serde_json::to_vec(&schema11)?)
})
}
fn get_notetype_id_by_name(&self, name: String) -> Result<i64> {
self.with_col(|col| {
col.storage
.get_notetype_id(&name)
.map(|nt| nt.unwrap_or(NoteTypeID(0)).0)
})
}
fn add_or_update_notetype_legacy(&self, input: pb::AddOrUpdateNotetypeIn) -> Result<i64> {
self.with_col(|col| {
let legacy: NoteTypeSchema11 = serde_json::from_slice(&input.json)?;
let mut nt: NoteType = legacy.into();
if nt.id.0 == 0 {
col.add_notetype(&mut nt)?;
} else {
col.update_notetype(&mut nt, input.preserve_usn_and_mtime)?;
}
Ok(nt.id.0)
})
}
fn remove_notetype(&self, _id: i64) -> Result<()> {
println!("fixme: remove notetype");
Ok(())
}
fn new_note(&self, ntid: i64) -> Result<pb::Note> {
self.with_col(|col| {
let nt = col
.get_notetype(NoteTypeID(ntid))?
.ok_or(AnkiError::NotFound)?;
Ok(nt.new_note().into())
})
}
fn add_note(&self, input: pb::AddNoteIn) -> Result<i64> {
self.with_col(|col| {
let mut note: Note = input.note.ok_or(AnkiError::NotFound)?.into();
col.add_note(&mut note, DeckID(input.deck_id))
.map(|_| note.id.0)
})
}
fn update_note(&self, pbnote: pb::Note) -> Result<()> {
self.with_col(|col| {
let mut note: Note = pbnote.into();
col.update_note(&mut note)
})
}
fn get_note(&self, nid: i64) -> Result<pb::Note> {
self.with_col(|col| {
col.storage
.get_note(NoteID(nid))?
.ok_or(AnkiError::NotFound)
.map(Into::into)
})
}
}
fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue {

View file

@ -45,6 +45,9 @@ pub enum AnkiError {
#[fail(display = "Close the existing collection first.")]
CollectionAlreadyOpen,
#[fail(display = "A requested item was not found.")]
NotFound,
}
// error helpers

View file

@ -2,7 +2,9 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::{
backend_proto as pb,
collection::Collection,
decks::DeckID,
define_newtype,
err::{AnkiError, Result},
notetype::{CardGenContext, NoteField, NoteType, NoteTypeID},
@ -105,6 +107,36 @@ impl Note {
}
}
impl From<Note> for pb::Note {
fn from(n: Note) -> Self {
pb::Note {
id: n.id.0,
guid: n.guid,
ntid: n.ntid.0,
mtime_secs: n.mtime.0 as u32,
usn: n.usn.0,
tags: n.tags,
fields: n.fields,
}
}
}
impl From<pb::Note> for Note {
fn from(n: pb::Note) -> Self {
Note {
id: NoteID(n.id),
guid: n.guid,
ntid: NoteTypeID(n.ntid),
mtime: TimestampSecs(n.mtime_secs as i64),
usn: Usn(n.usn),
tags: n.tags,
fields: n.fields,
sort_field: None,
checksum: None,
}
}
}
/// Text must be passed to strip_html_preserving_image_filenames() by
/// caller prior to passing in here.
pub(crate) fn field_checksum(text: &str) -> u32 {
@ -130,20 +162,37 @@ fn anki_base91(mut n: u64) -> String {
}
impl Collection {
pub fn add_note(&mut self, note: &mut Note) -> Result<()> {
fn canonify_note_tags(&self, note: &mut Note, usn: Usn) -> Result<()> {
// fixme: avoid the excess split/join
note.tags = self
.canonify_tags(&note.tags.join(" "), usn)?
.0
.split(' ')
.map(Into::into)
.collect();
Ok(())
}
pub fn add_note(&mut self, note: &mut Note, did: DeckID) -> Result<()> {
self.transact(None, |col| {
let nt = col
.get_notetype(note.ntid)?
.ok_or_else(|| AnkiError::invalid_input("missing note type"))?;
let ctx = CardGenContext::new(&nt, col.usn()?);
col.add_note_inner(&ctx, note)
col.add_note_inner(&ctx, note, did)
})
}
pub(crate) fn add_note_inner(&mut self, ctx: &CardGenContext, note: &mut Note) -> Result<()> {
pub(crate) fn add_note_inner(
&mut self,
ctx: &CardGenContext,
note: &mut Note,
did: DeckID,
) -> Result<()> {
self.canonify_note_tags(note, ctx.usn)?;
note.prepare_for_update(&ctx.notetype, ctx.usn)?;
self.storage.add_note(note)?;
self.generate_cards_for_new_note(ctx, note)
self.generate_cards_for_new_note(ctx, note, did)
}
pub fn update_note(&mut self, note: &mut Note) -> Result<()> {
@ -161,6 +210,7 @@ impl Collection {
ctx: &CardGenContext,
note: &mut Note,
) -> Result<()> {
self.canonify_note_tags(note, ctx.usn)?;
note.prepare_for_update(ctx.notetype, ctx.usn)?;
self.generate_cards_for_existing_note(ctx, note)?;
self.storage.update_note(note)?;
@ -198,7 +248,7 @@ mod test {
let mut note = nt.new_note();
// if no cards are generated, 1 card is added
col.add_note(&mut note).unwrap();
col.add_note(&mut note, DeckID(1)).unwrap();
let existing = col.storage.existing_cards_for_note(note.id)?;
assert_eq!(existing.len(), 1);
assert_eq!(existing[0].ord, 0);
@ -220,7 +270,7 @@ mod test {
// cloze cards also generate card 0 if no clozes are found
let nt = col.get_notetype_by_name("cloze")?.unwrap();
let mut note = nt.new_note();
col.add_note(&mut note).unwrap();
col.add_note(&mut note, DeckID(1)).unwrap();
let existing = col.storage.existing_cards_for_note(note.id)?;
assert_eq!(existing.len(), 1);
assert_eq!(existing[0].ord, 0);

View file

@ -204,8 +204,9 @@ impl Collection {
&mut self,
ctx: &CardGenContext,
note: &Note,
target_deck_id: DeckID,
) -> Result<()> {
self.generate_cards_for_note(ctx, note, false)
self.generate_cards_for_note(ctx, note, &[], Some(target_deck_id))
}
pub(crate) fn generate_cards_for_existing_note(
@ -213,25 +214,22 @@ impl Collection {
ctx: &CardGenContext,
note: &Note,
) -> Result<()> {
self.generate_cards_for_note(ctx, note, true)
let existing = self.storage.existing_cards_for_note(note.id)?;
self.generate_cards_for_note(ctx, note, &existing, None)
}
fn generate_cards_for_note(
&mut self,
ctx: &CardGenContext,
note: &Note,
check_existing: bool,
existing: &[AlreadyGeneratedCardInfo],
target_deck_id: Option<DeckID>,
) -> Result<()> {
let existing = if check_existing {
self.storage.existing_cards_for_note(note.id)?
} else {
vec![]
};
let cards = ctx.new_cards_required(note, &existing);
if cards.is_empty() {
return Ok(());
}
self.add_generated_cards(ctx, note.id, &cards)
self.add_generated_cards(ctx, note.id, &cards, target_deck_id)
}
pub(crate) fn generate_cards_for_notetype(&mut self, ctx: &CardGenContext) -> Result<()> {
@ -246,7 +244,7 @@ impl Collection {
continue;
}
let note = self.storage.get_note(nid)?.unwrap();
self.generate_cards_for_note(ctx, &note, true)?;
self.generate_cards_for_note(ctx, &note, &existing_cards, None)?;
}
Ok(())
@ -257,11 +255,16 @@ impl Collection {
ctx: &CardGenContext,
nid: NoteID,
cards: &[CardToGenerate],
target_deck_id: Option<DeckID>,
) -> Result<()> {
let mut next_pos = None;
for c in cards {
// fixme: deal with case where invalid deck pointed to
let did = c.did.unwrap_or_else(|| ctx.notetype.target_deck_id());
// fixme: deprecated note type deck id
let did = c
.did
.or(target_deck_id)
.unwrap_or_else(|| ctx.notetype.target_deck_id());
let due = c.due.unwrap_or_else(|| {
if next_pos.is_none() {
next_pos = Some(self.get_and_update_next_card_position().unwrap_or(0));

View file

@ -168,16 +168,23 @@ impl NoteType {
pub(crate) fn prepare_for_adding(&mut self) -> Result<()> {
// defaults to 0
self.config.target_deck_id = 1;
if self.config.target_deck_id == 0 {
self.config.target_deck_id = 1;
}
if self.fields.is_empty() {
return Err(AnkiError::invalid_input("1 field required"));
}
if self.templates.is_empty() {
return Err(AnkiError::invalid_input("1 template required"));
}
self.prepare_for_update()
}
pub(crate) fn prepare_for_update(&mut self) -> Result<()> {
self.normalize_names();
self.ensure_names_unique();
self.update_requirements();
// fixme: deal with duplicate note type names on update
Ok(())
}
@ -205,15 +212,34 @@ impl From<NoteType> for NoteTypeProto {
}
impl Collection {
/// Add a new notetype, and allocate it an ID.
pub fn add_notetype(&mut self, nt: &mut NoteType) -> Result<()> {
nt.prepare_for_adding()?;
self.transact(None, |col| col.storage.add_new_notetype(nt))
}
/// Saves changes to a note type. This will force a full sync if templates
/// or fields have been added/removed/reordered.
pub fn update_notetype(&mut self, nt: &mut NoteType) -> Result<()> {
pub fn update_notetype(&mut self, nt: &mut NoteType, preserve_usn: bool) -> Result<()> {
nt.prepare_for_update()?;
if !preserve_usn {
nt.mtime_secs = TimestampSecs::now();
nt.usn = self.usn()?;
}
self.transact(None, |col| {
let existing_notetype = col
.get_notetype(nt.id)?
.ok_or_else(|| AnkiError::invalid_input("no such notetype"))?;
col.update_notes_for_changed_fields(nt, existing_notetype.fields.len())?;
col.update_cards_for_changed_templates(nt, existing_notetype.templates.len())?;
if !preserve_usn {
let existing_notetype = col
.get_notetype(nt.id)?
.ok_or_else(|| AnkiError::invalid_input("no such notetype"))?;
col.update_notes_for_changed_fields(nt, existing_notetype.fields.len())?;
col.update_cards_for_changed_templates(nt, existing_notetype.templates.len())?;
}
col.storage.update_notetype_config(&nt)?;
col.storage.update_notetype_fields(nt.id, &nt.fields)?;
col.storage
.update_notetype_templates(nt.id, &nt.templates)?;
// fixme: update cache instead of clearing
col.state.notetype_cache.remove(&nt.id);

View file

@ -195,7 +195,7 @@ impl From<CardRequirement> for CardRequirementSchema11 {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct NoteFieldSchema11 {
pub(crate) name: String,
pub(crate) ord: u16,
pub(crate) ord: Option<u16>,
#[serde(deserialize_with = "deserialize_bool_from_anything")]
pub(crate) sticky: bool,
#[serde(deserialize_with = "deserialize_bool_from_anything")]
@ -210,7 +210,7 @@ impl Default for NoteFieldSchema11 {
fn default() -> Self {
Self {
name: String::new(),
ord: 0,
ord: None,
sticky: false,
rtl: false,
font: "Arial".to_string(),
@ -223,7 +223,7 @@ impl Default for NoteFieldSchema11 {
impl From<NoteFieldSchema11> for NoteField {
fn from(f: NoteFieldSchema11) -> Self {
NoteField {
ord: Some(f.ord as u32),
ord: f.ord.map(|o| o as u32),
name: f.name,
config: NoteFieldConfig {
sticky: f.sticky,
@ -243,7 +243,7 @@ impl From<NoteField> for NoteFieldSchema11 {
let conf = p.config;
NoteFieldSchema11 {
name: p.name,
ord: p.ord.unwrap() as u16,
ord: p.ord.map(|o| o as u16),
sticky: conf.sticky,
rtl: conf.rtl,
font: conf.font_name,
@ -256,7 +256,7 @@ impl From<NoteField> for NoteFieldSchema11 {
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct CardTemplateSchema11 {
pub(crate) name: String,
pub(crate) ord: u16,
pub(crate) ord: Option<u16>,
pub(crate) qfmt: String,
#[serde(default)]
pub(crate) afmt: String,
@ -277,7 +277,7 @@ pub struct CardTemplateSchema11 {
impl From<CardTemplateSchema11> for CardTemplate {
fn from(t: CardTemplateSchema11) -> Self {
CardTemplate {
ord: Some(t.ord as u32),
ord: t.ord.map(|t| t as u32),
name: t.name,
mtime_secs: TimestampSecs(0),
usn: Usn(0),
@ -302,7 +302,7 @@ impl From<CardTemplate> for CardTemplateSchema11 {
let conf = p.config;
CardTemplateSchema11 {
name: p.name,
ord: p.ord.unwrap() as u16,
ord: p.ord.map(|o| o as u16),
qfmt: conf.q_format,
afmt: conf.a_format,
bqfmt: conf.q_format_browser,

View file

@ -123,7 +123,7 @@ impl Collection {
#[cfg(test)]
mod test {
use super::{ords_changed, TemplateOrdChanges};
use crate::{collection::open_test_collection, err::Result, search::SortMode};
use crate::{collection::open_test_collection, decks::DeckID, err::Result, search::SortMode};
#[test]
fn ord_changes() {
@ -190,10 +190,10 @@ mod test {
let mut note = nt.new_note();
assert_eq!(note.fields.len(), 2);
note.fields = vec!["one".into(), "two".into()];
col.add_note(&mut note)?;
col.add_note(&mut note, DeckID(1))?;
nt.add_field("three");
col.update_notetype(&mut nt)?;
col.update_notetype(&mut nt, false)?;
let note = col.storage.get_note(note.id)?.unwrap();
assert_eq!(
@ -202,7 +202,7 @@ mod test {
);
nt.fields.remove(1);
col.update_notetype(&mut nt)?;
col.update_notetype(&mut nt, false)?;
let note = col.storage.get_note(note.id)?.unwrap();
assert_eq!(note.fields, vec!["one".to_string(), "".into()]);
@ -220,7 +220,7 @@ mod test {
let mut note = nt.new_note();
assert_eq!(note.fields.len(), 2);
note.fields = vec!["one".into(), "two".into()];
col.add_note(&mut note)?;
col.add_note(&mut note, DeckID(1))?;
assert_eq!(
col.search_cards(&format!("nid:{}", note.id), SortMode::NoOrder)
@ -231,7 +231,7 @@ mod test {
// add an extra card template
nt.add_template("card 2", "{{Front}}", "");
col.update_notetype(&mut nt)?;
col.update_notetype(&mut nt, false)?;
assert_eq!(
col.search_cards(&format!("nid:{}", note.id), SortMode::NoOrder)

View file

@ -0,0 +1,15 @@
select
mid,
(
select
name
from notetypes nt
where
nt.id = mid
) as name,
count(id)
from notes
group by
mid
order by
name

View file

@ -105,7 +105,19 @@ impl SqliteStorage {
.collect()
}
fn update_notetype_fields(&self, ntid: NoteTypeID, fields: &[NoteField]) -> Result<()> {
/// Returns list of (id, name, use_count)
pub fn get_notetype_use_counts(&self) -> Result<Vec<(NoteTypeID, String, u32)>> {
self.db
.prepare_cached(include_str!("get_use_counts.sql"))?
.query_and_then(NO_PARAMS, |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))?
.collect()
}
pub(crate) fn update_notetype_fields(
&self,
ntid: NoteTypeID,
fields: &[NoteField],
) -> Result<()> {
self.db
.prepare_cached("delete from fields where ntid=?")?
.execute(&[ntid])?;
@ -119,7 +131,7 @@ impl SqliteStorage {
Ok(())
}
fn update_notetype_templates(
pub(crate) fn update_notetype_templates(
&self,
ntid: NoteTypeID,
templates: &[CardTemplate],
@ -146,7 +158,7 @@ impl SqliteStorage {
Ok(())
}
fn update_notetype_config(&self, nt: &NoteType) -> Result<()> {
pub(crate) fn update_notetype_config(&self, nt: &NoteType) -> Result<()> {
assert!(nt.id.0 != 0);
let mut stmt = self
.db