diff --git a/rslib/Cargo.toml b/rslib/Cargo.toml index 5eda132bd..9e69275f4 100644 --- a/rslib/Cargo.toml +++ b/rslib/Cargo.toml @@ -43,6 +43,8 @@ num_enum = "0.4.2" # pinned as any changes could invalidate sqlite indexes unicase = "=2.6.0" futures = "0.3.4" +rand = "0.7.3" +num-integer = "0.1.42" # pinned until rusqlite 0.22 comes out [target.'cfg(target_vendor="apple")'.dependencies.rusqlite] diff --git a/rslib/src/notes.rs b/rslib/src/notes.rs index 47966aa1e..b40537ffa 100644 --- a/rslib/src/notes.rs +++ b/rslib/src/notes.rs @@ -5,8 +5,9 @@ use crate::err::{AnkiError, Result}; use crate::notetype::NoteTypeID; use crate::text::strip_html_preserving_image_filenames; use crate::timestamp::TimestampSecs; -use crate::{define_newtype, types::Usn}; -use std::convert::TryInto; +use crate::{collection::Collection, define_newtype, types::Usn}; +use num_integer::Integer; +use std::{collections::HashSet, convert::TryInto}; define_newtype!(NoteID, i64); @@ -20,12 +21,26 @@ pub struct Note { pub mtime: TimestampSecs, pub usn: Usn, pub tags: Vec, - pub fields: Vec, + pub(crate) fields: Vec, pub(crate) sort_field: Option, pub(crate) checksum: Option, } impl Note { + pub(crate) fn new(ntid: NoteTypeID, field_count: usize) -> Self { + Note { + id: NoteID(0), + guid: guid(), + ntid, + mtime: TimestampSecs(0), + usn: Usn(0), + tags: vec![], + fields: vec!["".to_string(); field_count], + sort_field: None, + checksum: None, + } + } + pub fn fields(&self) -> &Vec { &self.fields } @@ -58,9 +73,23 @@ impl Note { self.sort_field = Some(sort_field.into()); self.checksum = Some(checksum); self.mtime = TimestampSecs::now(); - // hard-coded for now self.usn = usn; } + + #[allow(dead_code)] + pub(crate) fn nonempty_fields(&self) -> HashSet { + self.fields + .iter() + .enumerate() + .filter_map(|(ord, s)| { + if s.trim().is_empty() { + None + } else { + Some(ord as u16) + } + }) + .collect() + } } /// Text must be passed to strip_html_preserving_image_filenames() by @@ -69,3 +98,51 @@ pub(crate) fn field_checksum(text: &str) -> u32 { let digest = sha1::Sha1::from(text).digest().bytes(); u32::from_be_bytes(digest[..4].try_into().unwrap()) } + +fn guid() -> String { + anki_base91(rand::random()) +} + +fn anki_base91(mut n: u64) -> String { + let table = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\ + 0123456789!#$%&()*+,-./:;<=>?@[]^_`{|}~"; + let mut buf = String::new(); + while n > 0 { + let (q, r) = n.div_rem(&(table.len() as u64)); + buf.push(table[r as usize] as char); + n = q; + } + + buf.chars().rev().collect() +} + +impl Collection { + pub fn add_note(&mut self, note: &mut Note) -> Result<()> { + self.transact(None, |col| { + println!("fixme: need to add cards, abort if no cards generated, etc"); + // fixme: proper index + note.prepare_for_update(0, col.usn()?); + col.storage.add_note(note) + }) + } +} + +#[cfg(test)] +mod test { + use super::{anki_base91, field_checksum}; + + #[test] + fn test_base91() { + // match the python implementation for now + assert_eq!(anki_base91(0), ""); + assert_eq!(anki_base91(1), "b"); + assert_eq!(anki_base91(u64::max_value()), "Rj&Z5m[>Zp"); + assert_eq!(anki_base91(1234567890), "saAKk"); + } + + #[test] + fn test_field_checksum() { + assert_eq!(field_checksum("test"), 2840236005); + assert_eq!(field_checksum("今日"), 1464653051); + } +} diff --git a/rslib/src/notetype/mod.rs b/rslib/src/notetype/mod.rs index 0287a2ed2..22a59b9fa 100644 --- a/rslib/src/notetype/mod.rs +++ b/rslib/src/notetype/mod.rs @@ -20,6 +20,7 @@ use crate::{ collection::Collection, define_newtype, err::{AnkiError, Result}, + notes::Note, template::{without_legacy_template_directives, FieldRequirements, ParsedTemplate}, text::ensure_string_in_nfc, timestamp::TimestampSecs, @@ -163,6 +164,10 @@ impl NoteType { self.ensure_names_unique(); self.update_requirements(); } + + pub fn new_note(&self) -> Note { + Note::new(self.id, self.fields.len()) + } } impl From for NoteTypeProto { diff --git a/rslib/src/notetype/schemachange.rs b/rslib/src/notetype/schemachange.rs index 16e3bf702..ca8f8f5f7 100644 --- a/rslib/src/notetype/schemachange.rs +++ b/rslib/src/notetype/schemachange.rs @@ -35,7 +35,7 @@ impl Collection { Some(map) => map, }; - let nids = self.search_notes(&format!("mid:{}", nt.id))?; + let nids = self.search_notes_only(&format!("mid:{}", nt.id))?; let usn = self.usn()?; for nid in nids { let mut note = self.storage.get_note(nid)?.unwrap(); @@ -63,11 +63,35 @@ impl Collection { #[cfg(test)] mod test { use crate::collection::open_test_collection; + use crate::err::Result; #[test] - fn fields() { - let mut _col = open_test_collection(); + fn fields() -> Result<()> { + let mut col = open_test_collection(); + let mut nt = col + .storage + .get_full_notetype(col.get_current_notetype_id().unwrap())? + .unwrap(); + let mut note = nt.new_note(); + assert_eq!(note.fields.len(), 2); + note.fields = vec!["one".into(), "two".into()]; + col.add_note(&mut note)?; - // fixme: need note adding before we can check this + nt.add_field("three"); + col.update_notetype(&mut nt)?; + + let note = col.storage.get_note(note.id)?.unwrap(); + assert_eq!( + note.fields, + vec!["one".to_string(), "two".into(), "".into()] + ); + + nt.fields.remove(1); + col.update_notetype(&mut nt)?; + + let note = col.storage.get_note(note.id)?.unwrap(); + assert_eq!(note.fields, vec!["one".to_string(), "".into()]); + + Ok(()) } } diff --git a/rslib/src/storage/note/add.sql b/rslib/src/storage/note/add.sql new file mode 100644 index 000000000..d4b12b6b6 --- /dev/null +++ b/rslib/src/storage/note/add.sql @@ -0,0 +1,40 @@ +insert into notes ( + id, + guid, + mid, + mod, + usn, + tags, + flds, + sfld, + csum, + flags, + data + ) +values + ( + ( + case + when ?1 in ( + select + id + from notes + ) then ( + select + max(id) + 1 + from notes + ) + else ?1 + end + ), + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + 0, + "" + ) \ No newline at end of file diff --git a/rslib/src/storage/note/mod.rs b/rslib/src/storage/note/mod.rs index 3ae2906ab..9faf7e80a 100644 --- a/rslib/src/storage/note/mod.rs +++ b/rslib/src/storage/note/mod.rs @@ -5,6 +5,7 @@ use crate::{ err::Result, notes::{Note, NoteID}, tags::{join_tags, split_tags}, + timestamp::TimestampMillis, }; use rusqlite::{params, OptionalExtension}; @@ -40,6 +41,7 @@ impl super::SqliteStorage { /// Caller must call note.prepare_for_update() prior to calling this. pub(crate) fn update_note(&self, note: &Note) -> Result<()> { + assert!(note.id.0 != 0); let mut stmt = self.db.prepare_cached(include_str!("update.sql"))?; stmt.execute(params![ note.guid, @@ -47,11 +49,29 @@ impl super::SqliteStorage { note.mtime, note.usn, join_tags(¬e.tags), - join_fields(¬e.fields), + join_fields(¬e.fields()), note.sort_field.as_ref().unwrap(), note.checksum.unwrap(), note.id ])?; Ok(()) } + + pub(crate) fn add_note(&self, note: &mut Note) -> Result<()> { + assert!(note.id.0 == 0); + let mut stmt = self.db.prepare_cached(include_str!("add.sql"))?; + stmt.execute(params![ + TimestampMillis::now(), + note.guid, + note.ntid, + note.mtime, + note.usn, + join_tags(¬e.tags), + join_fields(¬e.fields()), + note.sort_field.as_ref().unwrap(), + note.checksum.unwrap(), + ])?; + note.id.0 = self.db.last_insert_rowid(); + Ok(()) + } }