template changes and card generation

Cloze cards are not yet supported, missing decks are not handled,
and more testing is still required.
This commit is contained in:
Damien Elmes 2020-04-20 21:32:55 +10:00
parent c34e3e8096
commit 83bcb084fe
13 changed files with 579 additions and 171 deletions

View File

@ -45,6 +45,7 @@ unicase = "=2.6.0"
futures = "0.3.4"
rand = "0.7.3"
num-integer = "0.1.42"
itertools = "0.9.0"
# pinned until rusqlite 0.22 comes out
[target.'cfg(target_vendor="apple")'.dependencies.rusqlite]

View File

@ -100,11 +100,12 @@ impl Undoable for UpdateCardUndo {
}
impl Card {
pub fn new(nid: NoteID, ord: u16, deck_id: DeckID) -> Self {
pub fn new(nid: NoteID, ord: u16, deck_id: DeckID, due: i32) -> Self {
let mut card = Card::default();
card.nid = nid;
card.ord = ord;
card.did = deck_id;
card.due = due;
card
}
}

View File

@ -39,6 +39,7 @@ pub(crate) enum ConfigKey {
Rollover,
LocalOffset,
CurrentNoteTypeID,
NextNewCardPosition,
}
impl From<ConfigKey> for &'static str {
@ -51,6 +52,7 @@ impl From<ConfigKey> for &'static str {
ConfigKey::Rollover => "rollover",
ConfigKey::LocalOffset => "localOffset",
ConfigKey::CurrentNoteTypeID => "curModel",
ConfigKey::NextNewCardPosition => "nextPos",
}
}
}
@ -132,6 +134,14 @@ impl Collection {
pub(crate) fn set_current_notetype_id(&self, id: NoteTypeID) -> Result<()> {
self.set_config(ConfigKey::CurrentNoteTypeID, &id)
}
pub(crate) fn get_and_update_next_card_position(&self) -> Result<u32> {
let pos: u32 = self
.get_config_optional(ConfigKey::NextNewCardPosition)
.unwrap_or_default();
self.set_config(ConfigKey::NextNewCardPosition, &pos.wrapping_add(1))?;
Ok(pos)
}
}
#[derive(Deserialize, PartialEq, Debug)]

View File

@ -2,7 +2,6 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::{
card::Card,
collection::Collection,
define_newtype,
err::{AnkiError, Result},
@ -134,36 +133,42 @@ impl Collection {
pub fn add_note(&mut self, note: &mut Note) -> Result<()> {
self.transact(None, |col| {
let nt = col
.storage
.get_notetype(note.ntid)?
.ok_or_else(|| AnkiError::invalid_input("missing note type"))?;
let cardgen = CardGenContext::new(&nt, col.usn()?);
col.add_note_inner(note, &cardgen)
let ctx = CardGenContext::new(&nt, col.usn()?);
col.add_note_inner(&ctx, note)
})
}
pub(crate) fn add_note_inner(
&mut self,
note: &mut Note,
cardgen: &CardGenContext,
) -> Result<()> {
let nt = cardgen.notetype;
note.prepare_for_update(nt, cardgen.usn)?;
let nonempty_fields = note.nonempty_fields(&cardgen.notetype.fields);
let cards =
cardgen.new_cards_required(&nonempty_fields, nt.target_deck_id(), &Default::default());
pub(crate) fn add_note_inner(&mut self, ctx: &CardGenContext, note: &mut Note) -> Result<()> {
note.prepare_for_update(&ctx.notetype, ctx.usn)?;
let cards = ctx.new_cards_required(note, Default::default());
if cards.is_empty() {
return Err(AnkiError::NoCardsGenerated);
}
// add the note
// add note first, as we need the allocated ID for the cards
self.storage.add_note(note)?;
// and its associated cards
for (card_ord, target_deck_id) in cards {
let mut card = Card::new(note.id, card_ord as u16, target_deck_id);
self.add_card(&mut card)?;
}
self.add_generated_cards(ctx, note.id, &cards)
}
pub fn update_note(&mut self, note: &mut Note) -> 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.update_note_inner(&ctx, note)
})
}
pub(crate) fn update_note_inner(
&mut self,
ctx: &CardGenContext,
note: &mut Note,
) -> Result<()> {
note.prepare_for_update(ctx.notetype, ctx.usn)?;
self.generate_cards_for_existing_note(ctx, note)?;
self.storage.update_note(note)?;
Ok(())
}

View File

