diff --git a/.github/scripts/trailing-newlines.sh b/.github/scripts/trailing-newlines.sh index e310c6747..ff8c71dfe 100755 --- a/.github/scripts/trailing-newlines.sh +++ b/.github/scripts/trailing-newlines.sh @@ -2,7 +2,12 @@ set -eu -o pipefail ${SHELLFLAGS} -files=$(rg -l '[^\n]\z' -g '!*.{svg,scss,json,sql}' || true) +# Checking version to force it fail the build if rg is not installed. +# Because `set -e` does not work inside the subshell $() +rg --version > /dev/null 2>&1 + +files=$(rg -l '[^\n]\z' -g '!*.{png,svg,scss,json,sql}' || true) + if [ "$files" != "" ]; then echo "the following files are missing a newline on the last line:" echo $files diff --git a/CONTRIBUTORS b/CONTRIBUTORS index d19694caa..9ed7429b9 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -29,6 +29,7 @@ Yoonchae Lee Evandro Coan Alan Du Yuchen Lei +Henry Tang ******************** diff --git a/pylib/Makefile b/pylib/Makefile index d019999a2..9727fc921 100644 --- a/pylib/Makefile +++ b/pylib/Makefile @@ -8,10 +8,20 @@ endif MAKEFLAGS += --warn-undefined-variables MAKEFLAGS += --no-builtin-rules +RUNARGS := +MYPY_ARGS := FIND := $(if $(wildcard /bin/find),/bin/find,/usr/bin/find) +ifndef OS + OS := unknown +endif + +# https://anki.tenderapp.com/discussions/beta-testing/1860-error-unused-type-ignore-comment +ifneq (${OS},Windows_NT) + MYPY_ARGS := --warn-unused-ignores +endif + .DELETE_ON_ERROR: -RUNARGS := .SUFFIXES: BLACKARGS := -t py36 anki tests setup.py tools/*.py --exclude='_pb2|buildinfo' ISORTARGS := anki tests setup.py @@ -39,7 +49,7 @@ PROTODEPS := $(wildcard ../proto/*.proto) .build/hooks: tools/genhooks.py tools/hookslib.py python tools/genhooks.py - black anki/hooks.py + python -m black anki/hooks.py @touch $@ BUILD_STEPS := .build/run-deps .build/dev-deps .build/py-proto anki/buildinfo.py .build/hooks @@ -53,7 +63,7 @@ check: $(BUILD_STEPS) .build/mypy .build/test .build/fmt .build/imports .build/l .PHONY: fix fix: $(BUILD_STEPS) isort $(ISORTARGS) - black $(BLACKARGS) + python -m black $(BLACKARGS) .PHONY: clean clean: @@ -65,7 +75,7 @@ clean: CHECKDEPS := $(shell ${FIND} anki tests -name '*.py' | grep -v buildinfo.py) .build/mypy: $(CHECKDEPS) - mypy anki + python -m mypy ${MYPY_ARGS} anki @touch $@ .build/test: $(CHECKDEPS) @@ -73,7 +83,8 @@ CHECKDEPS := $(shell ${FIND} anki tests -name '*.py' | grep -v buildinfo.py) @touch $@ .build/lint: $(CHECKDEPS) - pylint -j 0 --rcfile=.pylintrc -f colorized --extension-pkg-whitelist=ankirspy anki tests setup.py + python -m pylint -j 0 --rcfile=.pylintrc -f colorized \ + --extension-pkg-whitelist=ankirspy anki tests setup.py @touch $@ .build/imports: $(CHECKDEPS) @@ -81,7 +92,7 @@ CHECKDEPS := $(shell ${FIND} anki tests -name '*.py' | grep -v buildinfo.py) @touch $@ .build/fmt: $(CHECKDEPS) - black --check $(BLACKARGS) + python -m black --check $(BLACKARGS) @touch $@ # Building diff --git a/pylib/anki/dbproxy.py b/pylib/anki/dbproxy.py index a6ea0fec0..6ce136160 100644 --- a/pylib/anki/dbproxy.py +++ b/pylib/anki/dbproxy.py @@ -48,7 +48,6 @@ class DBProxy: for stmt in "insert", "update", "delete": if s.startswith(stmt): self.mod = True - assert ":" not in sql # fetch rows return self._backend.db_query(sql, args, first_row_only) @@ -84,7 +83,6 @@ class DBProxy: def executemany(self, sql: str, args: Iterable[Sequence[ValueForDB]]) -> None: self.mod = True - assert ":" not in sql if isinstance(args, list): list_args = args else: diff --git a/pylib/anki/lang.py b/pylib/anki/lang.py index e679b5d85..50bf528a7 100644 --- a/pylib/anki/lang.py +++ b/pylib/anki/lang.py @@ -133,7 +133,11 @@ def lang_to_disk_lang(lang: str) -> str: ): return lang.replace("_", "-") # other languages have the region portion stripped - return re.match("(.*)_", lang).group(1) + m = re.match("(.*)_", lang) + if m: + return m.group(1) + else: + return lang # the currently set interface language diff --git a/pylib/anki/sync.py b/pylib/anki/sync.py index 396d8fd1a..84f0a9e9f 100644 --- a/pylib/anki/sync.py +++ b/pylib/anki/sync.py @@ -279,7 +279,7 @@ from notes where %s""" lim = 250 while self.tablesLeft and lim: curTable = self.tablesLeft[0] - if not self.chunkRows: + if self.chunkRows is None: self.chunkRows = self.getChunkRows(curTable) rows = self.chunkRows[:lim] self.chunkRows = self.chunkRows[lim:] diff --git a/pylib/mypy.ini b/pylib/mypy.ini index 4215061bb..6ad6b351b 100644 --- a/pylib/mypy.ini +++ b/pylib/mypy.ini @@ -7,7 +7,6 @@ check_untyped_defs = true disallow_untyped_decorators = True warn_redundant_casts = True warn_unused_configs = True -warn_unused_ignores = True [mypy-win32file] ignore_missing_imports = True diff --git a/pylib/tests/test_importing.py b/pylib/tests/test_importing.py index fc90dcfe0..b563c1734 100644 --- a/pylib/tests/test_importing.py +++ b/pylib/tests/test_importing.py @@ -18,6 +18,15 @@ srcNotes = None srcCards = None +def clear_tempfile(tf): + """ https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file """ + try: + tf.close() + os.unlink(tf.name) + except: + pass + + def test_anki2_mediadupes(): tmp = getEmptyCol() # add a note that references a sound @@ -208,13 +217,15 @@ def test_tsv_tag_modified(): n.addTag("four") deck.addNote(n) - with NamedTemporaryFile(mode="w") as tf: + # https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file + with NamedTemporaryFile(mode="w", delete=False) as tf: tf.write("1\tb\tc\n") tf.flush() i = TextImporter(deck, tf.name) i.initMapping() i.tagModified = "boom" i.run() + clear_tempfile(tf) n.load() assert n["Front"] == "1" @@ -243,13 +254,15 @@ def test_tsv_tag_multiple_tags(): n.addTag("five") deck.addNote(n) - with NamedTemporaryFile(mode="w") as tf: + # https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file + with NamedTemporaryFile(mode="w", delete=False) as tf: tf.write("1\tb\tc\n") tf.flush() i = TextImporter(deck, tf.name) i.initMapping() i.tagModified = "five six" i.run() + clear_tempfile(tf) n.load() assert n["Front"] == "1" @@ -273,13 +286,15 @@ def test_csv_tag_only_if_modified(): n["Left"] = "3" deck.addNote(n) - with NamedTemporaryFile(mode="w") as tf: + # https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file + with NamedTemporaryFile(mode="w", delete=False) as tf: tf.write("1,2,3\n") tf.flush() i = TextImporter(deck, tf.name) i.initMapping() i.tagModified = "right" i.run() + clear_tempfile(tf) n.load() assert n.tags == [] diff --git a/pylib/tests/test_stats.py b/pylib/tests/test_stats.py index 7de3dc831..045983ac0 100644 --- a/pylib/tests/test_stats.py +++ b/pylib/tests/test_stats.py @@ -1,4 +1,5 @@ -# coding: utf-8 +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- import os import tempfile @@ -34,6 +35,6 @@ def test_graphs(): d = aopen(os.path.join(dir, "test.anki2")) g = d.stats() rep = g.report() - with open(os.path.join(dir, "test.html"), "w") as f: + with open(os.path.join(dir, "test.html"), "w", encoding="UTF-8") as f: f.write(rep) return diff --git a/qt/Makefile b/qt/Makefile index d92370c9d..4fef3e92b 100644 --- a/qt/Makefile +++ b/qt/Makefile @@ -9,6 +9,19 @@ MAKEFLAGS += --warn-undefined-variables MAKEFLAGS += --no-builtin-rules FIND := $(if $(wildcard /bin/find),/bin/find,/usr/bin/find) +MYPY_ARGS := +PYLINT_ARGS := + +ifndef OS + OS := unknown +endif + +# https://anki.tenderapp.com/discussions/beta-testing/1860-error-unused-type-ignore-comment +ifneq (${OS},Windows_NT) + MYPY_ARGS := --warn-unused-ignores +else + PYLINT_ARGS := --ignored-modules=win32file,pywintypes,socket,win32pipe +endif .DELETE_ON_ERROR: .SUFFIXES: @@ -48,7 +61,7 @@ TSDEPS := $(wildcard ts/src/*.ts) $(wildcard ts/scss/*.scss) .build/hooks: tools/genhooks_gui.py ../pylib/tools/hookslib.py python tools/genhooks_gui.py - black aqt/gui_hooks.py + python -m black aqt/gui_hooks.py @touch $@ BUILD_STEPS := .build/run-deps .build/dev-deps .build/js .build/ui aqt/buildinfo.py .build/hooks .build/i18n @@ -62,7 +75,7 @@ check: $(BUILD_STEPS) .build/mypy .build/test .build/fmt .build/imports .build/l .PHONY: fix fix: $(BUILD_STEPS) isort $(ISORTARGS) - black $(BLACKARGS) + python -m black $(BLACKARGS) (cd ts && npm run pretty) .PHONY: clean @@ -86,7 +99,7 @@ PYLIB := ../pylib CHECKDEPS := $(shell ${FIND} aqt tests -name '*.py' | grep -v buildinfo.py) .build/mypy: $(CHECKDEPS) .build/qt-stubs - mypy aqt + python -m mypy ${MYPY_ARGS} aqt @touch $@ .build/test: $(CHECKDEPS) @@ -94,7 +107,8 @@ CHECKDEPS := $(shell ${FIND} aqt tests -name '*.py' | grep -v buildinfo.py) @touch $@ .build/lint: $(CHECKDEPS) - pylint -j 0 --rcfile=.pylintrc -f colorized --extension-pkg-whitelist=PyQt5,ankirspy aqt tests setup.py + python -m pylint -j 0 --rcfile=.pylintrc -f colorized ${PYLINT_ARGS} \ + --extension-pkg-whitelist=PyQt5,ankirspy aqt tests setup.py @touch $@ .build/imports: $(CHECKDEPS) @@ -102,7 +116,7 @@ CHECKDEPS := $(shell ${FIND} aqt tests -name '*.py' | grep -v buildinfo.py) @touch $@ .build/fmt: $(CHECKDEPS) - black --check $(BLACKARGS) + python -m black --check $(BLACKARGS) @touch $@ .build/qt-stubs: diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 4c38a70ec..bb587a78a 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -865,7 +865,7 @@ QTableView {{ gridline-color: {grid} }} def _onRowChanged(self, current, previous) -> None: update = self.updateTitle() show = self.model.cards and update == 1 - self.form.splitter.widget(1).setVisible(not not show) + self.form.splitter.widget(1).setVisible(bool(show)) idx = self.form.tableView.selectionModel().currentIndex() if idx.isValid(): self.card = self.model.getCard(idx) @@ -1660,12 +1660,12 @@ where id in %s""" and self._previewState == "answer" and not self._previewBothSides ) - self._previewPrev.setEnabled(not not (self.singleCard and canBack)) + self._previewPrev.setEnabled(bool(self.singleCard and canBack)) canForward = ( self.currentRow() < self.model.rowCount(None) - 1 or self._previewState == "question" ) - self._previewNext.setEnabled(not not (self.singleCard and canForward)) + self._previewNext.setEnabled(bool(self.singleCard and canForward)) def _closePreview(self): if self._previewWindow: @@ -1925,7 +1925,7 @@ update cards set usn=?, mod=?, did=? where id in """ ###################################################################### def isSuspended(self): - return not not (self.card and self.card.queue == QUEUE_TYPE_SUSPENDED) + return bool(self.card and self.card.queue == QUEUE_TYPE_SUSPENDED) def onSuspend(self): self.editor.saveNow(self._onSuspend) @@ -1986,7 +1986,7 @@ update cards set usn=?, mod=?, did=? where id in """ self.deleteTags(tags="marked", label=False) def isMarked(self): - return not not (self.card and self.card.note().hasTag("Marked")) + return bool(self.card and self.card.note().hasTag("Marked")) # Repositioning ###################################################################### diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index ed183e40e..b269df969 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -63,9 +63,18 @@ _html = """ html { background: %s; } #topbutsOuter { background: %s; } -
%s
-
- +
+
+%s +
+
+
+
+ """ # caller is responsible for resetting note on reset @@ -83,6 +92,7 @@ class Editor: self.setupWeb() self.setupShortcuts() self.setupTags() + gui_hooks.editor_did_init(self) # Initial setup ############################################################ @@ -117,20 +127,35 @@ class Editor: # The color selection buttons do not use an icon so the HTML must be specified manually tip = _("Set foreground colour (F7)") righttopbtns.append( - """""".format( + """ """.format( tip ) ) tip = _("Change colour (F8)") righttopbtns.extend( [ - """""".format( + """""".format( tip ), self._addButton( @@ -249,18 +274,23 @@ class Editor: theclass = "linkb" if not disables: theclass += " perm" - return ( - '''""".format( - imgelm=imgelm, - cmd=cmd, - tip=tip, - labelelm=labelelm, - id=idstr, - togglesc=toggleScript, - theclass=theclass, - ) + return """ """.format( + imgelm=imgelm, + cmd=cmd, + tip=tip, + labelelm=labelelm, + id=idstr, + togglesc=toggleScript, + theclass=theclass, ) def setupShortcuts(self) -> None: @@ -426,6 +456,7 @@ class Editor: json.dumps(focusTo), json.dumps(self.note.id), ) + js = gui_hooks.editor_will_load_note(js, self.note, self) self.web.evalWithCallback(js, oncallback) def fonts(self) -> List[Tuple[str, int, bool]]: @@ -466,9 +497,10 @@ class Editor: return True m = self.note.model() for c, f in enumerate(self.note.fields): + f = f.replace("
", "").strip() notChangedvalues = {"", "
"} if previousNote and m["flds"][c]["sticky"]: - notChangedvalues.add(previousNote.fields[c]) + notChangedvalues.add(previousNote.fields[c].replace("
", "").strip()) if f not in notChangedvalues: return False return True diff --git a/qt/aqt/gui_hooks.py b/qt/aqt/gui_hooks.py index a066504ab..aecaaec3c 100644 --- a/qt/aqt/gui_hooks.py +++ b/qt/aqt/gui_hooks.py @@ -1028,6 +1028,30 @@ class _EditorDidFocusFieldHook: editor_did_focus_field = _EditorDidFocusFieldHook() +class _EditorDidInitHook: + _hooks: List[Callable[["aqt.editor.Editor"], None]] = [] + + def append(self, cb: Callable[["aqt.editor.Editor"], None]) -> None: + """(editor: aqt.editor.Editor)""" + self._hooks.append(cb) + + def remove(self, cb: Callable[["aqt.editor.Editor"], None]) -> None: + if cb in self._hooks: + self._hooks.remove(cb) + + def __call__(self, editor: aqt.editor.Editor) -> None: + for hook in self._hooks: + try: + hook(editor) + except: + # if the hook fails, remove it + self._hooks.remove(hook) + raise + + +editor_did_init = _EditorDidInitHook() + + class _EditorDidInitButtonsHook: _hooks: List[Callable[[List, "aqt.editor.Editor"], None]] = [] @@ -1183,6 +1207,40 @@ class _EditorWebViewDidInitHook: editor_web_view_did_init = _EditorWebViewDidInitHook() +class _EditorWillLoadNoteFilter: + """Allows changing the javascript commands to load note before + executing it and do change in the QT editor.""" + + _hooks: List[Callable[[str, "anki.notes.Note", "aqt.editor.Editor"], str]] = [] + + def append( + self, cb: Callable[[str, "anki.notes.Note", "aqt.editor.Editor"], str] + ) -> None: + """(js: str, note: anki.notes.Note, editor: aqt.editor.Editor)""" + self._hooks.append(cb) + + def remove( + self, cb: Callable[[str, "anki.notes.Note", "aqt.editor.Editor"], str] + ) -> None: + if cb in self._hooks: + self._hooks.remove(cb) + + def __call__( + self, js: str, note: anki.notes.Note, editor: aqt.editor.Editor + ) -> str: + for filter in self._hooks: + try: + js = filter(js, note, editor) + except: + # if the hook fails, remove it + self._hooks.remove(filter) + raise + return js + + +editor_will_load_note = _EditorWillLoadNoteFilter() + + class _EditorWillShowContextMenuHook: _hooks: List[Callable[["aqt.editor.EditorWebView", QMenu], None]] = [] diff --git a/qt/aqt/main.py b/qt/aqt/main.py index baf5c49f9..7e84f59aa 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -1462,8 +1462,8 @@ will be lost. Continue?""" # make sure ctypes is bundled from ctypes import windll, wintypes # type: ignore - _dummy = windll - _dummy = wintypes + _dummy1 = windll + _dummy2 = wintypes def maybeHideAccelerators(self, tgt: Optional[Any] = None) -> None: if not self.hideMenuAccels: diff --git a/qt/mypy.ini b/qt/mypy.ini index 5d5a5ca40..a69375016 100644 --- a/qt/mypy.ini +++ b/qt/mypy.ini @@ -6,7 +6,6 @@ show_error_codes = true disallow_untyped_decorators = True warn_redundant_casts = True warn_unused_configs = True -warn_unused_ignores = True [mypy-win32file] ignore_missing_imports = True diff --git a/qt/po/scripts/copy-qt-files b/qt/po/scripts/copy-qt-files index 248ca491c..a57ff947a 100755 --- a/qt/po/scripts/copy-qt-files +++ b/qt/po/scripts/copy-qt-files @@ -13,4 +13,4 @@ case "$(uname -s)" in ;; esac -rsync -a "$qtTranslations"/qt* "$out" +rsync -a "$qtTranslations/" "$out/" diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index be3232a92..a8887e10b 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -497,6 +497,14 @@ def emptyNewCard(): name="editor_web_view_did_init", args=["editor_web_view: aqt.editor.EditorWebView"], ), + Hook(name="editor_did_init", args=["editor: aqt.editor.Editor"],), + Hook( + name="editor_will_load_note", + args=["js: str", "note: anki.notes.Note", "editor: aqt.editor.Editor"], + return_type="str", + doc="""Allows changing the javascript commands to load note before + executing it and do change in the QT editor.""", + ), # Sound/video ################### Hook(name="av_player_will_play", args=["tag: anki.sound.AVTag"]), diff --git a/qt/tools/typecheck-setup.sh b/qt/tools/typecheck-setup.sh index acda0b599..f4b682452 100755 --- a/qt/tools/typecheck-setup.sh +++ b/qt/tools/typecheck-setup.sh @@ -22,6 +22,9 @@ case "$(uname -s)" in ;; esac -cmd="rsync -a ${TOOLS}/stubs/PyQt5/ ${modDir}/" - -$cmd > /dev/null 2>&1 || sudo $cmd +if [[ "w${OS}" == "wWindows_NT" ]]; +then + rsync -a "${TOOLS}/stubs/PyQt5/" "${modDir}/" +else + rsync -a "${TOOLS}/stubs/PyQt5/" "${modDir}/" || sudo rsync -a "${TOOLS}/stubs/PyQt5/" "${modDir}/" +fi diff --git a/qt/ts/src/editor.ts b/qt/ts/src/editor.ts index f3eba5c4a..eea5fd8c5 100644 --- a/qt/ts/src/editor.ts +++ b/qt/ts/src/editor.ts @@ -333,20 +333,33 @@ function setFields(fields) { if (!f) { f = "
"; } - txt += `${n}`; - txt += `
${f}
`; - txt += ""; + txt += ` + + ${n} + + + +
${f}
+ + `; } - $("#fields").html( - "" + - txt + - "
" - ); + $("#fields").html(` + +${txt} +
`); maybeDisableButtons(); } diff --git a/rslib/Cargo.toml b/rslib/Cargo.toml index 627d96efa..906fb1692 100644 --- a/rslib/Cargo.toml +++ b/rslib/Cargo.toml @@ -40,11 +40,16 @@ serde_repr = "0.1.5" num_enum = "0.4.2" unicase = "2.6.0" -[target.'cfg(target_vendor="apple")'.dependencies] -rusqlite = { version = "0.21.0", features = ["trace", "functions", "collation"] } +# pinned until rusqlite 0.22 comes out +[target.'cfg(target_vendor="apple")'.dependencies.rusqlite] +git = "https://github.com/ankitects/rusqlite.git" +branch="nulsafe-text" +features = ["trace", "functions", "collation"] -[target.'cfg(not(target_vendor="apple"))'.dependencies] -rusqlite = { version = "0.21.0", features = ["trace", "functions", "collation", "bundled"] } +[target.'cfg(not(target_vendor="apple"))'.dependencies.rusqlite] +git = "https://github.com/ankitects/rusqlite.git" +branch="nulsafe-text" +features = ["trace", "functions", "collation", "bundled"] [target.'cfg(linux)'.dependencies] reqwest = { version = "0.10.1", features = ["json", "native-tls-vendored"] } diff --git a/rslib/build.rs b/rslib/build.rs index 7d147f5c0..ef384d339 100644 --- a/rslib/build.rs +++ b/rslib/build.rs @@ -108,6 +108,7 @@ fn main() -> std::io::Result<()> { let path = entry.path(); println!("cargo:rerun-if-changed=./ftl/{}", fname); buf += &fs::read_to_string(path)?; + buf.push('\n'); } } let combined_ftl = Path::new("src/i18n/ftl/template.ftl"); @@ -145,6 +146,7 @@ fn main() -> std::io::Result<()> { let path = entry.path(); println!("cargo:rerun-if-changed={:?}", entry.path()); buf += &fs::read_to_string(path)?; + buf.push('\n'); } langs .entry(lang_name) diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index 7e5b2f87a..b56510717 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -14,7 +14,7 @@ use crate::{ types::{ObjID, Usn}, }; use regex::Regex; -use rusqlite::{params, Connection, NO_PARAMS}; +use rusqlite::{functions::FunctionFlags, params, Connection, NO_PARAMS}; use std::cmp::Ordering; use std::{ borrow::Cow, @@ -71,58 +71,73 @@ fn open_or_create_collection_db(path: &Path) -> Result { /// to split provided fields and return field at zero-based index. /// If out of range, returns empty string. fn add_field_index_function(db: &Connection) -> rusqlite::Result<()> { - db.create_scalar_function("field_at_index", 2, true, |ctx| { - let mut fields = ctx.get_raw(0).as_str()?.split('\x1f'); - let idx: u16 = ctx.get(1)?; - Ok(fields.nth(idx as usize).unwrap_or("").to_string()) - }) + db.create_scalar_function( + "field_at_index", + 2, + FunctionFlags::SQLITE_DETERMINISTIC, + |ctx| { + let mut fields = ctx.get_raw(0).as_str()?.split('\x1f'); + let idx: u16 = ctx.get(1)?; + Ok(fields.nth(idx as usize).unwrap_or("").to_string()) + }, + ) } fn add_without_combining_function(db: &Connection) -> rusqlite::Result<()> { - db.create_scalar_function("without_combining", 1, true, |ctx| { - let text = ctx.get_raw(0).as_str()?; - Ok(match without_combining(text) { - Cow::Borrowed(_) => None, - Cow::Owned(o) => Some(o), - }) - }) + db.create_scalar_function( + "without_combining", + 1, + FunctionFlags::SQLITE_DETERMINISTIC, + |ctx| { + let text = ctx.get_raw(0).as_str()?; + Ok(match without_combining(text) { + Cow::Borrowed(_) => None, + Cow::Owned(o) => Some(o), + }) + }, + ) } /// Adds sql function regexp(regex, string) -> is_match /// Taken from the rusqlite docs fn add_regexp_function(db: &Connection) -> rusqlite::Result<()> { - db.create_scalar_function("regexp", 2, true, move |ctx| { - assert_eq!(ctx.len(), 2, "called with unexpected number of arguments"); + db.create_scalar_function( + "regexp", + 2, + FunctionFlags::SQLITE_DETERMINISTIC, + move |ctx| { + assert_eq!(ctx.len(), 2, "called with unexpected number of arguments"); - let saved_re: Option<&Regex> = ctx.get_aux(0)?; - let new_re = match saved_re { - None => { - let s = ctx.get::(0)?; - match Regex::new(&s) { - Ok(r) => Some(r), - Err(err) => return Err(rusqlite::Error::UserFunctionError(Box::new(err))), + let saved_re: Option<&Regex> = ctx.get_aux(0)?; + let new_re = match saved_re { + None => { + let s = ctx.get::(0)?; + match Regex::new(&s) { + Ok(r) => Some(r), + Err(err) => return Err(rusqlite::Error::UserFunctionError(Box::new(err))), + } } + Some(_) => None, + }; + + let is_match = { + let re = saved_re.unwrap_or_else(|| new_re.as_ref().unwrap()); + + let text = ctx + .get_raw(1) + .as_str() + .map_err(|e| rusqlite::Error::UserFunctionError(e.into()))?; + + re.is_match(text) + }; + + if let Some(re) = new_re { + ctx.set_aux(0, re); } - Some(_) => None, - }; - let is_match = { - let re = saved_re.unwrap_or_else(|| new_re.as_ref().unwrap()); - - let text = ctx - .get_raw(1) - .as_str() - .map_err(|e| rusqlite::Error::UserFunctionError(e.into()))?; - - re.is_match(text) - }; - - if let Some(re) = new_re { - ctx.set_aux(0, re); - } - - Ok(is_match) - }) + Ok(is_match) + }, + ) } /// Fetch schema version from database.