the start of note adding, and note type changes

This commit is contained in:
Damien Elmes 2020-04-16 17:02:59 +10:00
parent 7c23deb562
commit f24dc05c8d
6 changed files with 177 additions and 9 deletions

View file

@ -43,6 +43,8 @@ num_enum = "0.4.2"
# pinned as any changes could invalidate sqlite indexes # pinned as any changes could invalidate sqlite indexes
unicase = "=2.6.0" unicase = "=2.6.0"
futures = "0.3.4" futures = "0.3.4"
rand = "0.7.3"
num-integer = "0.1.42"
# pinned until rusqlite 0.22 comes out # pinned until rusqlite 0.22 comes out
[target.'cfg(target_vendor="apple")'.dependencies.rusqlite] [target.'cfg(target_vendor="apple")'.dependencies.rusqlite]

View file

@ -5,8 +5,9 @@ use crate::err::{AnkiError, Result};
use crate::notetype::NoteTypeID; use crate::notetype::NoteTypeID;
use crate::text::strip_html_preserving_image_filenames; use crate::text::strip_html_preserving_image_filenames;
use crate::timestamp::TimestampSecs; use crate::timestamp::TimestampSecs;
use crate::{define_newtype, types::Usn}; use crate::{collection::Collection, define_newtype, types::Usn};
use std::convert::TryInto; use num_integer::Integer;
use std::{collections::HashSet, convert::TryInto};
define_newtype!(NoteID, i64); define_newtype!(NoteID, i64);
@ -20,12 +21,26 @@ pub struct Note {
pub mtime: TimestampSecs, pub mtime: TimestampSecs,
pub usn: Usn, pub usn: Usn,
pub tags: Vec<String>, pub tags: Vec<String>,
pub fields: Vec<String>, pub(crate) fields: Vec<String>,
pub(crate) sort_field: Option<String>, pub(crate) sort_field: Option<String>,
pub(crate) checksum: Option<u32>, pub(crate) checksum: Option<u32>,
} }
impl Note { 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<String> { pub fn fields(&self) -> &Vec<String> {
&self.fields &self.fields
} }
@ -58,9 +73,23 @@ impl Note {
self.sort_field = Some(sort_field.into()); self.sort_field = Some(sort_field.into());
self.checksum = Some(checksum); self.checksum = Some(checksum);
self.mtime = TimestampSecs::now(); self.mtime = TimestampSecs::now();
// hard-coded for now
self.usn = usn; self.usn = usn;
} }
#[allow(dead_code)]
pub(crate) fn nonempty_fields(&self) -> HashSet<u16> {
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 /// 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(); let digest = sha1::Sha1::from(text).digest().bytes();
u32::from_be_bytes(digest[..4].try_into().unwrap()) 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);
}
}

View file

@ -20,6 +20,7 @@ use crate::{
collection::Collection, collection::Collection,
define_newtype, define_newtype,
err::{AnkiError, Result}, err::{AnkiError, Result},
notes::Note,
template::{without_legacy_template_directives, FieldRequirements, ParsedTemplate}, template::{without_legacy_template_directives, FieldRequirements, ParsedTemplate},
text::ensure_string_in_nfc, text::ensure_string_in_nfc,
timestamp::TimestampSecs, timestamp::TimestampSecs,
@ -163,6 +164,10 @@ impl NoteType {
self.ensure_names_unique(); self.ensure_names_unique();
self.update_requirements(); self.update_requirements();
} }
pub fn new_note(&self) -> Note {
Note::new(self.id, self.fields.len())
}
} }
impl From<NoteType> for NoteTypeProto { impl From<NoteType> for NoteTypeProto {

View file

@ -35,7 +35,7 @@ impl Collection {
Some(map) => map, 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()?; let usn = self.usn()?;
for nid in nids { for nid in nids {
let mut note = self.storage.get_note(nid)?.unwrap(); let mut note = self.storage.get_note(nid)?.unwrap();
@ -63,11 +63,35 @@ impl Collection {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::collection::open_test_collection; use crate::collection::open_test_collection;
use crate::err::Result;
#[test] #[test]
fn fields() { fn fields() -> Result<()> {
let mut _col = open_test_collection(); 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(())
} }
} }

View file

@ -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,
""
)

View file

@ -5,6 +5,7 @@ use crate::{
err::Result, err::Result,
notes::{Note, NoteID}, notes::{Note, NoteID},
tags::{join_tags, split_tags}, tags::{join_tags, split_tags},
timestamp::TimestampMillis,
}; };
use rusqlite::{params, OptionalExtension}; use rusqlite::{params, OptionalExtension};
@ -40,6 +41,7 @@ impl super::SqliteStorage {
/// Caller must call note.prepare_for_update() prior to calling this. /// Caller must call note.prepare_for_update() prior to calling this.
pub(crate) fn update_note(&self, note: &Note) -> Result<()> { 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"))?; let mut stmt = self.db.prepare_cached(include_str!("update.sql"))?;
stmt.execute(params![ stmt.execute(params![
note.guid, note.guid,
@ -47,11 +49,29 @@ impl super::SqliteStorage {
note.mtime, note.mtime,
note.usn, note.usn,
join_tags(&note.tags), join_tags(&note.tags),
join_fields(&note.fields), join_fields(&note.fields()),
note.sort_field.as_ref().unwrap(), note.sort_field.as_ref().unwrap(),
note.checksum.unwrap(), note.checksum.unwrap(),
note.id note.id
])?; ])?;
Ok(()) 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(&note.tags),
join_fields(&note.fields()),
note.sort_field.as_ref().unwrap(),
note.checksum.unwrap(),
])?;
note.id.0 = self.db.last_insert_rowid();
Ok(())
}
} }