@ -0,0 +1,211 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::NoteType;
use crate::{
card::{Card, CardID},
collection::Collection,
decks::DeckID,
err::Result,
notes::{Note, NoteID},
template::ParsedTemplate,
types::Usn,
};
use itertools::Itertools;
use std::collections::HashSet;
/// Info about an existing card required when generating new cards
pub(crate) struct AlreadyGeneratedCardInfo {
pub id: CardID,
pub nid: NoteID,
pub ord: u32,
pub original_deck_id: DeckID,
pub position_if_new: Option<u32>,
}
#[derive(Debug)]
pub(crate) struct CardToGenerate {
pub ord: u32,
pub did: Option<DeckID>,
pub due: Option<u32>,
}
/// Info required to determine whether a particular card ordinal should exist,
/// and which deck it should be placed in.
pub(crate) struct SingleCardGenContext<'a> {
template: Option<ParsedTemplate<'a>>,
target_deck_id: Option<DeckID>,
}
/// Info required to determine which cards should be generated when note added/updated,
/// and where they should be placed.
pub(crate) struct CardGenContext<'a> {
pub usn: Usn,
pub notetype: &'a NoteType,
cards: Vec<SingleCardGenContext<'a>>,
}
impl CardGenContext<'_> {
pub(crate) fn new(nt: &NoteType, usn: Usn) -> CardGenContext<'_> {
CardGenContext {
usn,
notetype: &nt,
cards: nt
.templates
.iter()
.map(|tmpl| SingleCardGenContext {
template: tmpl.parsed_question(),
target_deck_id: tmpl.target_deck_id(),
})
.collect(),
}
}
/// If template[ord] generates a non-empty question given nonempty_fields, return the provided
/// deck id, or an overriden one. If question is empty, return None.
fn is_nonempty(&self, card_ord: usize, nonempty_fields: &HashSet<&str>) -> bool {
let card = &self.cards[card_ord];
let template = match card.template {
Some(ref template) => template,
None => {
// template failed to parse; card can not be generated
return false;
}
};
template.renders_with_fields(&nonempty_fields)
}
/// Returns the cards that need to be generated for the provided note.
pub(crate) fn new_cards_required(
&self,
note: &Note,
existing: &[AlreadyGeneratedCardInfo],
) -> Vec<CardToGenerate> {
let nonempty_fields = note.nonempty_fields(&self.notetype.fields);
let extracted = extract_data_from_existing_cards(existing);
self.new_cards_required_inner(&nonempty_fields, &extracted)
}
fn new_cards_required_inner(
&self,
nonempty_fields: &HashSet<&str>,
extracted: &ExtractedCardInfo,
) -> Vec<CardToGenerate> {
self.cards
.iter()
.enumerate()
.filter_map(|(ord, card)| {
if !extracted.existing_ords.contains(&(ord as u32))
&& self.is_nonempty(ord, &nonempty_fields)
{
Some(CardToGenerate {
ord: ord as u32,
did: card.target_deck_id.or(extracted.deck_id),
due: extracted.due,
})
} else {
None
}
})
.collect()
}
}
// this could be reworked in the future to avoid the extra vec allocation
fn group_generated_cards_by_note(
items: Vec<AlreadyGeneratedCardInfo>,
) -> Vec<(NoteID, Vec<AlreadyGeneratedCardInfo>)> {
let mut out = vec![];
for (key, group) in &items.into_iter().group_by(|c| c.nid) {
out.push((key, group.collect()));
}
out
}
#[derive(Debug, PartialEq, Default)]
pub(crate) struct ExtractedCardInfo {
// if set, the due position new cards should be given
pub due: Option<u32>,
// if set, the deck all current cards are in
pub deck_id: Option<DeckID>,
pub existing_ords: HashSet<u32>,
}
pub(crate) fn extract_data_from_existing_cards(
cards: &[AlreadyGeneratedCardInfo],
) -> ExtractedCardInfo {
let mut due = None;
let mut deck_ids = HashSet::new();
for card in cards {
if due.is_none() && card.position_if_new.is_some() {
due = card.position_if_new;
}
deck_ids.insert(card.original_deck_id);
}
let existing_ords: HashSet<_> = cards.iter().map(|c| c.ord).collect();
ExtractedCardInfo {
due,
deck_id: if deck_ids.len() == 1 {
deck_ids.into_iter().next()
} else {
None
},
existing_ords,
}
}
impl Collection {
pub(crate) fn generate_cards_for_existing_note(
&mut self,
ctx: &CardGenContext,
note: &Note,
) -> Result<()> {
let existing = self.storage.existing_cards_for_note(note.id)?;
let cards = ctx.new_cards_required(note, &existing);
if cards.is_empty() {
return Ok(());
}
self.add_generated_cards(ctx, note.id, &cards)
}
pub(crate) fn generate_cards_for_notetype(&mut self, ctx: &CardGenContext) -> Result<()> {
let existing_cards = self.storage.existing_cards_for_notetype(ctx.notetype.id)?;
let by_note = group_generated_cards_by_note(existing_cards);
for (nid, existing_cards) in by_note {
if existing_cards.len() == ctx.notetype.templates.len() {
// nothing to do
continue;
}
let note = self.storage.get_note(nid)?.unwrap();
self.generate_cards_for_existing_note(ctx, &note)?;
}
Ok(())
}
pub(crate) fn add_generated_cards(
&mut self,
ctx: &CardGenContext,
nid: NoteID,
cards: &[CardToGenerate],
) -> Result<()> {
let mut next_pos = None;
for c in cards {
let did = c.did.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));
}
next_pos.unwrap()
});
let mut card = Card::new(nid, c.ord as u16, did, due as i32);
self.add_card(&mut card)?;
}
Ok(())
}
}
// fixme: deal with case where invalid deck pointed to
// fixme: cloze cards, & avoid template count comparison for cloze

