diff --git a/CHANGELOG.md b/CHANGELOG.md index 85d4159..79020d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## 0.1.3 (TBD) +The biggest change, by far, the shift of database storage into SQLite. +This means that the org-roam cache needs to be built manually at least +once via `M-x org-roam-build-cache`. +### Breaking Changes +* [#200][gh-200] Move Org-roam cache into a SQLite database. + ### New Features * [#182][gh-182] Support file name aliases via `#+ROAM_ALIAS`. * [#188][gh-188] Add `org-roam-protocol`, shifting `roam://` link handling into Emacs-lisp. @@ -99,6 +105,7 @@ Mostly a documentation/cleanup release. [gh-165]: https://github.com/jethrokuan/org-roam/pull/165 [gh-182]: https://github.com/jethrokuan/org-roam/pull/182 [gh-188]: https://github.com/jethrokuan/org-roam/pull/188 +[gh-200]: https://github.com/jethrokuan/org-roam/pull/200 # Local Variables: # eval: (auto-fill-mode -1) diff --git a/README.md b/README.md index 70fd9fb..334d4cd 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,9 @@ As of February 2020, it is in a very early stage of development. Here's a screenshot of `org-roam`. The `org-roam` buffer shows backlinks for the active org buffer in the left window, as well as the -surrounding content in the backlink file. The backlink database is -built asynchronously in the background, and is not noticeable to the -end user. The graph is generated from the link structure, and can be -used to navigate to the respective files. +surrounding content in the backlink file. The database is built once, +and updated incrementally. The graph is generated from the link +structure, and can be used to navigate to the respective files. ![img](doc/images/org-roam-graph.gif) diff --git a/doc/configuration.md b/doc/configuration.md index 3768c42..1d05f7d 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -35,9 +35,12 @@ Here is an example `.dir-locals.el` file that would be placed in a second Org-roam directory. ```emacs-lisp -((nil . ((eval . (setq-local org-roam-directory (locate-dominating-file default-directory ".dir-locals.el")))))) +((nil . ((org-roam-directory . "/path/to/here/")))) ``` +Remember to run `org-roam-build-cache` from a file within that +directory, at least once. + ## Org-roam Buffer The Org-roam buffer defaults to popping up from the right. You may diff --git a/doc/tour.md b/doc/tour.md index 8896167..101b9c2 100644 --- a/doc/tour.md +++ b/doc/tour.md @@ -13,13 +13,11 @@ Without further ado, let's begin! ## Building the Cache -Assuming you've set `org-roam-directory` appropriately, running `M-x -org-roam--build-cache-async` should build up the caches that will -allow you to begin using Org-roam. I do this on startup: - -```emacs-lisp -(add-hook 'after-init-hook 'org-roam--build-cache-async) -``` +The cache is a sqlite database named `org-roam.db`, which resides at +the root of your `org-roam-directory`. To begin, we need to do a first +build of this cache. To do so, run `M-x org-roam-build-cache`. This +may take a while the first time, but is generally instantaneous in +subsequent runs. ## Finding a Note diff --git a/org-roam-db.el b/org-roam-db.el new file mode 100644 index 0000000..d519cbb --- /dev/null +++ b/org-roam-db.el @@ -0,0 +1,146 @@ +;;; org-roam-db.el --- Roam Research replica with Org-mode -*- coding: utf-8; lexical-binding: t -*- + +;; Copyright © 2020 Jethro Kuan + +;; Author: Jethro Kuan + +;; This file is NOT part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation; either version 3, or (at your option) +;; any later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +;; Boston, MA 02110-1301, USA. + +;;; Commentary: +;; +;; This code is heavily referenced from https://github.com/magit/forge. +;; +;;; Code: + +(require 'emacsql) +(require 'emacsql-sqlite) + +;;; Options + +(defcustom org-roam-directory (expand-file-name "~/org-roam/") + "Default path to Org-roam files. + +All Org files, at any level of nesting, is considered part of the Org-roam." + :type 'directory + :group 'org-roam) + +(defconst org-roam-db-filename "org-roam.db" + "Name of the Org-roam database file.") + +(defconst org-roam--db-version 1) +(defconst org-roam--sqlite-available-p + (with-demoted-errors "Org-roam initialization: %S" + (emacsql-sqlite-ensure-binary) + t)) + +(defvar org-roam--db-connection (make-hash-table :test #'equal) + "Database connection to Org-roam database.") + +;;; Core + +(defun org-roam--get-db () + "Return the sqlite db file." + (interactive "P") + (expand-file-name org-roam-db-filename org-roam-directory)) + +(defun org-roam--get-db-connection () + "Return the database connection, if any." + (gethash (file-truename org-roam-directory) + org-roam--db-connection)) + +(defun org-roam-db () + (unless (and (org-roam--get-db-connection) + (emacsql-live-p (org-roam--get-db-connection))) + (let* ((db-file (org-roam--get-db)) + (init-db (not (file-exists-p db-file)))) + (make-directory (file-name-directory db-file) t) + (let ((conn (emacsql-sqlite db-file))) + (puthash (file-truename org-roam-directory) + conn + org-roam--db-connection) + (when init-db + (org-roam--db-init conn)) + (let* ((version (caar (emacsql conn "PRAGMA user_version"))) + (version (org-roam--db-maybe-update conn version))) + (cond + ((> version org-roam--db-version) + (emacsql-close conn) + (user-error + "The Org-roam database was created with a newer Org-roam version. %s" + "You need to update the Org-roam package.") + ((< version org-roam--db-version) + (emacsql-close conn) + (error "BUG: The Org-roam database scheme changed %s" + "and there is no upgrade path")))))))) + (org-roam--get-db-connection)) + +;;; Api + +(defun org-roam-sql (sql &rest args) + (if (stringp sql) + (emacsql (org-roam-db) (apply #'format sql args)) + (apply #'emacsql (org-roam-db) sql args))) + +;;; Schemata + +(defconst org-roam--db-table-schemata + '((files + [(file :unique :primary-key) + (hash :not-null) + (last-modified :not-null) + ]) + + (file-links + [(file-from :not-null) + (file-to :not-null) + (properties :not-null)]) + + (titles + [ + (file :not-null) + titles]) + + (refs + [(ref :unique :not-null) + (file :not-null)]))) + +(defun org-roam--db-init (db) + (emacsql-with-transaction db + (pcase-dolist (`(,table . ,schema) org-roam--db-table-schemata) + (emacsql db [:create-table $i1 $S2] table schema)) + (emacsql db (format "PRAGMA user_version = %s" org-roam--db-version)))) + +(defun org-roam--db-maybe-update (db version) + (emacsql-with-transaction db + 'ignore + ;; Do nothing now + version)) + +(defun org-roam--db-close (&optional db) + (unless db + (setq db (org-roam--get-db-connection))) + (when (and db (emacsql-live-p db)) + (emacsql-close db))) + +(defun org-roam--db-close-all () + (dolist (conn (hash-table-values org-roam--db-connection)) + (org-roam--db-close conn))) + +(provide 'org-roam-db) + +;;; org-roam-db.el ends here diff --git a/org-roam-protocol.el b/org-roam-protocol.el index 8ae6c3d..e7af38b 100644 --- a/org-roam-protocol.el +++ b/org-roam-protocol.el @@ -2,10 +2,6 @@ ;; Copyright © 2020 Jethro Kuan ;; Author: Jethro Kuan -;; URL: https://github.com/jethrokuan/org-roam -;; Keywords: org-mode, roam, convenience -;; Version: 0.1.2 -;; Package-Requires: ((emacs "26.1") (org "9.0")) ;; This file is NOT part of GNU Emacs. @@ -71,8 +67,7 @@ If the function returns nil, the filename is removed from the list of filenames passed from emacsclient to the server. If the function returns a non-nil value, that value is passed to the server as filename." - (let ((the-protocol (concat (regexp-quote org-roam-protocol-the-protocol) - ":"))) + (let ((the-protocol (concat (regexp-quote org-roam-protocol-the-protocol) ":"))) (when (string-match the-protocol fname) (cadr (split-string fname the-protocol))))) diff --git a/org-roam-utils.el b/org-roam-utils.el deleted file mode 100644 index 0f64a82..0000000 --- a/org-roam-utils.el +++ /dev/null @@ -1,222 +0,0 @@ -;;; org-roam-utils.el --- Roam Research replica with Org-mode -*- coding: utf-8; lexical-binding: t -*- - -;; Copyright © 2020 Jethro Kuan - -;; Author: Jethro Kuan -;; URL: https://github.com/jethrokuan/org-roam -;; Keywords: org-mode, roam, convenience -;; Version: 0.1.2 -;; Package-Requires: ((emacs "26.1") (org "9.0")) - -;; This file is NOT part of GNU Emacs. - -;; This program is free software; you can redistribute it and/or modify -;; it under the terms of the GNU General Public License as published by -;; the Free Software Foundation; either version 3, or (at your option) -;; any later version. -;; -;; This program is distributed in the hope that it will be useful, -;; but WITHOUT ANY WARRANTY; without even the implied warranty of -;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -;; GNU General Public License for more details. -;; -;; You should have received a copy of the GNU General Public License -;; along with GNU Emacs; see the file COPYING. If not, write to the -;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, -;; Boston, MA 02110-1301, USA. - -;;; Commentary: -;; -;; This library is an attempt at injecting Roam functionality into Org-mode. -;; This is achieved primarily through building caches for forward links, -;; backward links, and file titles. -;; -;;; Code: - -(require 'org) -(require 'org-element) -(require 'ob-core) ;for org-babel-parse-header-arguments -(require 'subr-x) -(require 'cl-lib) - -(defun org-roam--file-name-extension (filename) - "Return file name extension for FILENAME. - -Like file-name-extension, but does not strip version number." - (save-match-data - (let ((file (file-name-nondirectory filename))) - (if (and (string-match "\\.[^.]*\\'" file) - (not (eq 0 (match-beginning 0)))) - (substring file (+ (match-beginning 0) 1)))))) - -(defun org-roam--org-file-p (path) - "Check if PATH is pointing to an org file." - (let ((ext (org-roam--file-name-extension path))) - (or (string= ext "org") - (and - (string= ext "gpg") - (string= (org-roam--file-name-extension (file-name-sans-extension path)) "org"))))) - -(defun org-roam--find-files (dir) - "Return all `org-roam' files in `DIR'." - (if (file-exists-p dir) - (let ((files (directory-files dir t "." t)) - (dir-ignore-regexp (concat "\\(?:" - "\\." - "\\|\\.\\." - "\\)$")) - result) - (dolist (file files) - (cond - ((file-directory-p file) - (when (not (string-match dir-ignore-regexp file)) - (setq result (append (org-roam--find-files file) result)))) - ((and (file-readable-p file) - (org-roam--org-file-p file)) - (setq result (cons (file-truename file) result))))) - result))) - -(defun org-roam--parse-content (&optional file-path) - "Parse the current buffer, and return a list of items for processing." - (org-element-map (org-element-parse-buffer) 'link - (lambda (link) - (let ((type (org-element-property :type link)) - (path (org-element-property :path link)) - (start (org-element-property :begin link))) - (when (and (string= type "file") - (org-roam--org-file-p path)) - (goto-char start) - (let* ((element (org-element-at-point)) - (begin (or (org-element-property :content-begin element) - (org-element-property :begin element))) - (content (or (org-element-property :raw-value element) - (buffer-substring - begin - (or (org-element-property :content-end element) - (org-element-property :end element))))) - (content (string-trim content)) - (file-path (or file-path - (file-truename (buffer-file-name (current-buffer)))))) - (list :from file-path - :to (file-truename (expand-file-name path (file-name-directory file-path))) - :properties (list :content content :point begin)))))))) - -(cl-defun org-roam--insert-item (item &key forward backward) - "Insert ITEM into FORWARD and BACKWARD cache. - -ITEM is of the form: (:from from-path :to to-path :properties (:content preview-content :point point))." - (pcase-let ((`(:from ,p-from :to ,p-to :properties ,props) item)) - ;; Build forward-links - (let ((links (gethash p-from forward))) - (if links - (puthash p-from - (if (member p-to links) - links - (cons p-to links)) forward) - (puthash p-from (list p-to) forward))) - ;; Build backward-links - (let ((contents-hash (gethash p-to backward))) - (if contents-hash - (if-let ((contents-list (gethash p-from contents-hash))) - (let ((updated (cons props contents-list))) - (puthash p-from updated contents-hash) - (puthash p-to contents-hash backward)) - (progn - (puthash p-from (list props) contents-hash) - (puthash p-to contents-hash backward))) - (let ((contents-hash (make-hash-table :test #'equal))) - (puthash p-from (list props) contents-hash) - (puthash p-to contents-hash backward)))))) - -(defun org-roam--extract-global-props (props) - "Extract PROPS from the current buffer." - (let ((buf (org-element-parse-buffer)) - (res '())) - (dolist (prop props) - (let ((p (org-element-map - buf - 'keyword - (lambda (kw) - (when (string= (org-element-property :key kw) prop) - (org-element-property :value kw))) - :first-match t))) - (setq res (cons (cons prop p) res)))) - res)) - -(defun org-roam--aliases-str-to-list (str) - "Function to transform string STR into list of alias titles. - -This snippet is obtained from ox-hugo: -https://github.com/kaushalmodi/ox-hugo/blob/a80b250987bc770600c424a10b3bca6ff7282e3c/ox-hugo.el#L3131" - (when (stringp str) - (let* ((str (org-trim str)) - (str-list (split-string str "\n")) - ret) - (dolist (str-elem str-list) - (let* ((format-str ":dummy '(%s)") ;The :dummy key is discarded in the `lst' var below. - (alist (org-babel-parse-header-arguments (format format-str str-elem))) - (lst (cdr (car alist))) - (str-list2 (mapcar (lambda (elem) - (cond - ((symbolp elem) - (symbol-name elem)) - (t - elem))) - lst))) - (setq ret (append ret str-list2)))) - ret))) - -(defun org-roam--extract-titles () - "Extract the titles from current buffer. - -Titles are obtained via the #+TITLE property, or aliases -specified via the #+ROAM_ALIAS property." - (let* ((props (org-roam--extract-global-props '("TITLE" "ROAM_ALIAS"))) - (aliases (cdr (assoc "ROAM_ALIAS" props))) - (title (cdr (assoc "TITLE" props))) - (alias-list (org-roam--aliases-str-to-list aliases))) - (if title - (cons title alias-list) - alias-list))) - -(defun org-roam--extract-ref () - "Extract the ref from current buffer." - (cdr (assoc "ROAM_KEY" (org-roam--extract-global-props '("ROAM_KEY"))))) - -(defun org-roam--build-cache (dir) - "Build the org-roam caches in DIR." - (let ((backward-links (make-hash-table :test #'equal)) - (forward-links (make-hash-table :test #'equal)) - (file-titles (make-hash-table :test #'equal)) - (refs (make-hash-table :test #'equal))) - (let* ((org-roam-files (org-roam--find-files dir)) - (file-items (mapcar (lambda (file) - (with-temp-buffer - (insert-file-contents file) - (org-roam--parse-content file))) org-roam-files))) - (dolist (items file-items) - (dolist (item items) - (org-roam--insert-item - item - :forward forward-links - :backward backward-links))) - (dolist (file org-roam-files) - (with-temp-buffer - (insert-file-contents file) - (when-let ((titles (org-roam--extract-titles))) - (puthash file titles file-titles)) - (when-let ((ref (org-roam--extract-ref))) - ;; FIXME: this overrides previous refs, should probably have a - ;; warning when ref is not unique - (puthash ref file refs))) - org-roam-files)) - (list - :directory dir - :forward forward-links - :backward backward-links - :titles file-titles - :refs refs))) - -(provide 'org-roam-utils) - -;;; org-roam-utils.el ends here diff --git a/org-roam.el b/org-roam.el index c029256..9fd7353 100644 --- a/org-roam.el +++ b/org-roam.el @@ -6,7 +6,7 @@ ;; URL: https://github.com/jethrokuan/org-roam ;; Keywords: org-mode, roam, convenience ;; Version: 0.1.2 -;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (s "1.12.0") (async "1.9.4") (org "9.0")) +;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (s "1.12.0") (org "9.0") (emacsql "3.0.0") (emacsql-sqlite "1.0.0")) ;; This file is NOT part of GNU Emacs. @@ -35,14 +35,15 @@ ;;; Code: (eval-when-compile (require 'cl-lib)) -(require 'dash) +(require 'org) (require 'org-element) -(require 'async) +(require 'ob-core) ;for org-babel-parse-header-arguments (require 'subr-x) +(require 'dash) (require 's) (require 'f) -(require 'org-roam-utils) -(require 'eieio) +(require 'cl-lib) +(require 'org-roam-db) ;;; Customizations (defgroup org-roam nil @@ -52,12 +53,10 @@ :link '(url-link :tag "Github" "https://github.com/jethrokuan/org-roam") :link '(url-link :tag "Online Manual" "https://org-roam.readthedocs.io/")) -(defcustom org-roam-directory (expand-file-name "~/org-roam/") - "Path to Org-roam files. - -All Org files, at any level of nesting, is considered part of the Org-roam." - :type 'directory - :group 'org-roam) +(defgroup org-roam-faces nil + "Faces used by Org-Roam." + :group 'org-roam + :group 'faces) (defcustom org-roam-new-file-directory nil "Path to where new Org-roam files are created. @@ -66,11 +65,6 @@ If nil, default to the org-roam-directory (preferred)." :type 'directory :group 'org-roam) -(defcustom org-roam-mute-cache-build nil - "Whether to mute the cache build message." - :type 'boolean - :group 'org-roam) - (defcustom org-roam-buffer-position 'right "Position of `org-roam' buffer. @@ -81,14 +75,6 @@ Valid values are (const right)) :group 'org-roam) -(defcustom org-roam-file-name-function #'org-roam--file-name-timestamp-title - "The function used to generate filenames. - -The function takes as parameter `TITLE', a string the user inputs." - :group 'org-roam - :type '(choice (const :tag "Default" org-roam--file-name-timestamp-title) - (function :tag "Personalized function"))) - (defcustom org-roam-link-title-format "%s" "The format string used when inserting org-roam links that use their title." :type 'string @@ -126,55 +112,15 @@ If nil, always ask for filename." :group 'org-roam) (defcustom org-roam-graph-max-title-length 100 - "Maximum length of titles in graphviz graph nodes" + "Maximum length of titles in Graphviz graph nodes." :type 'number :group 'org-roam) (defcustom org-roam-graph-node-shape "ellipse" - "Maximum length of titles in graphviz graph nodes" + "Shape of Graphviz nodes." :type 'string :group 'org-roam) -(defgroup org-roam-faces nil - "Faces used by Org-Roam." - :group 'org-roam - :group 'faces) - -;;; Polyfills -;; These are for functions I use that are only available in newer Emacs - -;; Introduced in Emacs 27.1 -(unless (fboundp 'make-empty-file) - (defun make-empty-file (filename &optional parents) - "Create an empty file FILENAME. -Optional arg PARENTS, if non-nil then creates parent dirs as needed. - -If called interactively, then PARENTS is non-nil." - (interactive - (let ((filename (read-file-name "Create empty file: "))) - (list filename t))) - (when (and (file-exists-p filename) (null parents)) - (signal 'file-already-exists `("File exists" ,filename))) - (let ((paren-dir (file-name-directory filename))) - (when (and paren-dir (not (file-exists-p paren-dir))) - (make-directory paren-dir parents))) - (write-region "" nil filename nil 0))) - -;;; Classes - -(defclass org-roam-cache () - ((initialized :initarg :initialized - :documentation "Is cache valid?") - (forward-links :initarg :forward-links - :documentation "Cache containing forward links.") - (backward-links :initarg :backward-links - :documentation "Cache containing backward links.") - (titles :initarg :titles - :documentation "Cache containing titles for org-roam files.") - (refs :initarg :refs - :documentation "Cache with ref as key, and file path as value.")) - "All cache for an org-roam directory.") - ;;; Dynamic variables (defvar org-roam--current-buffer nil "Currently displayed file in `org-roam' buffer.") @@ -182,70 +128,196 @@ If called interactively, then PARENTS is non-nil." (defvar org-roam-last-window nil "Last window `org-roam' was called from.") -(defvar org-roam--cache (make-hash-table :test 'equal) - "The list of cache separated by directory.") - -(defvar-local org-roam--local-cache-ref nil - "Local reference of the buffer's cache object.") - -(defvar-local org-roam--local-cache-id nil - "Local reference of the buffer's cache object id, which is - comparable by \"eq\".") - ;;; Utilities -(defun org-roam-directory-normalized () - "Get the org-roam-directory normalized so that it can be used -as a unique key." - (directory-file-name (file-truename org-roam-directory))) +(defun org-roam--touch-file (path) + "Touches an empty file at PATH." + (make-directory (file-name-directory path) t) + (f-touch path)) -(defmacro org-roam--get-local (name) - "Get a variable that is local to the current org-roam-directory." - `(gethash (org-roam-directory-normalized) ,name nil)) +(defun org-roam--file-name-extension (filename) + "Return file name extension for FILENAME. +Like file-name-extension, but does not strip version number." + (save-match-data + (let ((file (file-name-nondirectory filename))) + (if (and (string-match "\\.[^.]*\\'" file) + (not (eq 0 (match-beginning 0)))) + (substring file (+ (match-beginning 0) 1)))))) -(defmacro org-roam--set-local (name value) - "Set a variable that is local to the current org-roam-directory." - `(puthash (org-roam-directory-normalized) ,value ,name)) +(defun org-roam--org-file-p (path) + "Check if PATH is pointing to an org file." + (let ((ext (org-roam--file-name-extension path))) + (or (string= ext "org") + (and + (string= ext "gpg") + (string= (org-roam--file-name-extension (file-name-sans-extension path)) "org"))))) -(defun org-roam--get-directory-cache () - "Get the cache object for the current org-roam-directory." - (unless (eq org-roam--local-cache-id org-roam-directory) - ;; Prevent needless repeated calls to org-roam-directory-normalized by - ;; having a reference to the cache object that matches the local buffer. - (setq org-roam--local-cache-ref - (let* ((cache (org-roam--get-local org-roam--cache))) - (if cache - cache - (let ((new-cache (org-roam--default-cache))) - (org-roam--set-local org-roam--cache new-cache) - new-cache)))) - (setq org-roam--local-cache-id org-roam-directory)) - org-roam--local-cache-ref) +(defun org-roam--find-files (dir) + "Return all `org-roam' files in `DIR'." + (if (file-exists-p dir) + (let ((files (directory-files dir t "." t)) + (dir-ignore-regexp (concat "\\(?:" + "\\." + "\\|\\.\\." + "\\)$")) + result) + (dolist (file files) + (cond + ((file-directory-p file) + (when (not (string-match dir-ignore-regexp file)) + (setq result (append (org-roam--find-files file) result)))) + ((and (file-readable-p file) + (org-roam--org-file-p file)) + (setq result (cons (file-truename file) result))))) + result))) + +(defun org-roam--get-links (&optional file-path) + "Get the links in the buffer. +If FILE-PATH is passed, use that as the source file." + (let ((file-path (or file-path + (file-truename (buffer-file-name (current-buffer)))))) + (org-element-map (org-element-parse-buffer) 'link + (lambda (link) + (let ((type (org-element-property :type link)) + (path (org-element-property :path link)) + (start (org-element-property :begin link))) + (when (and (string= type "file") + (org-roam--org-file-p path)) + (goto-char start) + (let* ((element (org-element-at-point)) + (begin (or (org-element-property :content-begin element) + (org-element-property :begin element))) + (content (or (org-element-property :raw-value element) + (buffer-substring + begin + (or (org-element-property :content-end element) + (org-element-property :end element))))) + (content (string-trim content))) + (vector file-path + (file-truename (expand-file-name path (file-name-directory file-path))) + (list :content content :point begin))))))))) + +(defun org-roam--extract-global-props (props) + "Extract PROPS from the current buffer." + (let ((buf (org-element-parse-buffer)) + (res '())) + (dolist (prop props) + (let ((p (org-element-map + buf + 'keyword + (lambda (kw) + (when (string= (org-element-property :key kw) prop) + (org-element-property :value kw))) + :first-match t))) + (setq res (cons (cons prop p) res)))) + res)) + +(defun org-roam--aliases-str-to-list (str) + "Function to transform string STR into list of alias titles. + +This snippet is obtained from ox-hugo: +https://github.com/kaushalmodi/ox-hugo/blob/a80b250987bc770600c424a10b3bca6ff7282e3c/ox-hugo.el#L3131" + (when (stringp str) + (let* ((str (org-trim str)) + (str-list (split-string str "\n")) + ret) + (dolist (str-elem str-list) + (let* ((format-str ":dummy '(%s)") ;The :dummy key is discarded in the `lst' var below. + (alist (org-babel-parse-header-arguments (format format-str str-elem))) + (lst (cdr (car alist))) + (str-list2 (mapcar (lambda (elem) + (cond + ((symbolp elem) + (symbol-name elem)) + (t + elem))) + lst))) + (setq ret (append ret str-list2)))) + ret))) + +(defun org-roam--extract-titles () + "Extract the titles from current buffer. +Titles are obtained via the #+TITLE property, or aliases +specified via the #+ROAM_ALIAS property." + (let* ((props (org-roam--extract-global-props '("TITLE" "ROAM_ALIAS"))) + (aliases (cdr (assoc "ROAM_ALIAS" props))) + (title (cdr (assoc "TITLE" props))) + (alias-list (org-roam--aliases-str-to-list aliases))) + (if title + (cons title alias-list) + alias-list))) + +(defun org-roam--extract-ref () + "Extract the ref from current buffer." + (cdr (assoc "ROAM_KEY" (org-roam--extract-global-props '("ROAM_KEY"))))) + +(defun org-roam--insert-links (links) + "Insert LINK into the org-roam cache." + (org-roam-sql + [:insert :into file-links + :values $v1] + links)) + +(defun org-roam--insert-titles (file titles) + "Insert TITLES into the org-roam-cache." + (org-roam-sql + [:insert :into titles + :values $v1] + (list (vector file titles)))) + +(defun org-roam--insert-ref (file ref) + "Insert REF into the Org-roam cache." + (org-roam-sql + [:insert :into refs + :values $v1] + (list (vector ref file)))) + +(defun org-roam--clear-cache () + "Clears all entries in the caches." + (interactive) + (when (file-exists-p (org-roam--get-db)) + (org-roam-sql [:delete :from files]) + (org-roam-sql [:delete :from titles]) + (org-roam-sql [:delete :from file-links]) + (org-roam-sql [:delete :from files]) + (org-roam-sql [:delete :from refs]))) + +(defun org-roam--clear-file-from-cache (&optional filepath) + "Remove any related links to the file at FILEPATH. +This is equivalent to removing the node from the graph." + (let* ((path (or filepath + (buffer-file-name (current-buffer)))) + (file (file-truename path))) + (org-roam-sql [:delete :from files + :where (= file $s1)] + file) + (org-roam-sql [:delete :from file-links + :where (= file-from $s1)] + file) + (org-roam-sql [:delete :from titles + :where (= file $s1)] + file) + (org-roam-sql [:delete :from refs + :where (= file $s1)] + file))) + +(defun org-roam--get-current-files () + "Return a hash of file to buffer string hash." + (let* ((current-files (org-roam-sql [:select * :from files])) + (ht (make-hash-table :test #'equal))) + (dolist (row current-files) + (puthash (car row) (cadr row) ht)) + ht)) (defun org-roam--cache-initialized-p () - "Is cache valid?" - (oref (org-roam--get-directory-cache) initialized)) - -(defun org-roam--forward-links-cache () - "Cache containing forward links." - (oref (org-roam--get-directory-cache) forward-links)) - -(defun org-roam--backward-links-cache () - "Cache containing backward links." - (oref (org-roam--get-directory-cache) backward-links)) - -(defun org-roam--titles-cache () - "Cache containing titles for org-roam files." - (oref (org-roam--get-directory-cache) titles)) - -(defun org-roam--refs-cache () - "Cache containing refs for org-roam files." - (oref (org-roam--get-directory-cache) refs)) + "Whether the cache has been initialized." + (and (file-exists-p (org-roam--get-db)) + (> (caar (org-roam-sql [:select (funcall count) :from titles])) + 0))) (defun org-roam--ensure-cache-built () "Ensures that org-roam cache is built." (unless (org-roam--cache-initialized-p) - (org-roam--build-cache-async) - (user-error "Your Org-Roam cache isn't built yet! Please wait"))) + (error "[Org-roam] your cache isn't built yet! Please wait."))) (defun org-roam--org-roam-file-p (&optional file) "Return t if FILE is part of org-roam system, defaulting to the name of the current buffer. Else, return nil." @@ -258,11 +330,10 @@ as a unique key." (defun org-roam--get-titles-from-cache (file) "Return titles and aliases of `FILE' from the cache." - (or (gethash file (org-roam--titles-cache)) - (progn - (unless (org-roam--cache-initialized-p) - (user-error "The Org-Roam caches aren't built! Please run org-roam--build-cache-async")) - nil))) + (caar (org-roam-sql [:select [titles] :from titles + :where (= file $s1)] + file + :limit 1))) (defun org-roam--get-title-from-cache (file) "Return the title of `FILE' from the cache." @@ -341,7 +412,7 @@ It uses TITLE and the current timestamp to form a unique title." (setq file-path (org-roam--new-file-path (file-name-fn title) t)) (if (file-exists-p file-path) file-path - (make-empty-file file-path t) + (org-roam--touch-file file-path) (write-region (s-format (plist-get template :content) 'aget @@ -350,18 +421,6 @@ It uses TITLE and the current timestamp to form a unique title." nil file-path nil) file-path)))) -(defun org-roam--get-new-id (title) - "Return a new ID, given the note TITLE." - (let* ((proposed-slug (funcall org-roam-file-name-function title)) - (new-slug (if org-roam-filename-noconfirm - proposed-slug - (read-string "Enter ID (without extension): " - proposed-slug))) - (file-path (org-roam--new-file-path new-slug t))) - (if (file-exists-p file-path) - (user-error "There's already a file at %s") - new-slug))) - ;;; Inserting org-roam links (defun org-roam-insert (prefix) "Find an org-roam file, and insert a relative org link to it at point. @@ -397,13 +456,16 @@ If PREFIX, downcase the title before insertion." ;;; Finding org-roam files (defun org-roam--get-title-path-completions () "Return a list of cons pairs for titles to absolute path of Org-roam files." - (let ((files (org-roam--find-all-files)) - (res '())) - (dolist (file files) - (if-let (titles (org-roam--get-titles-from-cache file)) - (dolist (title titles) - (setq res (cons (cons title file) res))) - (setq res (cons (cons (org-roam--path-to-slug file) file) res)))) + (let* ((rows (org-roam-sql [:select [file titles] :from titles])) + res) + (dolist (row rows) + (let ((file-path (car row)) + (titles (cadr row))) + (if titles + (dolist (title titles) + (setq res (cons (cons title file-path) res))) + (setq res (cons (cons (org-roam--path-to-slug file-path) + file-path) res))))) res)) (defun org-roam-find-file () @@ -437,115 +499,102 @@ If PREFIX, downcase the title before insertion." (when-let ((name (completing-read "Choose a buffer: " names-and-buffers))) (switch-to-buffer (cdr (assoc name names-and-buffers)))))) -(defvar org-roam--ongoing-async-build (make-hash-table :test 'equal) - "Prevent multiple async cache builds. This can happen when - restoring a session or loading multiple org-roam files before a - build has completed.") - ;;; Building the org-roam cache -(defun org-roam--build-cache-async (&optional on-success) - "Builds the caches asychronously." +(defun org-roam-build-cache () + "Build the cache for `org-roam-directory'." (interactive) - (let ((existing (org-roam--get-local org-roam--ongoing-async-build))) - (unless (and (processp existing) - (not (async-ready existing))) - (org-roam--set-local - org-roam--ongoing-async-build - (async-start - `(lambda () - (setq load-path ',load-path) - (package-initialize) - (require 'org-roam-utils) - ,(async-inject-variables "org-roam-directory") - (org-roam--build-cache org-roam-directory)) - (lambda (cache) - (let ((org-roam-directory (plist-get cache :directory))) - (let ((obj (org-roam--get-directory-cache))) - (oset obj initialized t) - (oset obj forward-links (plist-get cache :forward)) - (oset obj backward-links (plist-get cache :backward)) - (oset obj titles (plist-get cache :titles)) - (oset obj refs (plist-get cache :refs))) - (unless org-roam-mute-cache-build - (message "Org-roam cache built!")) - (when on-success - (funcall on-success))))))))) - -(defun org-roam--clear-cache () - "Clears all entries in the caches." - (interactive) - (let ((cache (org-roam--get-directory-cache))) - (oset cache initialized nil) - (oset cache forward-links (make-hash-table :test #'equal)) - (oset cache backward-links (make-hash-table :test #'equal)) - (oset cache titles (make-hash-table :test #'equal)) - (oset cache refs (make-hash-table :test #'equal)))) - -(defun org-roam--default-cache () - "A default, uninitialized cache object." - (org-roam-cache :initialized nil - :forward-links (make-hash-table :test #'equal) - :backward-links (make-hash-table :test #'equal) - :titles (make-hash-table :test #'equal) - :refs (make-hash-table :test #'equal))) - -(defun org-roam--clear-file-from-cache (&optional filepath) - "Remove any related links to the file at FILEPATH. -This is equivalent to removing the node from the graph." - (let* ((path (or filepath - (buffer-file-name (current-buffer)))) - (file (file-truename path))) - ;; Step 1: Remove all existing links for file - (when-let ((forward-links (gethash file (org-roam--forward-links-cache)))) - ;; Delete backlinks to file - (dolist (link forward-links) - (when-let ((backward-links (gethash link (org-roam--backward-links-cache)))) - (remhash file backward-links) - (puthash link backward-links (org-roam--backward-links-cache)))) - ;; Clean out forward links - (remhash file (org-roam--forward-links-cache))) - ;; Step 2: Remove from the title cache - (remhash file (org-roam--titles-cache)) - ;; Step 3: Remove from the refs cache - (maphash (lambda (k v) - (when (string= v file) - (remhash k (org-roam--refs-cache)))) - (org-roam--refs-cache)))) + (org-roam-db) ;; To initialize the database, no-op if already initialized + (let* ((org-roam-files (org-roam--find-files org-roam-directory)) + (current-files (org-roam--get-current-files)) + (time (current-time)) + all-files all-links all-titles all-refs) + (dolist (file org-roam-files) + (with-temp-buffer + (insert-file-contents file) + (let ((contents-hash (secure-hash 'sha1 (current-buffer)))) + (unless (string= (gethash file current-files) + contents-hash) + (org-roam--clear-file-from-cache file) + (setq all-files + (cons (vector file contents-hash time) all-files)) + (when-let (links (org-roam--get-links file)) + (setq all-links (append links all-links))) + (let ((titles (org-roam--extract-titles))) + (setq all-titles (cons (vector file titles) all-titles))) + (when-let ((ref (org-roam--extract-ref))) + (setq all-refs (cons (vector ref file) all-refs)))) + (remhash file current-files)))) + (dolist (file (hash-table-keys current-files)) + ;; These files are no longer around, remove from cache... + (org-roam--clear-file-from-cache file)) + (when all-files + (org-roam-sql + [:insert :into files + :values $v1] + all-files)) + (when all-links + (org-roam-sql + [:insert :into file-links + :values $v1] + all-links)) + (when all-titles + (org-roam-sql + [:insert :into titles + :values $v1] + all-titles)) + (when all-refs + (org-roam-sql + [:insert :into refs + :values $v1] + all-refs)) + (let ((stats (list :files (length all-files) + :links (length all-links) + :titles (length all-titles) + :refs (length all-refs) + :deleted (length (hash-table-keys current-files))))) + (message (format "files: %s, links: %s, titles: %s, refs: %s, deleted: %s" + (plist-get stats :files) + (plist-get stats :links) + (plist-get stats :titles) + (plist-get stats :refs) + (plist-get stats :deleted))) + stats))) (defun org-roam--update-cache-titles () - "Insert the title of the current buffer into the cache." - (when-let ((titles (org-roam--extract-titles))) - (puthash (file-truename (buffer-file-name (current-buffer))) - titles - (org-roam--titles-cache)))) + "Update the title of the current buffer into the cache." + (let ((file (file-truename (buffer-file-name (current-buffer))))) + (org-roam-sql [:delete :from titles + :where (= file $s1)] + file) + (org-roam--insert-titles file (org-roam--extract-titles)))) (defun org-roam--update-cache-refs () - "Insert the ref of the current buffer into the cache." - (when-let ((ref (org-roam--extract-ref))) - (puthash ref - (file-truename (buffer-file-name (current-buffer))) - (org-roam--refs-cache)))) + "Update the ref of the current buffer into the cache." + (let ((file (file-truename (buffer-file-name (current-buffer))))) + (org-roam-sql [:delete :from refs + :where (= file $s1)] + file) + (when-let ((ref (org-roam--extract-ref))) + (org-roam--insert-ref file ref)))) + +(defun org-roam--update-cache-links () + "Update the file links of the current buffer in the cache." + (let ((file (file-truename (buffer-file-name (current-buffer))))) + (org-roam-sql [:delete :from file-links + :where (= file-from $s1)] + file) + (when-let ((links (org-roam--get-links))) + (org-roam--insert-links links)))) (defun org-roam--update-cache () "Update org-roam caches for the current buffer file." (save-excursion - (org-roam--clear-file-from-cache) - ;; Insert into title cache (org-roam--update-cache-titles) - ;; Insert into ref cache (org-roam--update-cache-refs) - ;; Insert new items - (let ((items (org-roam--parse-content))) - (dolist (item items) - (org-roam--insert-item - item - :forward (org-roam--forward-links-cache) - :backward (org-roam--backward-links-cache)))) - ;; Rerender buffer + (org-roam--update-cache-links) (org-roam--maybe-update-buffer :redisplay t))) ;;; Org-roam daily notes - (defun org-roam--file-for-time (time) "Create and find file for TIME." (let* ((org-roam-templates (list (list "daily" (list :file (lambda (title) title) @@ -618,10 +667,15 @@ If item at point is not org-roam specific, default to Org behaviour." (select-window org-roam-last-window)) (find-file file))) +(defun org-roam--get-backlinks (file) + (org-roam-sql [:select [file-from, file-to, properties] :from file-links + :where (= file-to $s1)] + file)) + (defun org-roam-update (file-path) "Show the backlinks for given org file for file at `FILE-PATH'." + (org-roam--ensure-cache-built) (let* ((source-org-roam-directory org-roam-directory)) - (org-roam--ensure-cache-built) (let ((buffer-title (org-roam--get-title-or-slug file-path))) (with-current-buffer org-roam-buffer ;; When dir-locals.el is used to override org-roam-directory, @@ -640,24 +694,27 @@ If item at point is not org-roam specific, default to Org behaviour." (setq org-return-follows-link t) (insert (propertize buffer-title 'font-lock-face 'org-document-title)) - (if-let ((backlinks (gethash file-path (org-roam--backward-links-cache)))) + (if-let* ((backlinks (org-roam--get-backlinks file-path)) + (grouped-backlinks (--group-by (nth 0 it) backlinks))) (progn (insert (format "\n\n* %d Backlinks\n" - (hash-table-count backlinks))) - (maphash (lambda (file-from contents) - (insert (format "** [[file:%s][%s]]\n" - file-from - (org-roam--get-title-or-slug file-from))) - (dolist (properties contents) - (let ((content (propertize - (s-trim (s-replace "\n" " " - (plist-get properties :content))) - 'font-lock-face 'org-block - 'help-echo "mouse-1: visit backlinked note" - 'file-from file-from - 'file-from-point (plist-get properties :point)))) - (insert (format "%s \n\n" content))))) - backlinks)) + (length backlinks))) + (dolist (group grouped-backlinks) + (let ((file-from (car group)) + (bls (cdr group))) + (insert (format "** [[file:%s][%s]]\n" + file-from + (org-roam--get-title-or-slug file-from))) + (dolist (backlink bls) + (pcase-let ((`(,file-from ,file-to ,props) backlink)) + (insert (propertize + (s-trim (s-replace "\n" " " + (plist-get props :content))) + 'font-lock-face 'org-block + 'help-echo "mouse-1: visit backlinked note" + 'file-from file-from + 'file-from-point (plist-get props :point))) + (insert "\n\n")))))) (insert "\n\n* No backlinks!"))) (read-only-mode 1)))))) @@ -667,24 +724,24 @@ If item at point is not org-roam specific, default to Org behaviour." (org-roam--ensure-cache-built) (with-temp-buffer (insert "digraph {\n") - (dolist (file (org-roam--find-all-files)) - (let ((title (org-roam--get-title-or-slug file))) - (let ((shortened-title (s-truncate org-roam-graph-max-title-length title))) - (insert + (let ((rows (org-roam-sql [:select [file titles] :from titles]))) + (dolist (row rows) + (let* ((file (car row)) + (title (or (caadr row) + (org-roam--path-to-slug file))) + (shortened-title (s-truncate org-roam-graph-max-title-length title))) + (insert (format " \"%s\" [label=\"%s\", shape=%s, URL=\"roam://%s\", tooltip=\"%s\"];\n" - title + file shortened-title org-roam-graph-node-shape file - title - ))))) - (maphash - (lambda (from-link to-links) - (dolist (to-link to-links) - (insert (format " \"%s\" -> \"%s\";\n" - (org-roam--get-title-or-slug from-link) - (org-roam--get-title-or-slug to-link))))) - (org-roam--forward-links-cache)) + title))))) + (let ((link-rows (org-roam-sql [:select :distinct [file-to file-from] :from file-links]))) + (dolist (row link-rows) + (insert (format " \"%s\" -> \"%s\";\n" + (car row) + (cadr row))))) (insert "}") (buffer-string))) @@ -715,7 +772,6 @@ This needs to be quick/infrequent, because this is run at (when (and (or redisplay (not (eq org-roam--current-buffer buffer))) (eq 'visible (org-roam--current-visibility)) - (org-roam--cache-initialized-p) (buffer-local-value 'buffer-file-truename buffer)) (setq org-roam--current-buffer buffer) (org-roam-update (expand-file-name @@ -740,13 +796,8 @@ Applies `org-roam-link-face' if PATH correponds to a Roam file." (setq org-roam-last-window (get-buffer-window)) (add-hook 'post-command-hook #'org-roam--maybe-update-buffer nil t) (add-hook 'after-save-hook #'org-roam--update-cache nil t) - (if (org-roam--cache-initialized-p) - (org-roam--setup-found-file) - (org-roam--build-cache-async - (let ((buf (buffer-name))) - #'(lambda () - (with-current-buffer buf - (org-roam--setup-found-file)))))))) + (org-roam--setup-file-links) + (org-roam--maybe-update-buffer :redisplay nil))) (defun org-roam--setup-file-links () "Set up `file:' Org links with org-roam-link-face." @@ -759,11 +810,6 @@ This sets `file:' Org links to have the org-link face." (unless (version< org-version "9.2") (org-link-set-parameters "file" :face 'org-link))) -(defun org-roam--setup-found-file () - "Setup a buffer recognized via the \"find-file-hook\"." - (org-roam--setup-file-links) - (org-roam--maybe-update-buffer :redisplay nil)) - (defvar org-roam-mode-map (make-sparse-keymap) "Keymap for org-roam commands.") @@ -778,9 +824,10 @@ This sets `file:' Org links to have the org-link face." (not (auto-save-file-name-p new-file)) (org-roam--org-roam-file-p new-file)) (org-roam--ensure-cache-built) - (org-roam--clear-file-from-cache file) - - (let* ((files (gethash file (org-roam--backward-links-cache) nil)) + (let* ((files-to-rename (org-roam-sql [:select :distinct [file-from] + :from file-links + :where (= file-to $s1)] + file)) (path (file-truename file)) (new-path (file-truename new-file)) (slug (org-roam--get-title-or-slug file)) @@ -788,27 +835,31 @@ This sets `file:' Org links to have the org-link face." (new-slug (or (org-roam--get-title-from-cache path) (org-roam--get-title-or-slug new-path))) (new-title (format org-roam-link-title-format new-slug))) - (when files - (maphash (lambda (file-from props) - (let* ((file-dir (file-name-directory file-from)) - (relative-path (file-relative-name new-path file-dir)) - (old-relative-path (file-relative-name path file-dir)) - (slug-regex (regexp-quote (format "[[file:%s][%s]]" old-relative-path old-title))) - (named-regex (concat - (regexp-quote (format "[[file:%s][" old-relative-path)) - "\\(.*\\)" - (regexp-quote "]]")))) - (with-temp-file file-from - (insert-file-contents file-from) - (while (re-search-forward slug-regex nil t) - (replace-match (format "[[file:%s][%s]]" relative-path new-title))) - (goto-char (point-min)) - (while (re-search-forward named-regex nil t) - (replace-match (format "[[file:%s][\\1]]" relative-path)))) - (save-window-excursion - (find-file file-from) - (org-roam--update-cache)))) - files)) + (org-roam--clear-file-from-cache file) + (dolist (file-from files-to-rename) + (let* ((file-from (car file-from)) + (file-from (if (string-equal (file-truename file-from) + path) + new-path + file-from)) + (file-dir (file-name-directory file-from)) + (relative-path (file-relative-name new-path file-dir)) + (old-relative-path (file-relative-name path file-dir)) + (slug-regex (regexp-quote (format "[[file:%s][%s]]" old-relative-path old-title))) + (named-regex (concat + (regexp-quote (format "[[file:%s][" old-relative-path)) + "\\(.*\\)" + (regexp-quote "]]")))) + (with-temp-file file-from + (insert-file-contents file-from) + (while (re-search-forward slug-regex nil t) + (replace-match (format "[[file:%s][%s]]" relative-path new-title))) + (goto-char (point-min)) + (while (re-search-forward named-regex nil t) + (replace-match (format "[[file:%s][\\1]]" relative-path)))) + (save-window-excursion + (find-file file-from) + (org-roam--update-cache)))) (save-window-excursion (find-file new-path) (org-roam--update-cache))))) @@ -818,7 +869,7 @@ This sets `file:' Org links to have the org-link face." "Minor mode for Org-roam. When called interactively, toggle `org-roam-mode'. with prefix ARG, enable `org-roam-mode' -if ARG is posiwive, otherwise disable it. +if ARG is positive, otherwise disable it. When called from Lisp, enable `org-roam-mode' if ARG is omitted, nil, or positive. If ARG is `toggle', toggle `org-roam-mode'. Otherwise, behave as if called interactively." @@ -829,15 +880,16 @@ If ARG is `toggle', toggle `org-roam-mode'. Otherwise, behave as if called inter :global t (cond (org-roam-mode - (unless (org-roam--cache-initialized-p) - (org-roam--build-cache-async)) (add-hook 'find-file-hook #'org-roam--find-file-hook-function) + (add-hook 'kill-emacs-hook #'org-roam--db-close-all) (advice-add 'rename-file :after #'org-roam--rename-file-advice) (advice-add 'delete-file :before #'org-roam--delete-file-advice)) (t (remove-hook 'find-file-hook #'org-roam--find-file-hook-function) + (remove-hook 'kill-emacs-hook #'org-roam--db-close-all) (advice-remove 'rename-file #'org-roam--rename-file-advice) (advice-remove 'delete-file #'org-roam--delete-file-advice) + (org-roam--db-close-all) ;; Disable local hooks for all org-roam buffers (dolist (buf (org-roam--get-roam-buffers)) (with-current-buffer buf @@ -845,8 +897,6 @@ If ARG is `toggle', toggle `org-roam-mode'. Otherwise, behave as if called inter (remove-hook 'post-command-hook #'org-roam--maybe-update-buffer t) (remove-hook 'after-save-hook #'org-roam--update-cache t)))))) -(provide 'org-roam) - ;;; Show/hide the org-roam buffer (define-inline org-roam--current-visibility () "Return whether the current visibility state of the org-roam buffer. @@ -889,6 +939,9 @@ Valid states are 'visible, 'exists and 'none." ('visible (delete-window (get-buffer-window org-roam-buffer))) ('exists (org-roam--setup-buffer)) ('none (org-roam--setup-buffer)))) + +;;; - +(provide 'org-roam) ;;; org-roam.el ends here ;; Local Variables: diff --git a/tests/roam-files-multi/mf1.org b/tests/roam-files-multi/mf1.org deleted file mode 100644 index c09f809..0000000 --- a/tests/roam-files-multi/mf1.org +++ /dev/null @@ -1,7 +0,0 @@ -#+TITLE: Multi-File 1 - -link to [[file:nested/mf1.org][Nested Multi-File 1]] -link to [[file:mf2.org][Multi-File 2]] - -Arbitrary [[https://google.com][HTML]] link -Arbitrary text diff --git a/tests/roam-files-multi/mf2.org b/tests/roam-files-multi/mf2.org deleted file mode 100644 index 256b073..0000000 --- a/tests/roam-files-multi/mf2.org +++ /dev/null @@ -1,3 +0,0 @@ -#+TITLE: Multi-File 2 - -This file has no links. diff --git a/tests/roam-files-multi/mf3.org b/tests/roam-files-multi/mf3.org deleted file mode 100644 index a43de84..0000000 --- a/tests/roam-files-multi/mf3.org +++ /dev/null @@ -1,5 +0,0 @@ -#+TITLE: Multi-File 3 - -This file has a link to an file with no title. - -[[file:multi-no-title.org][multi-no-title]] diff --git a/tests/roam-files-multi/multi-no-title.org b/tests/roam-files-multi/multi-no-title.org deleted file mode 100644 index d751e65..0000000 --- a/tests/roam-files-multi/multi-no-title.org +++ /dev/null @@ -1 +0,0 @@ -no title in this file :O diff --git a/tests/roam-files-multi/nested/mf1.org b/tests/roam-files-multi/nested/mf1.org deleted file mode 100644 index e0e7fde..0000000 --- a/tests/roam-files-multi/nested/mf1.org +++ /dev/null @@ -1,4 +0,0 @@ -#+TITLE: Nested Multi-File 1 - -Link to [[file:mf2.org][Nested Multi-File 2]] -Link to [[file:../mf1.org][Mulit-File 1]] diff --git a/tests/roam-files-multi/nested/mf2.org b/tests/roam-files-multi/nested/mf2.org deleted file mode 100644 index 803a17b..0000000 --- a/tests/roam-files-multi/nested/mf2.org +++ /dev/null @@ -1,3 +0,0 @@ -#+TITLE: Nested Multi-File 2 - -Link to [[file:mf1.org][Nested Multi-File 1]] diff --git a/tests/roam-files/bar.org b/tests/roam-files/bar.org new file mode 100644 index 0000000..aa6c1ad --- /dev/null +++ b/tests/roam-files/bar.org @@ -0,0 +1,3 @@ +#+TITLE: Bar + +This is file bar. Bar links to [[file:nested/bar.org][Nested Bar]]. diff --git a/tests/roam-files/f1.org b/tests/roam-files/f1.org deleted file mode 100644 index 1effa2c..0000000 --- a/tests/roam-files/f1.org +++ /dev/null @@ -1,7 +0,0 @@ -#+TITLE: File 1 - -link to [[file:nested/f1.org][Nested File 1]] -link to [[file:f2.org][File 2]] - -Arbitrary [[https://google.com][HTML]] link -Arbitrary text diff --git a/tests/roam-files/f3.org b/tests/roam-files/f3.org deleted file mode 100644 index 13ff7c7..0000000 --- a/tests/roam-files/f3.org +++ /dev/null @@ -1,5 +0,0 @@ -#+TITLE: File 3 - -This file has a link to an file with no title. - -[[file:no-title.org][no-title]] diff --git a/tests/roam-files/foo.org b/tests/roam-files/foo.org new file mode 100644 index 0000000..463b0dd --- /dev/null +++ b/tests/roam-files/foo.org @@ -0,0 +1,8 @@ +#+TITLE: Foo + +This is the foo file. It contains a link to [[file:bar.org][Bar]]. + +To make the tests more robust, here are some arbitrary links: + +- [[https:google.com][Google]] +- [[mailto:foo@john.com][mail to foo]] diff --git a/tests/roam-files/nested/bar.org b/tests/roam-files/nested/bar.org new file mode 100644 index 0000000..7289e6d --- /dev/null +++ b/tests/roam-files/nested/bar.org @@ -0,0 +1,3 @@ +#+TITLE: Nested Bar + +This file is nested, 1 level deeper. It links to both [[file:../foo.org][Foo]] and [[file:foo.org][Nested Foo]]. diff --git a/tests/roam-files/nested/f1.org b/tests/roam-files/nested/f1.org deleted file mode 100644 index a09eb3f..0000000 --- a/tests/roam-files/nested/f1.org +++ /dev/null @@ -1,4 +0,0 @@ -#+TITLE: Nested File 1 - -Link to [[file:f2.org][Nested File 2]] -Link to [[file:../f1.org][File 1]] diff --git a/tests/roam-files/nested/f2.org b/tests/roam-files/nested/f2.org deleted file mode 100644 index 362ae4e..0000000 --- a/tests/roam-files/nested/f2.org +++ /dev/null @@ -1,3 +0,0 @@ -#+TITLE: Nested File 2 - -Link to [[file:f1.org][Nested File 1]] diff --git a/tests/roam-files/f2.org b/tests/roam-files/nested/foo.org similarity index 55% rename from tests/roam-files/f2.org rename to tests/roam-files/nested/foo.org index 797d8bb..c492dc1 100644 --- a/tests/roam-files/f2.org +++ b/tests/roam-files/nested/foo.org @@ -1,3 +1,3 @@ -#+TITLE: File 2 +#+TITLE: Nested Foo This file has no links. diff --git a/tests/roam-files/no-title.org b/tests/roam-files/no-title.org index d751e65..298e538 100644 --- a/tests/roam-files/no-title.org +++ b/tests/roam-files/no-title.org @@ -1 +1,3 @@ no title in this file :O + +links to itself, with no title: [[file:no-title.org][no-title]] diff --git a/tests/roam-files/unlinked.org b/tests/roam-files/unlinked.org new file mode 100644 index 0000000..2f72967 --- /dev/null +++ b/tests/roam-files/unlinked.org @@ -0,0 +1,3 @@ +#+TITLE: Unlinked + +Nothing links here :( diff --git a/tests/test-org-roam.el b/tests/test-org-roam.el index afdc45a..dae6098 100644 --- a/tests/test-org-roam.el +++ b/tests/test-org-roam.el @@ -29,6 +29,7 @@ (require 'buttercup) (require 'with-simulated-input) (require 'org-roam) +(require 'org-roam-db) (require 'dash) (defun abs-path (file-path) @@ -42,320 +43,262 @@ (defvar org-roam--tests-directory (file-truename (concat default-directory "tests/roam-files")) "Directory containing org-roam test org files.") -(defvar org-roam--tests-multi (file-truename (concat default-directory "tests/roam-files-multi")) - "Directory containing org-roam test org files.") - (defun org-roam--test-init () + (org-roam--db-close) (let ((original-dir org-roam--tests-directory) (new-dir (expand-file-name (make-temp-name "org-roam") temporary-file-directory))) (copy-directory original-dir new-dir) (setq org-roam-directory new-dir) - (setq org-roam-mute-cache-build t)) - (org-roam-mode +1)) - -(defun org-roam--test-multi-init () - (let ((original-dir-1 org-roam--tests-directory) - (original-dir-2 org-roam--tests-multi) - (new-dir-1 (expand-file-name (make-temp-name "org-roam") temporary-file-directory)) - (new-dir-2 (expand-file-name (make-temp-name "org-roam") temporary-file-directory))) - (copy-directory original-dir-1 new-dir-1) - (copy-directory original-dir-2 new-dir-2) - (setq org-roam-directory new-dir-1) - (setq org-roam-directory2 new-dir-2) - (setq org-roam-mute-cache-build t)) - (org-roam-mode +1)) - -(defun org-roam--test-build-cache () - "Builds the caches synchronously." - (let ((cache (org-roam--build-cache org-roam-directory))) - (let ((obj (org-roam--get-directory-cache))) - (oset obj initialized t) - (oset obj forward-links (plist-get cache :forward)) - (oset obj backward-links (plist-get cache :backward)) - (oset obj titles (plist-get cache :titles)) - (oset obj refs (plist-get cache :refs))))) + (org-roam-mode +1))) ;;; Tests -(describe "org-roam--build-cache-async" +(describe "org-roam-build-cache" (it "initializes correctly" - (org-roam--clear-cache) - (org-roam--test-multi-init) - (expect (org-roam--cache-initialized-p) :to-be nil) - (expect (hash-table-count (org-roam--forward-links-cache)) :to-be 0) - (expect (hash-table-count (org-roam--backward-links-cache)) :to-be 0) - (expect (hash-table-count (org-roam--titles-cache)) :to-be 0) + (org-roam--test-init) + (org-roam-build-cache) - (org-roam--build-cache-async) - (sleep-for 3) ;; Because it's async + ;; Cache + (expect (caar (org-roam-sql [:select (funcall count) :from files])) :to-be 8) + (expect (caar (org-roam-sql [:select (funcall count) :from file-links])) :to-be 5) + (expect (caar (org-roam-sql [:select (funcall count) :from titles])) :to-be 8) + (expect (caar (org-roam-sql [:select (funcall count) :from titles + :where titles :is-null])) :to-be 2) + (expect (caar (org-roam-sql [:select (funcall count) :from refs])) :to-be 1) - ;; Caches should be populated - (expect (org-roam--cache-initialized-p) :to-be t) - (expect (hash-table-count (org-roam--forward-links-cache)) :to-be 4) - (expect (hash-table-count (org-roam--backward-links-cache)) :to-be 5) - (expect (hash-table-count (org-roam--titles-cache)) :to-be 6) - (expect (hash-table-count (org-roam--refs-cache)) :to-be 1) + ;; TODO Test files - ;; Forward cache - (let ((f1 (gethash (abs-path "f1.org") - (org-roam--forward-links-cache))) - (f2 (gethash (abs-path "f2.org") - (org-roam--forward-links-cache))) - (nested-f1 (gethash (abs-path "nested/f1.org") - (org-roam--forward-links-cache))) - (nested-f2 (gethash (abs-path "nested/f2.org") - (org-roam--forward-links-cache))) - (expected-f1 (list (abs-path "nested/f1.org") - (abs-path "f2.org"))) - (expected-nested-f1 (list (abs-path "nested/f2.org") - (abs-path "f1.org"))) - (expected-nested-f2 (list (abs-path "nested/f1.org")))) + ;; Links -- File-from + (expect (caar (org-roam-sql [:select (funcall count) :from file-links + :where (= file-from $s1)] + (abs-path "foo.org"))) :to-be 1) + (expect (caar (org-roam-sql [:select (funcall count) :from file-links + :where (= file-from $s1)] + (abs-path "nested/bar.org"))) :to-be 2) - (expect f1 :to-have-same-items-as expected-f1) - (expect f2 :to-be nil) - (expect nested-f1 :to-have-same-items-as expected-nested-f1) - (expect nested-f2 :to-have-same-items-as expected-nested-f2)) + ;; Links -- File-to + (expect (caar (org-roam-sql [:select (funcall count) :from file-links + :where (= file-to $s1)] + (abs-path "nested/foo.org"))) :to-be 1) + (expect (caar (org-roam-sql [:select (funcall count) :from file-links + :where (= file-to $s1)] + (abs-path "nested/bar.org"))) :to-be 1) + (expect (caar (org-roam-sql [:select (funcall count) :from file-links + :where (= file-to $s1)] + (abs-path "unlinked.org"))) :to-be 0) + ;; TODO Test titles + (expect (org-roam-sql [:select * :from titles]) + :to-have-same-items-as + (list (list (abs-path "alias.org") + (list "t1" "a1" "a 2")) + (list (abs-path "bar.org") + (list "Bar")) + (list (abs-path "foo.org") + (list "Foo")) + (list (abs-path "nested/bar.org") + (list "Nested Bar")) + (list (abs-path "nested/foo.org") + (list "Nested Foo")) + (list (abs-path "no-title.org") nil) + (list (abs-path "web_ref.org") nil) + (list (abs-path "unlinked.org") + (list "Unlinked")))) - ;; Backward cache - (let ((f1 (hash-table-keys (gethash (abs-path "f1.org") - (org-roam--backward-links-cache)))) - (f2 (hash-table-keys (gethash (abs-path "f2.org") - (org-roam--backward-links-cache)))) - (nested-f1 (hash-table-keys - (gethash (abs-path "nested/f1.org") - (org-roam--backward-links-cache)))) - (nested-f2 (hash-table-keys - (gethash (abs-path "nested/f2.org") - (org-roam--backward-links-cache)))) - (expected-f1 (list (abs-path "nested/f1.org"))) - (expected-f2 (list (abs-path "f1.org"))) - (expected-nested-f1 (list (abs-path "nested/f2.org") - (abs-path "f1.org"))) - (expected-nested-f2 (list (abs-path "nested/f1.org")))) - (expect f1 :to-have-same-items-as expected-f1) - (expect f2 :to-have-same-items-as expected-f2) - (expect nested-f1 :to-have-same-items-as expected-nested-f1) - (expect nested-f2 :to-have-same-items-as expected-nested-f2)) + (expect (org-roam-sql [:select * :from refs]) + :to-have-same-items-as + (list (list "https://google.com/" (abs-path "web_ref.org")))) - ;; Titles Cache - (expect (gethash (abs-path "f1.org") - (org-roam--titles-cache)) :to-equal (list "File 1")) - (expect (gethash (abs-path "f2.org") - (org-roam--titles-cache)) :to-equal (list "File 2")) - (expect (gethash (abs-path "nested/f1.org") - (org-roam--titles-cache)) :to-equal (list "Nested File 1")) - (expect (gethash (abs-path "nested/f2.org") - (org-roam--titles-cache)) :to-equal (list "Nested File 2")) - (expect (gethash (abs-path "alias.org") - (org-roam--titles-cache)) :to-equal (list "t1" "a1" "a 2")) - (expect (gethash (abs-path "no-title.org") - (org-roam--titles-cache)) :to-be nil) - - ;; Refs Cache - (expect (gethash "https://google.com/" - (org-roam--refs-cache)) :to-equal (abs-path "web_ref.org")) - - ;; Multi - (let ((org-roam-directory org-roam-directory2)) - (org-roam--build-cache-async) - (sleep-for 3) ;; Because it's async - - ;; Caches should be populated - (expect (org-roam--cache-initialized-p) :to-be t) - (expect (hash-table-count (org-roam--forward-links-cache)) :to-be 4) - (expect (hash-table-count (org-roam--backward-links-cache)) :to-be 5) - (expect (hash-table-count (org-roam--titles-cache)) :to-be 5) - - ;; Forward cache - (let ((mf1 (gethash (abs-path "mf1.org") - (org-roam--forward-links-cache))) - (mf2 (gethash (abs-path "mf2.org") - (org-roam--forward-links-cache))) - (nested-mf1 (gethash (abs-path "nested/mf1.org") - (org-roam--forward-links-cache))) - (nested-mf2 (gethash (abs-path "nested/mf2.org") - (org-roam--forward-links-cache))) - (expected-mf1 (list (abs-path "nested/mf1.org") - (abs-path "mf2.org"))) - (expected-nested-mf1 (list (abs-path "nested/mf2.org") - (abs-path "mf1.org"))) - (expected-nested-mf2 (list (abs-path "nested/mf1.org")))) - - (expect mf1 :to-have-same-items-as expected-mf1) - (expect mf2 :to-be nil) - (expect nested-mf1 :to-have-same-items-as expected-nested-mf1) - (expect nested-mf2 :to-have-same-items-as expected-nested-mf2)) - - ;; Backward cache - (let ((mf1 (hash-table-keys - (gethash (abs-path "mf1.org") - (org-roam--backward-links-cache)))) - (mf2 (hash-table-keys - (gethash (abs-path "mf2.org") - (org-roam--backward-links-cache)))) - (nested-mf1 (hash-table-keys - (gethash (abs-path "nested/mf1.org") - (org-roam--backward-links-cache)))) - (nested-mf2 (hash-table-keys - (gethash (abs-path "nested/mf2.org") - (org-roam--backward-links-cache)))) - (expected-mf1 (list (abs-path "nested/mf1.org"))) - (expected-mf2 (list (abs-path "mf1.org"))) - (expected-nested-mf1 (list (abs-path "nested/mf2.org") - (abs-path "mf1.org"))) - (expected-nested-mf2 (list (abs-path "nested/mf1.org")))) - (expect mf1 :to-have-same-items-as expected-mf1) - (expect mf2 :to-have-same-items-as expected-mf2) - (expect nested-mf1 :to-have-same-items-as expected-nested-mf1) - (expect nested-mf2 :to-have-same-items-as expected-nested-mf2)) - - ;; Titles Cache - (expect (gethash (abs-path "mf1.org") - (org-roam--titles-cache)) - :to-equal (list "Multi-File 1")) - (expect (gethash (abs-path "mf2.org") - (org-roam--titles-cache)) - :to-equal (list "Multi-File 2")) - (expect (gethash (abs-path "nested/mf1.org") - (org-roam--titles-cache)) - :to-equal (list "Nested Multi-File 1")) - (expect (gethash (abs-path "nested/mf2.org") - (org-roam--titles-cache)) - :to-equal (list "Nested Multi-File 2")) - (expect (gethash (abs-path "no-title.org") - (org-roam--titles-cache)) - :to-be nil)))) + ;; Expect rebuilds to be really quick (nothing changed) + (expect (org-roam-build-cache) + :to-equal + (list :files 0 :links 0 :titles 0 :refs 0 :deleted 0)))) (describe "org-roam-insert" (before-each (org-roam--test-init) (org-roam--clear-cache) - (org-roam--test-build-cache)) + (org-roam-build-cache)) - (it "temp1 -> f1" + (it "temp1 -> foo" (let ((buf (org-roam--test-find-new-file "temp1.org"))) (with-current-buffer buf (with-simulated-input - "File SPC 1 RET" + "Foo RET" (org-roam-insert nil)))) - (expect (buffer-string) :to-match (regexp-quote "file:f1.org"))) + (expect (buffer-string) :to-match (regexp-quote "file:foo.org"))) - (it "temp2 -> nested/f1" + (it "temp2 -> nested/foo" (let ((buf (org-roam--test-find-new-file "temp2.org"))) (with-current-buffer buf (with-simulated-input - "Nested SPC File SPC 1 RET" + "Nested SPC Foo RET" (org-roam-insert nil)))) - (expect (buffer-string) :to-match (regexp-quote "file:nested/f1.org"))) + (expect (buffer-string) :to-match (regexp-quote "file:nested/foo.org"))) - (it "nested/temp3 -> f1" + (it "nested/temp3 -> foo" (let ((buf (org-roam--test-find-new-file "nested/temp3.org"))) (with-current-buffer buf (with-simulated-input - "File SPC 1 RET" + "Foo RET" (org-roam-insert nil)))) - (expect (buffer-string) :to-match (regexp-quote "file:../f1.org"))) + (expect (buffer-string) :to-match (regexp-quote "file:../foo.org"))) - (it "a/b/temp4 -> nested/f1" + (it "a/b/temp4 -> nested/foo" (let ((buf (org-roam--test-find-new-file "a/b/temp4.org"))) (with-current-buffer buf (with-simulated-input - "Nested SPC File SPC 1 RET" + "Nested SPC Foo RET" (org-roam-insert nil)))) - (expect (buffer-string) :to-match (regexp-quote "file:../../nested/f1.org")))) + (expect (buffer-string) :to-match (regexp-quote "file:../../nested/foo.org")))) (describe "rename file updates cache" (before-each (org-roam--test-init) (org-roam--clear-cache) - (org-roam--test-build-cache)) + (org-roam-build-cache)) - (it "f1 -> new_f1" - (rename-file (abs-path "f1.org") - (abs-path "new_f1.org")) + (it "foo -> new_foo" + (rename-file (abs-path "foo.org") + (abs-path "new_foo.org")) ;; Cache should be cleared of old file - (expect (gethash (abs-path "f1.org") (org-roam--forward-links-cache)) :to-be nil) - (expect (->> (org-roam--backward-links-cache) - (gethash (abs-path "nested/f1.org")) - (hash-table-keys) - (member (abs-path "f1.org"))) :to-be nil) + (expect (caar (org-roam-sql [:select (funcall count) + :from titles + :where (= file $s1)] + (abs-path "foo.org"))) :to-be 0) + (expect (caar (org-roam-sql [:select (funcall count) + :from refs + :where (= file $s1)] + (abs-path "foo.org"))) :to-be 0) + (expect (caar (org-roam-sql [:select (funcall count) + :from file-links + :where (= file-from $s1)] + (abs-path "foo.org"))) :to-be 0) - (expect (->> (org-roam--forward-links-cache) - (gethash (abs-path "new_f1.org"))) :not :to-be nil) - - (expect (->> (org-roam--forward-links-cache) - (gethash (abs-path "new_f1.org")) - (member (abs-path "nested/f1.org"))) :not :to-be nil) + ;; Cache should be updated + (expect (org-roam-sql [:select [file-to] + :from file-links + :where (= file-from $s1)] + (abs-path "new_foo.org")) + :to-have-same-items-as + (list (list (abs-path "bar.org")))) + (expect (org-roam-sql [:select [file-from] + :from file-links + :where (= file-to $s1)] + (abs-path "new_foo.org")) + :to-have-same-items-as + (list (list (abs-path "nested/bar.org")))) ;; Links are updated (expect (with-temp-buffer - (insert-file-contents (abs-path "nested/f1.org")) - (buffer-string)) :to-match (regexp-quote "[[file:../new_f1.org][File 1]]"))) + (insert-file-contents (abs-path "nested/bar.org")) + (buffer-string)) + :to-match + (regexp-quote "[[file:../new_foo.org][Foo]]"))) - (it "f1 -> f1 with spaces" - (rename-file (abs-path "f1.org") - (abs-path "f1 with spaces.org")) + (it "foo -> foo with spaces" + (rename-file (abs-path "foo.org") + (abs-path "foo with spaces.org")) ;; Cache should be cleared of old file - (expect (gethash (abs-path "f1.org") (org-roam--forward-links-cache)) :to-be nil) - (expect (->> (org-roam--backward-links-cache) - (gethash (abs-path "nested/f1.org")) - (hash-table-keys) - (member (abs-path "f1.org"))) :to-be nil) + (expect (caar (org-roam-sql [:select (funcall count) + :from titles + :where (= file $s1)] + (abs-path "foo.org"))) :to-be 0) + (expect (caar (org-roam-sql [:select (funcall count) + :from refs + :where (= file $s1)] + (abs-path "foo.org"))) :to-be 0) + (expect (caar (org-roam-sql [:select (funcall count) + :from file-links + :where (= file-from $s1)] + (abs-path "foo.org"))) :to-be 0) + + ;; Cache should be updated + (expect (org-roam-sql [:select [file-to] + :from file-links + :where (= file-from $s1)] + (abs-path "foo with spaces.org")) + :to-have-same-items-as + (list (list (abs-path "bar.org")))) + (expect (org-roam-sql [:select [file-from] + :from file-links + :where (= file-to $s1)] + (abs-path "foo with spaces.org")) + :to-have-same-items-as + (list (list (abs-path "nested/bar.org")))) + ;; Links are updated (expect (with-temp-buffer - (insert-file-contents (abs-path "nested/f1.org")) - (buffer-string)) :to-match (regexp-quote "[[file:../f1 with spaces.org][File 1]]"))) + (insert-file-contents (abs-path "nested/bar.org")) + (buffer-string)) + :to-match + (regexp-quote "[[file:../foo with spaces.org][Foo]]"))) (it "no-title -> meaningful-title" (rename-file (abs-path "no-title.org") (abs-path "meaningful-title.org")) ;; File has no forward links - (expect (gethash (abs-path "no-title.org") (org-roam--forward-links-cache)) :to-be nil) - (expect (gethash (abs-path "meaningful-title.org") - (org-roam--forward-links-cache)) :to-be nil) - - (expect (->> (org-roam--forward-links-cache) - (gethash (abs-path "f3.org")) - (member (abs-path "no-title.org"))) :to-be nil) - - (expect (->> (org-roam--forward-links-cache) - (gethash (abs-path "f3.org")) - (member (abs-path "meaningful-title.org"))) :not :to-be nil) + (expect (caar (org-roam-sql [:select (funcall count) + :from file-links + :where (= file-from $s1)] + (abs-path "no-title.org"))) :to-be 0) + (expect (caar (org-roam-sql [:select (funcall count) + :from file-links + :where (= file-from $s1)] + (abs-path "meaningful-title.org"))) :to-be 1) ;; Links are updated with the appropriate name (expect (with-temp-buffer - (insert-file-contents (abs-path "f3.org")) - (buffer-string)) :to-match (regexp-quote "[[file:meaningful-title.org][meaningful-title]]"))) + (insert-file-contents (abs-path "meaningful-title.org")) + (buffer-string)) + :to-match + (regexp-quote "[[file:meaningful-title.org][meaningful-title]]"))) (it "web_ref -> hello" - (expect (->> (org-roam--refs-cache) - (gethash "https://google.com/")) - :to-equal (abs-path "web_ref.org")) + (expect (org-roam-sql + [:select [file] :from refs + :where (= ref $s1)] + "https://google.com/") + :to-equal + (list (list (abs-path "web_ref.org")))) (rename-file (abs-path "web_ref.org") (abs-path "hello.org")) - (expect (->> (org-roam--refs-cache) - (gethash "https://google.com/")) - :to-equal (abs-path "hello.org")))) + (expect (org-roam-sql + [:select [file] :from refs + :where (= ref $s1)] + "https://google.com/") + :to-equal (list (list (abs-path "hello.org")))) + (expect (caar (org-roam-sql + [:select [ref] :from refs + :where (= file $s1)] + (abs-path "web_ref.org"))) + :to-equal nil))) (describe "delete file updates cache" (before-each (org-roam--test-init) (org-roam--clear-cache) - (org-roam--test-build-cache)) - (it "delete f1" - (delete-file (abs-path "f1.org")) - (expect (->> (org-roam--forward-links-cache) - (gethash (abs-path "f1.org"))) :to-be nil) - (expect (->> (org-roam--backward-links-cache) - (gethash (abs-path "nested/f1.org")) - (gethash (abs-path "f1.org"))) :to-be nil) - (expect (->> (org-roam--backward-links-cache) - (gethash (abs-path "nested/f1.org")) - (gethash (abs-path "nested/f2.org"))) :not :to-be nil)) + (org-roam-build-cache) + (sleep-for 1)) + + (it "delete foo" + (delete-file (abs-path "foo.org")) + (expect (caar (org-roam-sql [:select (funcall count) + :from titles + :where (= file $s1)] + (abs-path "foo.org"))) :to-be 0) + (expect (caar (org-roam-sql [:select (funcall count) + :from refs + :where (= file $s1)] + (abs-path "foo.org"))) :to-be 0) + (expect (caar (org-roam-sql [:select (funcall count) + :from file-links + :where (= file-from $s1)] + (abs-path "foo.org"))) :to-be 0)) + (it "delete web_ref" - (expect (->> (org-roam--refs-cache) - (gethash "https://google.com/")) - :to-equal (abs-path "web_ref.org")) + (expect (org-roam-sql [:select * :from refs]) + :to-have-same-items-as + (list (list "https://google.com/" (abs-path "web_ref.org")))) (delete-file (abs-path "web_ref.org")) - (expect (->> (org-roam--refs-cache) - (gethash "https://google.com/")) - :to-be nil))) + (expect (org-roam-sql [:select * :from refs]) + :to-have-same-items-as + (list))))