reader 3.11__tar.gz → 3.12__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {reader-3.11 → reader-3.12}/CHANGES.rst +22 -0
- {reader-3.11/src/reader.egg-info → reader-3.12}/PKG-INFO +1 -1
- {reader-3.11 → reader-3.12}/docs/dev.rst +7 -2
- {reader-3.11 → reader-3.12}/docs/guide.rst +23 -10
- {reader-3.11 → reader-3.12}/docs/internal.rst +26 -3
- {reader-3.11 → reader-3.12}/src/reader/__init__.py +1 -1
- {reader-3.11 → reader-3.12}/src/reader/_app/cli.py +0 -3
- {reader-3.11 → reader-3.12}/src/reader/_cli.py +3 -30
- reader-3.12/src/reader/_storage/__init__.py +39 -0
- reader-3.12/src/reader/_storage/_base.py +110 -0
- reader-3.12/src/reader/_storage/_changes.py +213 -0
- reader-3.12/src/reader/_storage/_entries.py +654 -0
- reader-3.12/src/reader/_storage/_feeds.py +404 -0
- {reader-3.11/src/reader → reader-3.12/src/reader/_storage}/_html_utils.py +3 -1
- reader-3.12/src/reader/_storage/_schema.py +284 -0
- reader-3.12/src/reader/_storage/_search.py +643 -0
- {reader-3.11/src/reader → reader-3.12/src/reader/_storage}/_sql_utils.py +94 -26
- {reader-3.11/src/reader → reader-3.12/src/reader/_storage}/_sqlite_utils.py +123 -104
- reader-3.12/src/reader/_storage/_tags.py +250 -0
- {reader-3.11 → reader-3.12}/src/reader/_types.py +152 -0
- {reader-3.11 → reader-3.12}/src/reader/_utils.py +0 -60
- {reader-3.11 → reader-3.12}/src/reader/core.py +1 -2
- {reader-3.11 → reader-3.12}/src/reader/exceptions.py +12 -0
- {reader-3.11 → reader-3.12}/src/reader/plugins/readtime.py +2 -2
- {reader-3.11 → reader-3.12}/src/reader/types.py +10 -0
- {reader-3.11 → reader-3.12/src/reader.egg-info}/PKG-INFO +1 -1
- {reader-3.11 → reader-3.12}/src/reader.egg-info/SOURCES.txt +12 -5
- {reader-3.11 → reader-3.12}/tests/conftest.py +0 -2
- reader-3.12/tests/test_changes.py +427 -0
- {reader-3.11 → reader-3.12}/tests/test_html_utils.py +1 -1
- {reader-3.11 → reader-3.12}/tests/test_reader.py +1 -3
- {reader-3.11 → reader-3.12}/tests/test_reader_context.py +4 -2
- {reader-3.11 → reader-3.12}/tests/test_reader_search.py +112 -276
- reader-3.12/tests/test_search.py +218 -0
- {reader-3.11 → reader-3.12}/tests/test_sql_utils.py +2 -2
- {reader-3.11 → reader-3.12}/tests/test_sqlite_utils.py +124 -89
- {reader-3.11 → reader-3.12}/tests/test_storage.py +41 -60
- {reader-3.11 → reader-3.12}/tox.ini +1 -1
- reader-3.11/src/reader/_search.py +0 -988
- reader-3.11/src/reader/_storage.py +0 -1808
- reader-3.11/tests/test_search.py +0 -204
- {reader-3.11 → reader-3.12}/LICENSE +0 -0
- {reader-3.11 → reader-3.12}/MANIFEST.in +0 -0
- {reader-3.11 → reader-3.12}/README.rst +0 -0
- {reader-3.11 → reader-3.12}/docs/Makefile +0 -0
- {reader-3.11 → reader-3.12}/docs/api.rst +0 -0
- {reader-3.11 → reader-3.12}/docs/app.rst +0 -0
- {reader-3.11 → reader-3.12}/docs/changelog.rst +0 -0
- {reader-3.11 → reader-3.12}/docs/cli.rst +0 -0
- {reader-3.11 → reader-3.12}/docs/conf.py +0 -0
- {reader-3.11 → reader-3.12}/docs/config.rst +0 -0
- {reader-3.11 → reader-3.12}/docs/contributing.rst +0 -0
- {reader-3.11 → reader-3.12}/docs/dev-app.rst +0 -0
- {reader-3.11 → reader-3.12}/docs/images/redesign-01.png +0 -0
- {reader-3.11 → reader-3.12}/docs/index.rst +0 -0
- {reader-3.11 → reader-3.12}/docs/install.rst +0 -0
- {reader-3.11 → reader-3.12}/docs/make.bat +0 -0
- {reader-3.11 → reader-3.12}/docs/plugins.rst +0 -0
- {reader-3.11 → reader-3.12}/docs/screenshots/dillo.png +0 -0
- {reader-3.11 → reader-3.12}/docs/screenshots/entries-feed.png +0 -0
- {reader-3.11 → reader-3.12}/docs/screenshots/entries.png +0 -0
- {reader-3.11 → reader-3.12}/docs/screenshots/entry-one.png +0 -0
- {reader-3.11 → reader-3.12}/docs/screenshots/entry-two.png +0 -0
- {reader-3.11 → reader-3.12}/docs/screenshots/feeds.png +0 -0
- {reader-3.11 → reader-3.12}/docs/screenshots/lynx.png +0 -0
- {reader-3.11 → reader-3.12}/docs/screenshots/search.png +0 -0
- {reader-3.11 → reader-3.12}/docs/tutorial.rst +0 -0
- {reader-3.11 → reader-3.12}/docs/why.rst +0 -0
- {reader-3.11 → reader-3.12}/examples/config.yaml +0 -0
- {reader-3.11 → reader-3.12}/examples/parser_only.py +0 -0
- {reader-3.11 → reader-3.12}/examples/podcast.py +0 -0
- {reader-3.11 → reader-3.12}/examples/terminal.py +0 -0
- {reader-3.11 → reader-3.12}/pyproject.toml +0 -0
- {reader-3.11 → reader-3.12}/run.sh +0 -0
- {reader-3.11 → reader-3.12}/scripts/.coverage +0 -0
- {reader-3.11 → reader-3.12}/scripts/backup.sh +0 -0
- {reader-3.11 → reader-3.12}/scripts/bench.py +0 -0
- {reader-3.11 → reader-3.12}/scripts/debug_storage_stats.py +0 -0
- {reader-3.11 → reader-3.12}/scripts/generate_import_all.py +0 -0
- {reader-3.11 → reader-3.12}/scripts/jscontrols.html +0 -0
- {reader-3.11 → reader-3.12}/scripts/jscontrols.py +0 -0
- {reader-3.11 → reader-3.12}/scripts/lines.sh +0 -0
- {reader-3.11 → reader-3.12}/scripts/release.py +0 -0
- {reader-3.11 → reader-3.12}/setup.cfg +0 -0
- {reader-3.11 → reader-3.12}/src/reader/__main__.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_app/__init__.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_app/api_thing.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_app/static/controls.js +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_app/static/style.css +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_app/templates/add_entry.html +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_app/templates/entries.html +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_app/templates/entry.html +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_app/templates/feeds.html +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_app/templates/layout.html +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_app/templates/macros.html +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_app/templates/metadata.html +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_app/templates/tags.html +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_app/wsgi.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_config.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_hash_utils.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_parser/__init__.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_parser/_http_utils.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_parser/_lazy.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_parser/_url_utils.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_parser/feedparser.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_parser/file.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_parser/http.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_parser/jsonfeed.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_parser/requests/__init__.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_parser/requests/_lazy.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_plugins/__init__.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_plugins/cli_status.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_plugins/enclosure_tags.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_plugins/preview_feed_list.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_plugins/share.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_plugins/sqlite_releases.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_plugins/templates/preview_feed_list.html +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_plugins/timer.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_update.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/__init__.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/__init__.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/api.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/datetimes/__init__.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/datetimes/asctime.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/datetimes/greek.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/datetimes/hungarian.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/datetimes/iso8601.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/datetimes/korean.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/datetimes/perforce.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/datetimes/rfc822.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/datetimes/w3dtf.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/encodings.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/exceptions.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/html.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/http.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/mixin.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/namespaces/_base.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/namespaces/admin.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/namespaces/cc.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/namespaces/dc.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/namespaces/georss.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/namespaces/itunes.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/namespaces/mediarss.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/namespaces/psc.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/parsers/json.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/parsers/loose.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/parsers/strict.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/py.typed +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/sanitizer.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/sgml.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/urls.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/_vendor/feedparser/util.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/plugins/__init__.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/plugins/enclosure_dedupe.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/plugins/entry_dedupe.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/plugins/mark_as_read.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/plugins/ua_fallback.py +0 -0
- {reader-3.11 → reader-3.12}/src/reader/py.typed +0 -0
- {reader-3.11 → reader-3.12}/src/reader.egg-info/dependency_links.txt +0 -0
- {reader-3.11 → reader-3.12}/src/reader.egg-info/requires.txt +0 -0
- {reader-3.11 → reader-3.12}/src/reader.egg-info/top_level.txt +0 -0
- {reader-3.11 → reader-3.12}/tests/data/10.json +0 -0
- {reader-3.11 → reader-3.12}/tests/data/10.json.py +0 -0
- {reader-3.11 → reader-3.12}/tests/data/custom +0 -0
- {reader-3.11 → reader-3.12}/tests/data/empty.atom +0 -0
- {reader-3.11 → reader-3.12}/tests/data/empty.atom.py +0 -0
- {reader-3.11 → reader-3.12}/tests/data/empty.json +0 -0
- {reader-3.11 → reader-3.12}/tests/data/empty.json.py +0 -0
- {reader-3.11 → reader-3.12}/tests/data/empty.rss +0 -0
- {reader-3.11 → reader-3.12}/tests/data/empty.rss.py +0 -0
- {reader-3.11 → reader-3.12}/tests/data/full.atom +0 -0
- {reader-3.11 → reader-3.12}/tests/data/full.atom.py +0 -0
- {reader-3.11 → reader-3.12}/tests/data/full.json +0 -0
- {reader-3.11 → reader-3.12}/tests/data/full.json.py +0 -0
- {reader-3.11 → reader-3.12}/tests/data/full.rss +0 -0
- {reader-3.11 → reader-3.12}/tests/data/full.rss.py +0 -0
- {reader-3.11 → reader-3.12}/tests/data/invalid.json +0 -0
- {reader-3.11 → reader-3.12}/tests/data/invalid.json.py +0 -0
- {reader-3.11 → reader-3.12}/tests/data/relative.atom +0 -0
- {reader-3.11 → reader-3.12}/tests/data/relative.atom.py +0 -0
- {reader-3.11 → reader-3.12}/tests/data/relative.rss +0 -0
- {reader-3.11 → reader-3.12}/tests/data/relative.rss.py +0 -0
- {reader-3.11 → reader-3.12}/tests/data/sqlite_releases.html +0 -0
- {reader-3.11 → reader-3.12}/tests/data/unknown.json +0 -0
- {reader-3.11 → reader-3.12}/tests/data/unknown.json.py +0 -0
- {reader-3.11 → reader-3.12}/tests/fakeparser.py +0 -0
- {reader-3.11 → reader-3.12}/tests/reader_methods.py +0 -0
- {reader-3.11 → reader-3.12}/tests/reader_test_plugins/__init__.py +0 -0
- {reader-3.11 → reader-3.12}/tests/reader_test_plugins/good.py +0 -0
- {reader-3.11 → reader-3.12}/tests/reader_test_plugins/init_error.py +0 -0
- {reader-3.11 → reader-3.12}/tests/reader_test_plugins/missing_dependency.py +0 -0
- {reader-3.11 → reader-3.12}/tests/reader_test_plugins/missing_entry_point.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test__types.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_app.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_app_wsgi.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_bench.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_cli.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_config.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_exceptions.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_hash_utils.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_lazy_imports.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_parser.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_plugins_cli_status.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_plugins_enclosure_dedupe.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_plugins_entry_dedupe.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_plugins_mark_as_read.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_plugins_preview_feed_list.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_plugins_readtime.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_plugins_sqlite_releases.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_plugins_ua_fallback.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_reader_counts.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_reader_deprecations.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_reader_filter.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_reader_hooks.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_reader_integration.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_reader_plugins.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_reader_private.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_reader_sort.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_tags.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_test_utils.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_types.py +0 -0
- {reader-3.11 → reader-3.12}/tests/test_utils.py +0 -0
- {reader-3.11 → reader-3.12}/tests/utils.py +0 -0
|
@@ -6,6 +6,28 @@ Changelog
|
|
|
6
6
|
:noindex:
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
Version 3.12
|
|
10
|
+
------------
|
|
11
|
+
|
|
12
|
+
Released 2024-03-05
|
|
13
|
+
|
|
14
|
+
* Split the :ref:`full-text search <fts>` index into a separate,
|
|
15
|
+
attached database.
|
|
16
|
+
(:issue:`323`)
|
|
17
|
+
* Require at least SQLite 3.18.
|
|
18
|
+
Previously, *reader* core required 3.15,
|
|
19
|
+
and only :meth:`~Reader.update_search` required 3.18.
|
|
20
|
+
(:issue:`323`)
|
|
21
|
+
* Enable `write-ahead logging`_ only once, when the database is created,
|
|
22
|
+
instead of every time it is opened.
|
|
23
|
+
(:issue:`323`)
|
|
24
|
+
* Vacuum the main database after migrations. (:issue:`323`)
|
|
25
|
+
* Add an internal :ref:`change tracking API <changes>`
|
|
26
|
+
to formalize how search keeps in sync with storage.
|
|
27
|
+
(:issue:`323`)
|
|
28
|
+
* Refactor storage internals. (:issue:`323`)
|
|
29
|
+
|
|
30
|
+
|
|
9
31
|
Version 3.11
|
|
10
32
|
------------
|
|
11
33
|
|
|
@@ -57,7 +57,6 @@ but I will prioritize supporting :doc:`contributors <contributing>`
|
|
|
57
57
|
|
|
58
58
|
* :ref:`searchable tag values <searchable tags>`, e.g. for comments
|
|
59
59
|
* :ref:`unification with entry.read/important <entry flag unification>`
|
|
60
|
-
* filter entries by entry tags, :issue:`328`
|
|
61
60
|
* optimistic locking, :issue:`308`
|
|
62
61
|
* filter tags by prefix, :issue:`309`
|
|
63
62
|
|
|
@@ -345,6 +344,8 @@ Storage internal API documented in version 3.10 (November 2023) in :issue:`325`.
|
|
|
345
344
|
Database optimization
|
|
346
345
|
~~~~~~~~~~~~~~~~~~~~~
|
|
347
346
|
|
|
347
|
+
Optimization lessons learned while fixing "database is locked" errors: :issue:`175#issuecomment-657495233`.
|
|
348
|
+
|
|
348
349
|
Some general guidance on schema/index design: :issue:`327#issuecomment-1859147186`.
|
|
349
350
|
|
|
350
351
|
Speeding up ``get_entries(sort='recent')``:
|
|
@@ -449,7 +450,8 @@ https://death.andgravity.com/query-builder
|
|
|
449
450
|
Pagination for methods that return iterators
|
|
450
451
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
451
452
|
|
|
452
|
-
Why do it for the private implementation: :issue:`167#issuecomment-626753299
|
|
453
|
+
Why do it for the private implementation: :issue:`167#issuecomment-626753299`;
|
|
454
|
+
|
|
453
455
|
|
|
454
456
|
Detailed requirements and API discussion for public pagination: :issue:`196#issuecomment-706038363`.
|
|
455
457
|
|
|
@@ -464,6 +466,9 @@ From the initial issue:
|
|
|
464
466
|
|
|
465
467
|
Enabling search by default, and alternative search APIs: :issue:`252`.
|
|
466
468
|
|
|
469
|
+
Change tracking API: :issue:`323#issuecomment-1930756417`, model validated in
|
|
470
|
+
`this gist <https://gist.github.com/lemon24/558955ad82ba2e4f50c0184c630c668c>`_.
|
|
471
|
+
|
|
467
472
|
External resources:
|
|
468
473
|
|
|
469
474
|
* Comprehensive, although a bit old (2017): `What every software engineer should know about search <https://medium.com/startup-grind/what-every-software-engineer-should-know-about-search-27d1df99f80d>`_ (`full version <http://webcache.googleusercontent.com/search?q=cache:https://medium.com/startup-grind/what-every-software-engineer-should-know-about-search-27d1df99f80d&sca_esv=570067020&prmd=ivn&strip=1&vwsrc=0>`_)
|
|
@@ -127,6 +127,8 @@ since each connection would be to a *different* database::
|
|
|
127
127
|
reader.exceptions.StorageError: usage error: cannot use a private database from threads other than the creating thread
|
|
128
128
|
|
|
129
129
|
|
|
130
|
+
.. _backups:
|
|
131
|
+
|
|
130
132
|
Back-ups
|
|
131
133
|
~~~~~~~~
|
|
132
134
|
|
|
@@ -439,16 +441,27 @@ the entries that changed since the last call,
|
|
|
439
441
|
so it is OK to call it relatively often.
|
|
440
442
|
|
|
441
443
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
444
|
+
Search can be turned on/off through the
|
|
445
|
+
:meth:`~Reader.enable_search()` / :meth:`~Reader.disable_search()` methods
|
|
446
|
+
(persistent across instances using the same database),
|
|
447
|
+
or the ``search_enabled`` argument of :func:`make_reader`;
|
|
448
|
+
by default, search is enabled automatically
|
|
449
|
+
on the first :meth:`~Reader.update_search()` call.
|
|
450
|
+
If search is enabled,
|
|
451
|
+
you should call :meth:`~Reader.update_search()` regularly
|
|
452
|
+
to prevent unprocesses changes from accumulating over time.
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
Because the search index can be almost as large as the main database,
|
|
456
|
+
the default implementation splits it into a separate, attached database,
|
|
457
|
+
which allows :ref:`backing up <backups>` the main database separately;
|
|
458
|
+
for a reader created with ``make_reader('db.sqlite')``,
|
|
459
|
+
the search index will be in ``db.sqlite.search``.
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
.. versionchanged:: 3.12
|
|
463
|
+
Split the full-text search index into a separate database.
|
|
464
|
+
|
|
452
465
|
|
|
453
466
|
|
|
454
467
|
|
|
@@ -143,9 +143,11 @@ but at least one other implementation needs to exists before that.
|
|
|
143
143
|
|
|
144
144
|
Currently, search is tightly-bound to a storage implementation
|
|
145
145
|
(see :meth:`~.BoundSearchStorageType.make_search`).
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
While the :ref:`change tracking API <changes>` allows
|
|
147
|
+
search implementations to keep in sync with text content changes,
|
|
148
|
+
there is no convenient way for :meth:`SearchType.search_entries`
|
|
149
|
+
to filter/sort results without storage cooperation;
|
|
150
|
+
:class:`StorageType` will need
|
|
149
151
|
additional capabilities to support this.
|
|
150
152
|
|
|
151
153
|
|
|
@@ -167,6 +169,27 @@ but at least one other implementation needs to exists before that.
|
|
|
167
169
|
:members:
|
|
168
170
|
|
|
169
171
|
|
|
172
|
+
.. _changes:
|
|
173
|
+
|
|
174
|
+
.. autoclass:: ChangeTrackingStorageType()
|
|
175
|
+
:members:
|
|
176
|
+
:show-inheritance:
|
|
177
|
+
|
|
178
|
+
.. autoclass:: ChangeTrackerType()
|
|
179
|
+
:members:
|
|
180
|
+
|
|
181
|
+
.. autoclass:: Change
|
|
182
|
+
:members:
|
|
183
|
+
|
|
184
|
+
.. autoclass:: Action
|
|
185
|
+
:members:
|
|
186
|
+
|
|
187
|
+
.. autoattribute:: reader.Entry._sequence
|
|
188
|
+
|
|
189
|
+
.. autoexception:: reader.exceptions.ChangeTrackingNotEnabledError
|
|
190
|
+
:show-inheritance:
|
|
191
|
+
|
|
192
|
+
|
|
170
193
|
Data objects
|
|
171
194
|
~~~~~~~~~~~~
|
|
172
195
|
|
|
@@ -35,9 +35,6 @@ def serve(config, host, port, plugin, verbose):
|
|
|
35
35
|
if plugin:
|
|
36
36
|
config['app']['plugins'] = dict.fromkeys(plugin)
|
|
37
37
|
|
|
38
|
-
# TODO: remove this once we make debug_storage a storage_arg
|
|
39
|
-
config['default']['reader'].pop('debug_storage', None)
|
|
40
|
-
|
|
41
38
|
app = create_app(config)
|
|
42
39
|
app.wsgi_app = make_add_response_headers_middleware(
|
|
43
40
|
app.wsgi_app,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import functools
|
|
2
|
-
import json
|
|
3
2
|
import logging
|
|
4
3
|
import os.path
|
|
5
4
|
import shutil
|
|
@@ -17,7 +16,6 @@ from ._config import make_reader_config
|
|
|
17
16
|
from ._config import make_reader_from_config
|
|
18
17
|
from ._plugins import Loader
|
|
19
18
|
from ._plugins import LoaderError
|
|
20
|
-
from ._sqlite_utils import DebugConnection
|
|
21
19
|
|
|
22
20
|
|
|
23
21
|
APP_NAME = reader.__name__
|
|
@@ -45,23 +43,7 @@ def abort(message, *args, **kwargs):
|
|
|
45
43
|
raise click.ClickException(message.format(*args, **kwargs))
|
|
46
44
|
|
|
47
45
|
|
|
48
|
-
def make_reader_with_plugins(
|
|
49
|
-
if debug_storage:
|
|
50
|
-
# TODO: the web app should be able to do this too
|
|
51
|
-
|
|
52
|
-
log_debug = logging.getLogger('reader._storage').debug
|
|
53
|
-
pid = os.getpid()
|
|
54
|
-
|
|
55
|
-
class Connection(DebugConnection):
|
|
56
|
-
_io_counters = True
|
|
57
|
-
|
|
58
|
-
@staticmethod
|
|
59
|
-
def _log_method(data):
|
|
60
|
-
data['pid'] = pid
|
|
61
|
-
log_debug(json.dumps(data))
|
|
62
|
-
|
|
63
|
-
kwargs['_storage_factory'] = Connection
|
|
64
|
-
|
|
46
|
+
def make_reader_with_plugins(**kwargs):
|
|
65
47
|
try:
|
|
66
48
|
return make_reader_from_config(**kwargs)
|
|
67
49
|
except StorageError as e:
|
|
@@ -170,7 +152,7 @@ def pass_reader(fn):
|
|
|
170
152
|
@functools.wraps(fn)
|
|
171
153
|
def wrapper(*args, **kwargs):
|
|
172
154
|
ctx = click.get_current_context().find_root()
|
|
173
|
-
# TODO: replace with ctx.obj.make_reader('cli')
|
|
155
|
+
# TODO: replace with ctx.obj.make_reader('cli')
|
|
174
156
|
reader = make_reader_with_plugins(**ctx.obj.merged('cli').get('reader', {}))
|
|
175
157
|
ctx.call_on_close(reader.close)
|
|
176
158
|
return fn(reader, *args, **kwargs)
|
|
@@ -215,14 +197,9 @@ def pass_reader(fn):
|
|
|
215
197
|
"If not provided, don't open local feeds."
|
|
216
198
|
),
|
|
217
199
|
)
|
|
218
|
-
@click.option(
|
|
219
|
-
'--debug-storage/--no-debug-storage',
|
|
220
|
-
hidden=True,
|
|
221
|
-
help="NOT TESTED. With -vv, log storage database calls.",
|
|
222
|
-
)
|
|
223
200
|
@click.version_option(reader.__version__, message='%(prog)s %(version)s')
|
|
224
201
|
@click.pass_obj
|
|
225
|
-
def cli(config, db, plugin, cli_plugin, feed_root
|
|
202
|
+
def cli(config, db, plugin, cli_plugin, feed_root):
|
|
226
203
|
# TODO: mention in docs that --db/--plugin/envvars ALWAYS override the config
|
|
227
204
|
# (same for wsgi envvars)
|
|
228
205
|
# NOTE: we can never use click defaults for --db/--plugin, because they would override the config always
|
|
@@ -247,10 +224,6 @@ def cli(config, db, plugin, cli_plugin, feed_root, debug_storage):
|
|
|
247
224
|
if feed_root is not None:
|
|
248
225
|
config['default']['reader']['feed_root'] = feed_root
|
|
249
226
|
|
|
250
|
-
# until we make debug_storage a proper make_reader argument,
|
|
251
|
-
# and we get rid of make_reader_with_plugins
|
|
252
|
-
config['default']['reader']['debug_storage'] = debug_storage
|
|
253
|
-
|
|
254
227
|
try:
|
|
255
228
|
loader = Loader()
|
|
256
229
|
loader.init(config, config.merged('cli').get('plugins', {}))
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .._types import ChangeTrackerType
|
|
6
|
+
from .._types import SearchType
|
|
7
|
+
from ._base import StorageBase
|
|
8
|
+
from ._changes import Changes
|
|
9
|
+
from ._entries import EntriesMixin
|
|
10
|
+
from ._feeds import FeedsMixin
|
|
11
|
+
from ._tags import TagsMixin
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Row value support was added in 3.15.
|
|
15
|
+
# pragma_*() tabled-valued functions were added in 3.16.
|
|
16
|
+
# last_insert_rowid() support for FTS5 was added in 3.18.
|
|
17
|
+
MINIMUM_SQLITE_VERSION = (3, 18)
|
|
18
|
+
|
|
19
|
+
# Both storage and search use the JSON1 extension.
|
|
20
|
+
REQUIRED_SQLITE_FUNCTIONS = ['json']
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Storage(FeedsMixin, EntriesMixin, TagsMixin, StorageBase):
|
|
24
|
+
"""Data access object used for all storage (except search).
|
|
25
|
+
|
|
26
|
+
This class is split into per-domain mixins, add new methods accordingly.
|
|
27
|
+
|
|
28
|
+
Add a test_storage.py::test_errors_locked test for each new public method.
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, path: str, timeout: float | None = None):
|
|
33
|
+
super().__init__(path, timeout)
|
|
34
|
+
self.changes: ChangeTrackerType = Changes(self)
|
|
35
|
+
|
|
36
|
+
def make_search(self) -> SearchType:
|
|
37
|
+
from ._search import Search
|
|
38
|
+
|
|
39
|
+
return Search(self)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sqlite3
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from collections.abc import Iterable
|
|
9
|
+
from functools import partial
|
|
10
|
+
from typing import Any
|
|
11
|
+
from typing import TypeVar
|
|
12
|
+
|
|
13
|
+
from . import _sqlite_utils
|
|
14
|
+
from ..exceptions import StorageError
|
|
15
|
+
from ._sql_utils import paginated_query
|
|
16
|
+
from ._sql_utils import Query
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
APPLICATION_ID = b'read'
|
|
20
|
+
|
|
21
|
+
_T = TypeVar('_T')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# also used by tests
|
|
25
|
+
CONNECTION_CLS = sqlite3.Connection
|
|
26
|
+
|
|
27
|
+
debug = os.environ.get('READER_DEBUG_STORAGE', '')
|
|
28
|
+
assert set(debug) <= {'m', 't', 'i'}, f"invalid READER_DEBUG_STORAGE={debug}"
|
|
29
|
+
|
|
30
|
+
if debug: # pragma: no cover
|
|
31
|
+
|
|
32
|
+
class CONNECTION_CLS(_sqlite_utils.DebugConnection): # type: ignore # noqa: F811
|
|
33
|
+
_set_trace = 't' in debug
|
|
34
|
+
_io_counters = 'i' in debug
|
|
35
|
+
_pid = os.getpid()
|
|
36
|
+
|
|
37
|
+
def _log_method(self, data): # type: ignore
|
|
38
|
+
data['pid'] = self._pid
|
|
39
|
+
print('STORAGE_DEBUG', json.dumps(data), file=sys.stderr)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
wrap_exceptions = partial(_sqlite_utils.wrap_exceptions, StorageError)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class StorageBase:
|
|
46
|
+
#: Private storage API.
|
|
47
|
+
chunk_size = 2**8
|
|
48
|
+
|
|
49
|
+
@wrap_exceptions(message="while opening database")
|
|
50
|
+
def __init__(self, path: str, timeout: float | None = None):
|
|
51
|
+
kwargs: dict[str, Any] = {'factory': CONNECTION_CLS}
|
|
52
|
+
if timeout is not None:
|
|
53
|
+
kwargs['timeout'] = timeout
|
|
54
|
+
|
|
55
|
+
self.factory = _sqlite_utils.LocalConnectionFactory(path, **kwargs)
|
|
56
|
+
db = self.factory()
|
|
57
|
+
try:
|
|
58
|
+
self.setup_db(db)
|
|
59
|
+
except BaseException:
|
|
60
|
+
db.close()
|
|
61
|
+
raise
|
|
62
|
+
|
|
63
|
+
self.path = path
|
|
64
|
+
self.timeout = timeout
|
|
65
|
+
|
|
66
|
+
def get_db(self) -> sqlite3.Connection:
|
|
67
|
+
return self.factory()
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def setup_db(db: sqlite3.Connection) -> None:
|
|
71
|
+
"""Private storage API (used by tests)."""
|
|
72
|
+
from ._schema import MIGRATION
|
|
73
|
+
from . import MINIMUM_SQLITE_VERSION, REQUIRED_SQLITE_FUNCTIONS
|
|
74
|
+
|
|
75
|
+
return _sqlite_utils.setup_db(
|
|
76
|
+
db,
|
|
77
|
+
migration=MIGRATION,
|
|
78
|
+
id=APPLICATION_ID,
|
|
79
|
+
minimum_sqlite_version=MINIMUM_SQLITE_VERSION,
|
|
80
|
+
required_sqlite_functions=REQUIRED_SQLITE_FUNCTIONS,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
@wrap_exceptions()
|
|
84
|
+
def __enter__(self) -> None:
|
|
85
|
+
self.factory.__enter__()
|
|
86
|
+
|
|
87
|
+
@wrap_exceptions()
|
|
88
|
+
def __exit__(self, *_: Any) -> None:
|
|
89
|
+
self.factory.__exit__()
|
|
90
|
+
|
|
91
|
+
@wrap_exceptions()
|
|
92
|
+
def close(self) -> None:
|
|
93
|
+
self.factory.close()
|
|
94
|
+
|
|
95
|
+
def paginated_query(
|
|
96
|
+
self,
|
|
97
|
+
make_query: Callable[[], tuple[Query, dict[str, Any]]],
|
|
98
|
+
limit: int | None = None,
|
|
99
|
+
last: tuple[Any, ...] | None = None,
|
|
100
|
+
row_factory: Callable[[tuple[Any, ...]], _T] | None = None,
|
|
101
|
+
) -> Iterable[_T]:
|
|
102
|
+
with wrap_exceptions():
|
|
103
|
+
yield from paginated_query(
|
|
104
|
+
self.get_db(),
|
|
105
|
+
make_query,
|
|
106
|
+
self.chunk_size,
|
|
107
|
+
limit or 0,
|
|
108
|
+
last,
|
|
109
|
+
row_factory,
|
|
110
|
+
)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
from typing import Any
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from .._types import Action
|
|
8
|
+
from .._types import Change
|
|
9
|
+
from ..exceptions import ChangeTrackingNotEnabledError
|
|
10
|
+
from ._base import wrap_exceptions
|
|
11
|
+
from ._sql_utils import parse_schema
|
|
12
|
+
from ._sql_utils import Query
|
|
13
|
+
from ._sqlite_utils import ddl_transaction
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
16
|
+
from ._base import StorageBase
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
ENABLED_EXC = {'no such table': lambda _: ChangeTrackingNotEnabledError()}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Changes:
|
|
23
|
+
def __init__(self, storage: StorageBase):
|
|
24
|
+
self.storage = storage
|
|
25
|
+
|
|
26
|
+
@wrap_exceptions()
|
|
27
|
+
def enable(self) -> None:
|
|
28
|
+
with ddl_transaction(self.storage.get_db()) as db:
|
|
29
|
+
try:
|
|
30
|
+
self._enable(db)
|
|
31
|
+
except sqlite3.OperationalError as e:
|
|
32
|
+
if "table changes already exists" in str(e).lower():
|
|
33
|
+
return
|
|
34
|
+
raise # pragma: no cover
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def _enable(cls, db: sqlite3.Connection) -> None:
|
|
38
|
+
assert db.in_transaction
|
|
39
|
+
for objects in SCHEMA.values():
|
|
40
|
+
for object in objects.values():
|
|
41
|
+
object.create(db)
|
|
42
|
+
db.execute("UPDATE entries SET sequence = randomblob(16)")
|
|
43
|
+
db.execute(
|
|
44
|
+
"""
|
|
45
|
+
INSERT INTO changes
|
|
46
|
+
SELECT sequence, feed, id, '', 1 FROM entries
|
|
47
|
+
"""
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@wrap_exceptions()
|
|
51
|
+
def disable(self) -> None:
|
|
52
|
+
with ddl_transaction(self.storage.get_db()) as db:
|
|
53
|
+
self._disable(db)
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def _disable(cls, db: sqlite3.Connection) -> None:
|
|
57
|
+
assert db.in_transaction
|
|
58
|
+
for objects in SCHEMA.values():
|
|
59
|
+
for object in objects.values():
|
|
60
|
+
db.execute(f"DROP {object.type} IF EXISTS {object.name}")
|
|
61
|
+
db.execute("UPDATE entries SET sequence = NULL")
|
|
62
|
+
|
|
63
|
+
@wrap_exceptions(ENABLED_EXC)
|
|
64
|
+
def get(
|
|
65
|
+
self, action: Action | None = None, limit: int | None = None
|
|
66
|
+
) -> list[Change]:
|
|
67
|
+
if not limit or limit > self.storage.chunk_size:
|
|
68
|
+
limit = self.storage.chunk_size
|
|
69
|
+
context = {'limit': limit}
|
|
70
|
+
# the ORDER_BY is only used for testing; should this return a set instead?
|
|
71
|
+
query = Query().SELECT('*').FROM('changes').ORDER_BY('rowid').LIMIT(':limit')
|
|
72
|
+
if action:
|
|
73
|
+
query.WHERE('action = :action')
|
|
74
|
+
context['action'] = action.value
|
|
75
|
+
rows = self.storage.get_db().execute(str(query), context)
|
|
76
|
+
return list(map(change_factory, rows))
|
|
77
|
+
|
|
78
|
+
@wrap_exceptions(ENABLED_EXC)
|
|
79
|
+
def done(self, changes: list[Change]) -> None:
|
|
80
|
+
if len(changes) > self.storage.chunk_size:
|
|
81
|
+
raise ValueError(f"too many changes, expected <= {self.storage.chunk_size}")
|
|
82
|
+
with self.storage.get_db() as db:
|
|
83
|
+
for change in changes:
|
|
84
|
+
db.execute(
|
|
85
|
+
"""
|
|
86
|
+
DELETE FROM changes
|
|
87
|
+
WHERE (sequence, feed, id, key, action)
|
|
88
|
+
= (:sequence, :feed, :id, :key, :action)
|
|
89
|
+
""",
|
|
90
|
+
change_to_dict(change),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def change_factory(row: tuple[Any, ...]) -> Change:
|
|
95
|
+
sequence, feed, id, key, action = row
|
|
96
|
+
resource = tuple(filter(bool, (feed, id)))
|
|
97
|
+
return Change(Action(action), sequence, resource, key or None)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def change_to_dict(change: Change) -> dict[str, Any]:
|
|
101
|
+
resource = change.resource_id + ('', '')
|
|
102
|
+
return dict(
|
|
103
|
+
sequence=change.sequence,
|
|
104
|
+
feed=resource[0],
|
|
105
|
+
id=resource[1],
|
|
106
|
+
key=change.tag_key or '',
|
|
107
|
+
action=change.action.value,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
SCHEMA = parse_schema("""
|
|
112
|
+
|
|
113
|
+
CREATE TABLE changes (
|
|
114
|
+
sequence BLOB NOT NULL,
|
|
115
|
+
feed TEXT NOT NULL,
|
|
116
|
+
id TEXT NOT NULL,
|
|
117
|
+
key TEXT NOT NULL,
|
|
118
|
+
action INTEGER NOT NULL,
|
|
119
|
+
|
|
120
|
+
PRIMARY KEY (sequence, feed, id, key)
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
CREATE TRIGGER changes_entry_insert
|
|
125
|
+
AFTER INSERT
|
|
126
|
+
ON entries
|
|
127
|
+
BEGIN
|
|
128
|
+
-- SELECT print(' entry_insert', new.feed, new.id);
|
|
129
|
+
|
|
130
|
+
UPDATE entries
|
|
131
|
+
SET sequence = randomblob(16)
|
|
132
|
+
WHERE (new.id, new.feed) = (id, feed);
|
|
133
|
+
|
|
134
|
+
INSERT OR REPLACE INTO changes
|
|
135
|
+
SELECT sequence, feed, id, '', 1
|
|
136
|
+
FROM entries
|
|
137
|
+
WHERE (feed, id) = (new.feed, new.id);
|
|
138
|
+
END;
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
-- Can't handle feed URL changes in changes_entry_update because
|
|
142
|
+
-- those entry updates are a consequence of ON UPDATE CASCADE,
|
|
143
|
+
-- which overrides the INSERT OR REPLACE used in the trigger,
|
|
144
|
+
-- because "conflict handling policy of the outer statement"
|
|
145
|
+
-- takes precedence per https://sqlite.org/lang_createtrigger.html.
|
|
146
|
+
-- Instead, we handle feed URL changes in changes_feed_changed.
|
|
147
|
+
|
|
148
|
+
CREATE TRIGGER changes_entry_update
|
|
149
|
+
AFTER UPDATE
|
|
150
|
+
OF title, summary, content
|
|
151
|
+
ON entries
|
|
152
|
+
WHEN
|
|
153
|
+
new.id = old.id AND new.feed = old.feed AND (
|
|
154
|
+
coalesce(new.title, '') != coalesce(old.title, '')
|
|
155
|
+
OR coalesce(new.summary, '') != coalesce(old.summary, '')
|
|
156
|
+
OR coalesce(new.content, '') != coalesce(old.content, '')
|
|
157
|
+
)
|
|
158
|
+
BEGIN
|
|
159
|
+
-- SELECT print(' entry_update', old.feed, old.id, '->', new.feed, new.id);
|
|
160
|
+
|
|
161
|
+
INSERT OR REPLACE INTO changes
|
|
162
|
+
VALUES (old.sequence, old.feed, old.id, '', 2);
|
|
163
|
+
|
|
164
|
+
UPDATE entries
|
|
165
|
+
SET sequence = randomblob(16)
|
|
166
|
+
WHERE (new.id, new.feed) = (id, feed);
|
|
167
|
+
|
|
168
|
+
INSERT OR REPLACE INTO changes
|
|
169
|
+
SELECT sequence, feed, id, '', 1
|
|
170
|
+
FROM entries
|
|
171
|
+
WHERE (feed, id) = (new.feed, new.id);
|
|
172
|
+
END;
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
CREATE TRIGGER changes_entry_delete
|
|
176
|
+
AFTER DELETE
|
|
177
|
+
ON entries
|
|
178
|
+
BEGIN
|
|
179
|
+
-- SELECT print(' entry_delete', old.feed, old.id);
|
|
180
|
+
|
|
181
|
+
INSERT OR REPLACE INTO changes
|
|
182
|
+
VALUES (old.sequence, old.feed, old.id, '', 2);
|
|
183
|
+
END;
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
CREATE TRIGGER changes_feed_changed
|
|
187
|
+
AFTER UPDATE
|
|
188
|
+
OF url, title, user_title
|
|
189
|
+
ON feeds
|
|
190
|
+
WHEN
|
|
191
|
+
new.url != old.url
|
|
192
|
+
OR coalesce(new.title, '') != coalesce(old.title, '')
|
|
193
|
+
OR coalesce(new.user_title, '') != coalesce(old.user_title, '')
|
|
194
|
+
BEGIN
|
|
195
|
+
-- SELECT print(' feed_url_change', old.url, '->', new.url);
|
|
196
|
+
|
|
197
|
+
INSERT OR REPLACE INTO changes
|
|
198
|
+
SELECT sequence, old.url, id, '', 2
|
|
199
|
+
FROM entries
|
|
200
|
+
WHERE feed = new.url;
|
|
201
|
+
|
|
202
|
+
UPDATE entries
|
|
203
|
+
SET sequence = randomblob(16)
|
|
204
|
+
WHERE feed = new.url;
|
|
205
|
+
|
|
206
|
+
INSERT OR REPLACE INTO changes
|
|
207
|
+
SELECT sequence, feed, id, '', 1
|
|
208
|
+
FROM entries
|
|
209
|
+
WHERE feed = new.url;
|
|
210
|
+
END;
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
""") # fmt: skip
|