View File

@ -1,85 +0,0 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::NoteType;
use crate::{decks::DeckID, template::ParsedTemplate, types::Usn};
use std::collections::HashSet;
/// Info required to determine whether a particular card ordinal should exist,
/// and which deck it should be placed in.
pub(crate) struct SingleCardGenContext<'a> {
template: Option<ParsedTemplate<'a>>,
target_deck_id: Option<DeckID>,
}
/// Info required to determine which cards should be generated when note added/updated,
/// and where they should be placed.
pub(crate) struct CardGenContext<'a> {
pub usn: Usn,
pub notetype: &'a NoteType,
cards: Vec<SingleCardGenContext<'a>>,
}
impl CardGenContext<'_> {
pub(crate) fn new(nt: &NoteType, usn: Usn) -> CardGenContext<'_> {
CardGenContext {
usn,
notetype: &nt,
cards: nt
.templates
.iter()
.map(|tmpl| SingleCardGenContext {
template: tmpl.parsed_question(),
target_deck_id: tmpl.target_deck_id(),
})
.collect(),
}
}
/// If template[ord] generates a non-empty question given nonempty_fields, return the provided
/// deck id, or an overriden one. If question is empty, return None.
pub fn deck_id_if_nonempty(
&self,
card_ord: usize,
nonempty_fields: &HashSet<&str>,
target_deck_id: DeckID,
) -> Option<DeckID> {
let card = &self.cards[card_ord];
let template = match card.template {
Some(ref template) => template,
None => {
// template failed to parse; card can not be generated
return None;
}
};
if template.renders_with_fields(&nonempty_fields) {
Some(card.target_deck_id.unwrap_or(target_deck_id))
} else {
None
}
}
/// Return a list of (ordinal, deck id) for any new cards not in existing_ords
/// that are non-empty, and thus need to be added.
pub fn new_cards_required(
&self,
nonempty_fields: &HashSet<&str>,
target_deck_id: DeckID,
existing_ords: &HashSet<u16>,
) -> Vec<(usize, DeckID)> {
self.cards
.iter()
.enumerate()
.filter_map(|(ord, card)| {
let deck_id = card.target_deck_id.unwrap_or(target_deck_id);
if existing_ords.contains(&(ord as u16)) {
None
} else {
self.deck_id_if_nonempty(ord, nonempty_fields, deck_id)
.map(|did| (ord, did))
}
})
.collect()
}
}

View File

@ -1,7 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
mod cardgeninfo;
mod cardgen;
mod fields;
mod schema11;
mod schemachange;
@ -13,7 +13,7 @@ pub use crate::backend_proto::{
CardRequirement, CardTemplateConfig, NoteFieldConfig, NoteType as NoteTypeProto,
NoteTypeConfig,
};
pub(crate) use cardgeninfo::CardGenContext;
pub(crate) use cardgen::{AlreadyGeneratedCardInfo, CardGenContext};
pub use fields::NoteField;
pub use schema11::{CardTemplateSchema11, NoteFieldSchema11, NoteTypeSchema11};
pub use stock::all_stock_notetypes;
@ -199,11 +199,10 @@ impl Collection {
pub fn update_notetype(&mut self, nt: &mut NoteType) -> Result<()> {
self.transact(None, |col| {
let existing_notetype = col
.storage
.get_notetype(nt.id)?
.ok_or_else(|| AnkiError::invalid_input("no such notetype"))?;
col.update_notes_for_changed_fields(nt, existing_notetype.fields.len())?;
// fixme: card templates
col.update_cards_for_changed_templates(nt, existing_notetype.templates.len())?;
// fixme: update cache instead of clearing
col.state.notetype_cache.remove(&nt.id);

View File

@ -1,45 +1,71 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::{NoteField, NoteType};
use super::{CardGenContext, NoteType};
use crate::{collection::Collection, err::Result};
/// If any fields added, removed or reordered, returns a list of the new
/// field length, comprised of the original ordinals.
fn field_change_map(fields: &[NoteField], previous_field_count: usize) -> Option<Vec<Option<u32>>> {
let map: Vec<_> = fields.iter().map(|f| f.ord).collect();
let changed = map.len() != previous_field_count
|| map
/// True if any ordinals added, removed or reordered.
fn ords_changed(ords: &[Option<u32>], previous_len: usize) -> bool {
ords.len() != previous_len
|| ords
.iter()
.enumerate()
.any(|(idx, f)| f != &Some(idx as u32));
if changed {
Some(map)
} else {
None
.any(|(idx, &ord)| ord != Some(idx as u32))
}
#[derive(Default, PartialEq, Debug)]
struct TemplateOrdChanges {
added: Vec<u32>,
removed: Vec<u32>,
// map of old->new
moved: Vec<(u32, u32)>,
}
impl TemplateOrdChanges {
fn new(ords: Vec<Option<u32>>, previous_len: u32) -> Self {
let mut changes = TemplateOrdChanges::default();
let mut removed: Vec<_> = (0..previous_len).map(|v| Some(v as u32)).collect();
for (idx, old_ord) in ords.into_iter().enumerate() {
if let Some(old_ord) = old_ord {
if let Some(entry) = removed.get_mut(old_ord as usize) {
// guard required to ensure we don't panic if invalid high ordinal received
*entry = None;
}
if old_ord == idx as u32 {
// no action
} else {
changes.moved.push((old_ord as u32, idx as u32));
}
} else {
changes.added.push(idx as u32);
}
}
changes.removed = removed.into_iter().filter_map(|v| v).collect();
changes
}
}
impl Collection {
/// Caller must create transaction
/// Rewrite notes to match the updated field schema.
/// Caller must create transaction.
pub(crate) fn update_notes_for_changed_fields(
&mut self,
nt: &NoteType,
previous_field_count: usize,
) -> Result<()> {
let change_map = match field_change_map(&nt.fields, previous_field_count) {
None => {
// nothing to do
return Ok(());
}
Some(map) => map,
};
let ords: Vec<_> = nt.fields.iter().map(|f| f.ord).collect();
if !ords_changed(&ords, previous_field_count) {
// nothing to do
return Ok(());
}
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();
note.fields = change_map
note.fields = ords
.iter()
.map(|f| {
if let Some(idx) = f {
@ -58,12 +84,97 @@ impl Collection {
}
Ok(())
}
/// Update cards after card templates added, removed or reordered.
/// Does not remove cards where the template still exists but creates an empty card.
/// Caller must create transaction.
pub(crate) fn update_cards_for_changed_templates(
&mut self,
nt: &NoteType,
previous_template_count: usize,
) -> Result<()> {
let ords: Vec<_> = nt.templates.iter().map(|f| f.ord).collect();
if !ords_changed(&ords, previous_template_count) {
// nothing to do
return Ok(());
}
let changes = TemplateOrdChanges::new(ords, previous_template_count as u32);
if !changes.removed.is_empty() {
self.storage
.remove_cards_for_deleted_templates(nt.id, &changes.removed)?;
}
if !changes.moved.is_empty() {
self.storage
.move_cards_for_repositioned_templates(nt.id, &changes.moved)?;
}
let ctx = CardGenContext::new(nt, self.usn()?);
self.generate_cards_for_notetype(&ctx)?;
Ok(())
}
}
#[cfg(test)]
mod test {
use crate::collection::open_test_collection;
use crate::err::Result;
use super::{ords_changed, TemplateOrdChanges};
use crate::{collection::open_test_collection, err::Result, search::SortMode};
#[test]
fn ord_changes() {
assert_eq!(ords_changed(&[Some(0), Some(1)], 2), false);
assert_eq!(ords_changed(&[Some(0), Some(1)], 1), true);
assert_eq!(ords_changed(&[Some(1), Some(0)], 2), true);
assert_eq!(ords_changed(&[None, Some(1)], 2), true);
assert_eq!(ords_changed(&[Some(0), Some(1), None], 2), true);
}
#[test]
fn template_changes() {
assert_eq!(
TemplateOrdChanges::new(vec![Some(0), Some(1)], 2),
TemplateOrdChanges::default(),
);
assert_eq!(
TemplateOrdChanges::new(vec![Some(0), Some(1)], 3),
TemplateOrdChanges {
removed: vec![2],
..Default::default()
}
);
assert_eq!(
TemplateOrdChanges::new(vec![Some(1)], 2),
TemplateOrdChanges {
removed: vec![0],
moved: vec![(1, 0)],
..Default::default()
}
);
assert_eq!(
TemplateOrdChanges::new(vec![Some(0), None], 1),
TemplateOrdChanges {
added: vec![1],
..Default::default()
}
);
assert_eq!(
TemplateOrdChanges::new(vec![Some(2), None, Some(0)], 2),
TemplateOrdChanges {
added: vec![1],
moved: vec![(2, 0), (0, 2)],
removed: vec![1],
}
);
assert_eq!(
TemplateOrdChanges::new(vec![None, Some(2), None, Some(4)], 5),
TemplateOrdChanges {
added: vec![0, 2],
moved: vec![(2, 1), (4, 3)],
removed: vec![0, 1, 3],
}
);
}
#[test]
fn fields() -> Result<()> {
@ -94,4 +205,37 @@ mod test {
Ok(())
}
#[test]
fn cards() -> Result<()> {
let mut col = open_test_collection();
let mut nt = col
.storage
.get_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)?;
assert_eq!(
col.search_cards(&format!("nid:{}", note.id), SortMode::NoOrder)
.unwrap()
.len(),
1
);
// add an extra card template
nt.add_template("card 2", "{{Front}}", "");
col.update_notetype(&mut nt)?;
assert_eq!(
col.search_cards(&format!("nid:{}", note.id), SortMode::NoOrder)
.unwrap()
.len(),
2
);
Ok(())
}
}

View File

@ -10,7 +10,9 @@ use crate::notes::field_checksum;
use crate::notetype::NoteTypeID;
use crate::text::matches_wildcard;
use crate::text::without_combining;
use crate::{collection::Collection, text::strip_html_preserving_image_filenames};
use crate::{
collection::Collection, storage::ids_to_string, text::strip_html_preserving_image_filenames,
};
use lazy_static::lazy_static;
use regex::{Captures, Regex};
use std::fmt::Write;
@ -373,21 +375,6 @@ impl SqlWriter<'_> {
}
}
// Write a list of IDs as '(x,y,...)' into the provided string.
fn ids_to_string<T>(buf: &mut String, ids: &[T])
where
T: std::fmt::Display,
{
buf.push('(');
if !ids.is_empty() {
for id in ids.iter().skip(1) {
write!(buf, "{},", id).unwrap();
}
write!(buf, "{}", ids[0]).unwrap();
}
buf.push(')');
}
/// Convert a string with _, % or * characters into a regex.
fn glob_to_re(glob: &str) -> Option<String> {
if !glob.contains(|c| c == '_' || c == '*' || c == '%') {
@ -426,7 +413,6 @@ fn glob_to_re(glob: &str) -> Option<String> {
#[cfg(test)]
mod test {
use super::ids_to_string;
use crate::{
collection::{open_collection, Collection},
i18n::I18n,
@ -436,23 +422,6 @@ mod test {
use std::{fs, path::PathBuf};
use tempfile::tempdir;
#[test]
fn ids_string() {
let mut s = String::new();
ids_to_string::<u8>(&mut s, &[]);
assert_eq!(s, "()");
s.clear();
ids_to_string(&mut s, &[7]);
assert_eq!(s, "(7)");
s.clear();
ids_to_string(&mut s, &[7, 6]);
assert_eq!(s, "(6,7)");
s.clear();
ids_to_string(&mut s, &[7, 6, 5]);
assert_eq!(s, "(6,5,7)");
s.clear();
}
use super::super::parser::parse;
use super::*;

View File

@ -12,3 +12,42 @@ mod tag;
mod upgrades;
pub(crate) use sqlite::SqliteStorage;
use std::fmt::Write;
// Write a list of IDs as '(x,y,...)' into the provided string.
pub(crate) fn ids_to_string<T>(buf: &mut String, ids: &[T])
where
T: std::fmt::Display,
{
buf.push('(');
if !ids.is_empty() {
for id in ids.iter().skip(1) {
write!(buf, "{},", id).unwrap();
}
write!(buf, "{}", ids[0]).unwrap();
}
buf.push(')');
}
#[cfg(test)]
mod test {
use super::ids_to_string;
#[test]
fn ids_string() {
let mut s = String::new();
ids_to_string::<u8>(&mut s, &[]);
assert_eq!(s, "()");
s.clear();
ids_to_string(&mut s, &[7]);
assert_eq!(s, "(7)");
s.clear();
ids_to_string(&mut s, &[7, 6]);
assert_eq!(s, "(6,7)");
s.clear();
ids_to_string(&mut s, &[7, 6, 5]);
assert_eq!(s, "(6,5,7)");
s.clear();
}
}

View File

@ -0,0 +1,10 @@
delete from cards
where
nid in (
select
id
from notes
where
mid = ?
)
and ord = ?;

View File

@ -0,0 +1,27 @@
select
id,
nid,
ord,
-- original deck
(
case
odid
when 0 then did
else odid
end
),
-- new position if card is empty
(
case
type
when 0 then (
case
odue
when 0 then due
else odue
end
)
else null
end
)
from cards c

View File

@ -1,10 +1,14 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::SqliteStorage;
use super::{ids_to_string, SqliteStorage};
use crate::{
err::{AnkiError, DBErrorKind, Result},
notetype::{CardTemplate, CardTemplateConfig, NoteField, NoteFieldConfig, NoteTypeConfig},
notes::NoteID,
notetype::{
AlreadyGeneratedCardInfo, CardTemplate, CardTemplateConfig, NoteField, NoteFieldConfig,
NoteTypeConfig,
},
notetype::{NoteType, NoteTypeID, NoteTypeSchema11},
timestamp::TimestampMillis,
};
@ -26,6 +30,16 @@ fn row_to_notetype_core(row: &Row) -> Result<NoteType> {
})
}
fn row_to_existing_card(row: &Row) -> Result<AlreadyGeneratedCardInfo> {
Ok(AlreadyGeneratedCardInfo {
id: row.get(0)?,
nid: row.get(1)?,
ord: row.get(2)?,
original_deck_id: row.get(3)?,
position_if_new: row.get(4)?,
})
}
impl SqliteStorage {
pub(crate) fn get_notetype(&self, ntid: NoteTypeID) -> Result<Option<NoteType>> {
match self.get_notetype_core(ntid)? {
@ -165,6 +179,69 @@ impl SqliteStorage {
Ok(())
}
pub(crate) fn remove_cards_for_deleted_templates(
&self,
ntid: NoteTypeID,
ords: &[u32],
) -> Result<()> {
let mut stmt = self
.db
.prepare(include_str!("delete_cards_for_template.sql"))?;
for ord in ords {
stmt.execute(params![ntid, ord])?;
}
Ok(())
}
pub(crate) fn move_cards_for_repositioned_templates(
&self,
ntid: NoteTypeID,
changes: &[(u32, u32)],
) -> Result<()> {
let case_clauses: Vec<_> = changes
.iter()
.map(|(old, new)| format!("when {} then {}", old, new))
.collect();
let mut sql = format!(
"update cards set ord = (case ord {} end)
where nid in (select id from notes where mid = ?)
and ord in ",
case_clauses.join(" ")
);
ids_to_string(
&mut sql,
&changes.iter().map(|(old, _)| old).collect::<Vec<_>>(),
);
self.db.prepare(&sql)?.execute(&[ntid])?;
Ok(())
}
pub(crate) fn existing_cards_for_notetype(
&self,
ntid: NoteTypeID,
) -> Result<Vec<AlreadyGeneratedCardInfo>> {
self.db
.prepare_cached(concat!(
include_str!("existing_cards.sql"),
" where c.nid in (select id from notes where mid=?)"
))?
.query_and_then(&[ntid], row_to_existing_card)?
.collect()
}
pub(crate) fn existing_cards_for_note(
&self,
nid: NoteID,
) -> Result<Vec<AlreadyGeneratedCardInfo>> {
self.db
.prepare_cached(concat!(
include_str!("existing_cards.sql"),
" where c.nid = ?"
))?
.query_and_then(&[nid], row_to_existing_card)?
.collect()
}
// Upgrading/downgrading/legacy
pub(crate) fn get_all_notetypes_as_schema11(