removed tco, added threading macros, added file comparisons
This commit is contained in:
parent
4956cde9bb
commit
e0a3f89e63
712
conf.org
712
conf.org
|
@ -2684,12 +2684,59 @@ These are =org-mode=-agnostic functions that pertain to sql. They are basically
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(require 'sql)
|
(require 'sql)
|
||||||
|
|
||||||
(defun nd/sql-cmd (db cmd)
|
;; this needs a better home :/
|
||||||
"Execute string CMD on database DB executing `sql-sqlite-program'.
|
(defun nd/alist-put (alist prop value &optional front)
|
||||||
Returns the output of CMD. SQL should not contain any quotes as if it
|
"For given ALIST, append VALUE to the current values in prop.
|
||||||
were entered on the shell."
|
Current values (that is the cdr of each key) is assumed to be a list.
|
||||||
(shell-command-to-string (format "%s %s \"%s\"" sql-sqlite-program db cmd)))
|
If PROP does not exist, create it. Return the new alist. If FRONT is
|
||||||
|
t, add to the front of current values list instead of the back."
|
||||||
|
(let* ((cur-cell (assoc prop alist))
|
||||||
|
(cur-values (cdr cur-cell)))
|
||||||
|
(cond
|
||||||
|
(cur-values
|
||||||
|
(let ((new-cdr (if front
|
||||||
|
`(,value ,@cur-values)
|
||||||
|
`(,@cur-values ,value))))
|
||||||
|
(setcdr cur-cell new-cdr) alist))
|
||||||
|
(cur-cell
|
||||||
|
(setcdr cur-cell `(,value)) alist)
|
||||||
|
(alist
|
||||||
|
(append alist `((,prop ,value))))
|
||||||
|
(t
|
||||||
|
`((,prop ,value))))))
|
||||||
|
|
||||||
|
;; this somehow doesn't exist O.o
|
||||||
|
;; this also needs a better home :/
|
||||||
|
(defun nd/sql-plist-get-vals(plist)
|
||||||
|
"Return all the values in PLIST."
|
||||||
|
(-slice plist 1 nil 2))
|
||||||
|
|
||||||
|
;; (defun nd/sql-construct-insert-transaction (all-data)
|
||||||
|
;; "Construct transaction string to insert ALL-DATA into SQL.
|
||||||
|
;; Does not actually execute the string."
|
||||||
|
;; (let* ((scan-tbl
|
||||||
|
;; (lambda (tbl)
|
||||||
|
;; (let ((name (car tbl))
|
||||||
|
;; (data (cdr tbl)))
|
||||||
|
;; (string-join (mapcar
|
||||||
|
;; (lambda (d)
|
||||||
|
;; (nd/sql-construct-insert name d))
|
||||||
|
;; data)))))
|
||||||
|
;; (ins (mapcar (lambda (tbl) (funcall scan-tbl tbl)) all-data))
|
||||||
|
;; (ins (string-join ins)))
|
||||||
|
;; (format "begin transaction; %s commit;" ins)))
|
||||||
|
|
||||||
|
;; SQL string parsing functions
|
||||||
|
(defun nd/sql-to-plist (out &rest cols)
|
||||||
|
"Parse SQL output string OUT to an plist representing the data.
|
||||||
|
COLS are the column names as symbols used to obtain OUT."
|
||||||
|
(unless (equal out "")
|
||||||
|
(let* ((out-trim (string-trim out))
|
||||||
|
(row-data (split-string out-trim "\n"))
|
||||||
|
(cell-data (mapcar (lambda (s) (split-string s "|")) row-data)))
|
||||||
|
(mapcar (lambda (d) (cl-mapcan #'list cols d)) cell-data))))
|
||||||
|
|
||||||
|
;; sql to string functions
|
||||||
(defun nd/sql-escape-text (txt)
|
(defun nd/sql-escape-text (txt)
|
||||||
"Escape and quote TXT in order to insert into sqlite db via 'insert'.
|
"Escape and quote TXT in order to insert into sqlite db via 'insert'.
|
||||||
This assumes the insertion command will be run on a shell where the
|
This assumes the insertion command will be run on a shell where the
|
||||||
|
@ -2707,33 +2754,123 @@ any other symbols to their symbol name."
|
||||||
(entry (symbol-name entry))
|
(entry (symbol-name entry))
|
||||||
(t "NULL")))
|
(t "NULL")))
|
||||||
|
|
||||||
(defun nd/sql-construct-insert (tbl-name tbl-data)
|
(defun nd/sql-kw-to-colname (kw)
|
||||||
"Concatenate DATA into escaped comma-separated string for SQL insertion."
|
"Return string representation of KW for column in sql database."
|
||||||
;; column names are the properties in the plist
|
(substring (symbol-name kw) 1))
|
||||||
(let* ((col-names (-slice tbl-data 0 nil 2))
|
|
||||||
|
(defun nd/sql-plist-concat (plist &optional sep)
|
||||||
|
"Concatenate a PLIST to string to be used in a SQL statement.
|
||||||
|
Returns a string formatted like 'prop1 = value1 SEP prop2 = value2'
|
||||||
|
from a plist like '(:prop1 value1 :prop2 value2)."
|
||||||
|
(let* ((sep (or sep ","))
|
||||||
|
(keys (plist-get-keys plist))
|
||||||
|
(keys (mapcar #'nd/sql-kw-to-colname keys))
|
||||||
|
(vals (nd/sql-plist-get-vals plist))
|
||||||
|
(vals (mapcar #'nd/sql-to-string vals))
|
||||||
|
(str (mapcar* (lambda (k v) (format "%s=%s" k v)) keys vals)))
|
||||||
|
(string-join str sep)))
|
||||||
|
|
||||||
|
;; SQL formatting functions
|
||||||
|
(defun nd/org-sql-fmt-insert (tbl-name tbl-data)
|
||||||
|
"Format SQL insert command from TBL-NAME and TBL-DATA."
|
||||||
|
(let* ((col-names (plist-get-keys tbl-data))
|
||||||
(col-names (mapcar (lambda (s) (substring (symbol-name s) 1)) col-names))
|
(col-names (mapcar (lambda (s) (substring (symbol-name s) 1)) col-names))
|
||||||
(col-names (string-join col-names ","))
|
(col-names (string-join col-names ","))
|
||||||
;; column values are the values of each property
|
(col-values (nd/sql-plist-get-vals tbl-data))
|
||||||
(col-values (-slice tbl-data 1 nil 2))
|
|
||||||
(col-values (mapcar #'nd/sql-to-string col-values))
|
(col-values (mapcar #'nd/sql-to-string col-values))
|
||||||
(col-values (string-join col-values ",")))
|
(col-values (string-join col-values ",")))
|
||||||
(format "insert into %s (%s) values (%s);" (symbol-name tbl-name)
|
(format "insert into %s (%s) values (%s);" (symbol-name tbl-name)
|
||||||
col-names col-values )))
|
col-names col-values )))
|
||||||
|
|
||||||
(defun nd/sql-construct-insert-transaction (all-data)
|
(defun nd/org-sql-fmt-update (tbl-name update)
|
||||||
"Construct transaction string to insert ALL-DATA into SQL.
|
"Format SQL update command from TBL-NAME, UPDATE, and CONDS."
|
||||||
Does not actually execute the string."
|
(let ((upd-str (nd/sql-plist-concat (car update)))
|
||||||
(let* ((scan-tbl
|
(conds-str (nd/sql-plist-concat (cdr update) " and ")))
|
||||||
(lambda (tbl)
|
(format "update %s set %s where %s;" (symbol-name tbl-name)
|
||||||
(let ((name (car tbl))
|
upd-str conds-str)))
|
||||||
(data (cdr tbl)))
|
|
||||||
(string-join (mapcar
|
(defun nd/org-sql-fmt-delete (tbl-name conds)
|
||||||
(lambda (d)
|
"Format SQL update command from TBL-NAME and CONDS."
|
||||||
(nd/sql-construct-insert name d))
|
(let ((conds-str (nd/sql-plist-concat conds " and ")))
|
||||||
data)))))
|
(format "delete from %s where %s;" (symbol-name tbl-name) conds-str)))
|
||||||
(ins (mapcar (lambda (tbl) (funcall scan-tbl tbl)) all-data))
|
|
||||||
(ins (string-join ins)))
|
(defun nd/org-sql-fmt-trans (sql-str)
|
||||||
(format "begin transaction; %s commit;" ins)))
|
"Format SQL transaction from list of SQL commands as strings SQL-STR."
|
||||||
|
(when sql-str
|
||||||
|
(nd/org-sql->> sql-str
|
||||||
|
(-flatten)
|
||||||
|
(string-join)
|
||||||
|
(format "begin transaction; %s commit;"))))
|
||||||
|
|
||||||
|
(defun nd/org-sql-fmt-multi (tbl fun)
|
||||||
|
(let ((name (car tbl))
|
||||||
|
(data (cdr tbl)))
|
||||||
|
(mapcar (lambda (r) (funcall fun name r)) data)))
|
||||||
|
|
||||||
|
(defun nd/org-sql-fmt-inserts (tbl)
|
||||||
|
(nd/org-sql-fmt-multi tbl #'nd/org-sql-fmt-insert))
|
||||||
|
|
||||||
|
(defun nd/org-sql-fmt-updates (tbl)
|
||||||
|
(nd/org-sql-fmt-multi tbl #'nd/org-sql-fmt-update))
|
||||||
|
|
||||||
|
(defun nd/org-sql-fmt-deletes (tbl)
|
||||||
|
(nd/org-sql-fmt-multi tbl #'nd/org-sql-fmt-delete))
|
||||||
|
|
||||||
|
;; SQL command abstractions
|
||||||
|
(defun nd/sql-cmd (db cmd &optional show-err foreign-keys)
|
||||||
|
"Execute string CMD on database DB executing `sql-sqlite-program'.
|
||||||
|
Returns the output of CMD. SQL should not contain any quotes as if it
|
||||||
|
were entered on the shell."
|
||||||
|
(when cmd
|
||||||
|
(let* ((err (if show-err "" " 2> /dev/null"))
|
||||||
|
(pragma (if foreign-keys
|
||||||
|
"PRAGMA foreign_keys = ON;"
|
||||||
|
"PRAGMA foreign_keys = OFF;"))
|
||||||
|
;; TODO, there has to be a better way to fix this foreign key bs
|
||||||
|
(cmd (format "%s %s \"%s%s\"%s" sql-sqlite-program db pragma cmd err)))
|
||||||
|
(shell-command-to-string cmd))))
|
||||||
|
|
||||||
|
(defun nd/sql-select (db tbl-name cols &optional conds)
|
||||||
|
"Select columns from TBL-NAME in DB where COLS is the list of columns.
|
||||||
|
If COLS is nil, all columns will be returned. Columns is expected as
|
||||||
|
a list of keywords like ':col1' and :col2'. CONDS, if supplied, is
|
||||||
|
a plist of conditions to test in the select statement. (currently
|
||||||
|
joined by AND)"
|
||||||
|
(let* ((colnames
|
||||||
|
(if (not cols) "*"
|
||||||
|
(string-join
|
||||||
|
(mapcar (lambda (s) (substring (symbol-name s) 1)) cols)
|
||||||
|
",")))
|
||||||
|
(tbl-str (symbol-name tbl-name))
|
||||||
|
(cmd (if (not conds)
|
||||||
|
(format "select %s from %s;" colnames tbl-str)
|
||||||
|
(let ((conds-str (nd/sql-plist-concat conds " and ")))
|
||||||
|
(format "select %s from %s where %s;" colnames
|
||||||
|
tbl-str conds-str))))
|
||||||
|
(out (nd/sql-cmd db cmd)))
|
||||||
|
(apply #'nd/sql-to-plist out cols)))
|
||||||
|
|
||||||
|
(defun nd/sql-delete (db tbl-name conds)
|
||||||
|
"Delete records from TBL-NAME in DB where CONDS are true.
|
||||||
|
CONDS is a plist of column names and values, '(:col1 val1 :col2 val2)',
|
||||||
|
where values will be deleted if the listed columns have the listed
|
||||||
|
values (AND condition)."
|
||||||
|
(let* ((conds-str (nd/sql-plist-concat " and "))
|
||||||
|
(cmd (format "delete from %s where %s;"
|
||||||
|
(symbol-name tbl-name) conds-str)))
|
||||||
|
(nd/sql-cmd db cmd)))
|
||||||
|
|
||||||
|
(defun nd/sql-update (db tbl-name update conds)
|
||||||
|
"Update records in TBL-NAME in DB with UPDATE where CONDS are true.
|
||||||
|
VALUES is a plist containing the columns and new values as
|
||||||
|
'(col1: newval1 col2: newval2) and CONDS is a similar plist
|
||||||
|
where columns in UPDATE will be updated if values matching those in
|
||||||
|
CONDS are found (AND condition)."
|
||||||
|
(let* ((upd-str (nd/sql-plist-concat update))
|
||||||
|
(conds-str (nd/sql-plist-to-condition conds " and "))
|
||||||
|
(cmd (format "update %s set %s where %s;"
|
||||||
|
(symbol-name tbl-name) upd-str conds-str)))
|
||||||
|
(nd/sql-cmd db cmd)))
|
||||||
|
|
||||||
(defun nd/sql-insert (db tbl-name tbl-data)
|
(defun nd/sql-insert (db tbl-name tbl-data)
|
||||||
"Insert list TBL-DATA into TBL-NAME in sqlite database DB."
|
"Insert list TBL-DATA into TBL-NAME in sqlite database DB."
|
||||||
|
@ -2742,70 +2879,6 @@ Does not actually execute the string."
|
||||||
(defun nd/sql-insert-multi (db all-data)
|
(defun nd/sql-insert-multi (db all-data)
|
||||||
"Insert ALL-DATA into sqlite DB."
|
"Insert ALL-DATA into sqlite DB."
|
||||||
(nd/sql-cmd db (nd/sql-construct-insert-transaction all-data)))
|
(nd/sql-cmd db (nd/sql-construct-insert-transaction all-data)))
|
||||||
|
|
||||||
(defun nd/alist-put (alist prop value &optional front)
|
|
||||||
"For given ALIST, append VALUE to the current values in prop.
|
|
||||||
Current values (that is the cdr of each key) is assumed to be a list.
|
|
||||||
If PROP does not exist, create it. Return the new alist. If FRONT is
|
|
||||||
t, add to the front of current values list instead of the back."
|
|
||||||
(let* ((cur-cell (assoc prop alist))
|
|
||||||
(cur-values (cdr cur-cell)))
|
|
||||||
(cond
|
|
||||||
(cur-values
|
|
||||||
(let ((new-cdr (if front
|
|
||||||
`(,value ,@cur-values)
|
|
||||||
`(,@cur-values ,value))))
|
|
||||||
(setcdr cur-cell new-cdr) alist))
|
|
||||||
(cur-cell
|
|
||||||
(setcdr cur-cell `(,value)) alist)
|
|
||||||
(alist
|
|
||||||
(append alist `((,prop ,value))))
|
|
||||||
(t
|
|
||||||
`((,prop ,value))))))
|
|
||||||
|
|
||||||
(defun nd/sql-to-plist (out &rest cols)
|
|
||||||
"Parse SQL output string OUT to an plist representing the data.
|
|
||||||
COLS are the column names as symbols used to obtain OUT."
|
|
||||||
(let* ((out-trim (string-trim out))
|
|
||||||
(row-data (split-string out-trim "\n"))
|
|
||||||
(cell-data (mapcar (lambda (s) (split-string s "|")) row-data)))
|
|
||||||
(mapcar (lambda (d) (cl-mapcan #'list cols d)) cell-data)))
|
|
||||||
|
|
||||||
(defun nd/sql-select (db tbl-name &rest cols)
|
|
||||||
"Select columns from TBL-NAME in DB where COLS is the list of columns.
|
|
||||||
If COLS is nil, all columns will be returned. Columns is expected as
|
|
||||||
a list of keywords like ':col1' and :col2'."
|
|
||||||
(let* ((colnames
|
|
||||||
(if (not cols) "*"
|
|
||||||
(string-join
|
|
||||||
(mapcar (lambda (s) (substring (symbol-name s) 1)) cols)
|
|
||||||
",")))
|
|
||||||
(cmd (format "select %s from %s;" colnames (symbol-name tbl-name)))
|
|
||||||
(out (nd/sql-cmd db cmd)))
|
|
||||||
(apply #'nd/sql-to-plist out cols)))
|
|
||||||
|
|
||||||
;; this somehow doesn't exist O.o
|
|
||||||
(defun nd/sql-plist-get-vals(plist)
|
|
||||||
"Return all the values in PLIST."
|
|
||||||
(-slice plist 1 nil 2))
|
|
||||||
|
|
||||||
(defun nd/sql-kw-to-colname (kw)
|
|
||||||
"Returns string representation of KW for column in sql database."
|
|
||||||
(substring (symbol-name kw) 1))
|
|
||||||
|
|
||||||
(defun nd/sql-delete (db tbl-name cols)
|
|
||||||
"Delete records from TBL-NAME in DB where COLS are true.
|
|
||||||
COND is a plist of column names and values, '(:col1 val1 :col2 val2)',
|
|
||||||
where values will be deleted if the listed columns have the listed
|
|
||||||
values (AND condition)."
|
|
||||||
(let* ((keys (plist-get-keys cols))
|
|
||||||
(keys (mapcar #'nd/sql-kw-to-colname keys))
|
|
||||||
(vals (nd/sql-plist-get-vals cols))
|
|
||||||
(vals (mapcar #'nd/sql-to-string vals))
|
|
||||||
(conds-str (mapcar* (lambda (k v) (format "%s=%s" k v)) keys vals))
|
|
||||||
(conds-str (string-join conds-str " and "))
|
|
||||||
(cmd (format "delete from %s where %s;" (symbol-name tbl-name) conds-str)))
|
|
||||||
(nd/sql-cmd db cmd)))
|
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
**** org parsing function
|
**** org parsing function
|
||||||
Basic functions to parse org strings
|
Basic functions to parse org strings
|
||||||
|
@ -2841,17 +2914,15 @@ If TS is nil or TS cannot be understood, nil will be returned."
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
**** org sql schemas
|
**** org sql schemas
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(defconst nd/org-sqlite-files-schema
|
(defconst nd/org-sql-schemas
|
||||||
"CREATE TABLE files (
|
'("CREATE TABLE files (
|
||||||
file_path TEXT PRIMARY KEY ASC,
|
file_path TEXT PRIMARY KEY ASC,
|
||||||
md5 TEXT NOT NULL,
|
md5 TEXT NOT NULL,
|
||||||
size INTEGER NOT NULL,
|
size INTEGER NOT NULL,
|
||||||
time_modified DATE,
|
time_modified DATE,
|
||||||
time_created DATE,
|
time_created DATE,
|
||||||
time_accessed DATE);"
|
time_accessed DATE);"
|
||||||
"Schema to build the files table in the org db.")
|
|
||||||
|
|
||||||
(defconst nd/org-sqlite-header-schema
|
|
||||||
"CREATE TABLE headlines (
|
"CREATE TABLE headlines (
|
||||||
file_path TEXT,
|
file_path TEXT,
|
||||||
headline_offset INTEGER,
|
headline_offset INTEGER,
|
||||||
|
@ -2867,10 +2938,9 @@ priority INTEGER,
|
||||||
content TEXT,
|
content TEXT,
|
||||||
PRIMARY KEY (file_path ASC, headline_offset ASC),
|
PRIMARY KEY (file_path ASC, headline_offset ASC),
|
||||||
FOREIGN KEY (file_path) REFERENCES files (file_path)
|
FOREIGN KEY (file_path) REFERENCES files (file_path)
|
||||||
|
ON UPDATE CASCADE
|
||||||
ON DELETE CASCADE);"
|
ON DELETE CASCADE);"
|
||||||
"Schema to build the headers table in the org db.")
|
|
||||||
|
|
||||||
(defconst nd/org-sqlite-tags-schema
|
|
||||||
"CREATE TABLE tags (
|
"CREATE TABLE tags (
|
||||||
file_path TEXT,
|
file_path TEXT,
|
||||||
headline_offset INTEGER,
|
headline_offset INTEGER,
|
||||||
|
@ -2878,11 +2948,10 @@ tag TEXT,
|
||||||
inherited BOOLEAN,
|
inherited BOOLEAN,
|
||||||
FOREIGN KEY (file_path, headline_offset)
|
FOREIGN KEY (file_path, headline_offset)
|
||||||
REFERENCES headlines (file_path, headline_offset)
|
REFERENCES headlines (file_path, headline_offset)
|
||||||
|
ON UPDATE CASCADE
|
||||||
ON DELETE CASCADE,
|
ON DELETE CASCADE,
|
||||||
PRIMARY KEY (file_path, headline_offset, tag, inherited));"
|
PRIMARY KEY (file_path, headline_offset, tag, inherited));"
|
||||||
"Schema to build the tags table in the org db.")
|
|
||||||
|
|
||||||
(defconst nd/org-sqlite-properties-schema
|
|
||||||
"CREATE TABLE properties (
|
"CREATE TABLE properties (
|
||||||
file_path TEXT,
|
file_path TEXT,
|
||||||
headline_offset INTEGER,
|
headline_offset INTEGER,
|
||||||
|
@ -2892,11 +2961,10 @@ val_text TEXT NOT NULL,
|
||||||
inherited BOOLEAN,
|
inherited BOOLEAN,
|
||||||
FOREIGN KEY (file_path, headline_offset)
|
FOREIGN KEY (file_path, headline_offset)
|
||||||
REFERENCES headlines (file_path, headline_offset)
|
REFERENCES headlines (file_path, headline_offset)
|
||||||
|
ON UPDATE CASCADE
|
||||||
ON DELETE CASCADE,
|
ON DELETE CASCADE,
|
||||||
PRIMARY KEY (file_path ASC, property_offset ASC));"
|
PRIMARY KEY (file_path ASC, property_offset ASC));"
|
||||||
"Schema to build the properties table in the org db.")
|
|
||||||
|
|
||||||
(defconst nd/org-sqlite-clocking-schema
|
|
||||||
"CREATE TABLE clocking (
|
"CREATE TABLE clocking (
|
||||||
file_path TEXT,
|
file_path TEXT,
|
||||||
headline_offset INTEGER,
|
headline_offset INTEGER,
|
||||||
|
@ -2906,11 +2974,10 @@ time_end DATE,
|
||||||
clock_note TEXT,
|
clock_note TEXT,
|
||||||
FOREIGN KEY (file_path, headline_offset)
|
FOREIGN KEY (file_path, headline_offset)
|
||||||
REFERENCES headlines (file_path, headline_offset)
|
REFERENCES headlines (file_path, headline_offset)
|
||||||
|
ON UPDATE CASCADE
|
||||||
ON DELETE CASCADE,
|
ON DELETE CASCADE,
|
||||||
PRIMARY KEY (file_path ASC, clock_offset ASC));"
|
PRIMARY KEY (file_path ASC, clock_offset ASC));"
|
||||||
"Schema to build the clocking table in the org db.")
|
|
||||||
|
|
||||||
(defconst nd/org-sqlite-logbook-schema
|
|
||||||
"CREATE TABLE logbook (
|
"CREATE TABLE logbook (
|
||||||
file_path TEXT,
|
file_path TEXT,
|
||||||
headline_offset INTEGER,
|
headline_offset INTEGER,
|
||||||
|
@ -2920,23 +2987,21 @@ header TEXT,
|
||||||
note TEXT,
|
note TEXT,
|
||||||
FOREIGN KEY (file_path, headline_offset)
|
FOREIGN KEY (file_path, headline_offset)
|
||||||
REFERENCES headlines (file_path, headline_offset)
|
REFERENCES headlines (file_path, headline_offset)
|
||||||
|
ON UPDATE CASCADE
|
||||||
ON DELETE CASCADE,
|
ON DELETE CASCADE,
|
||||||
PRIMARY KEY (file_path ASC, entry_offset ASC));"
|
PRIMARY KEY (file_path ASC, entry_offset ASC));"
|
||||||
"Schema to build the logbook table in the org db.")
|
|
||||||
|
|
||||||
(defconst nd/org-sqlite-state-changes-schema
|
|
||||||
"CREATE TABLE state_changes (
|
"CREATE TABLE state_changes (
|
||||||
file_path TEXT,
|
file_path TEXT,
|
||||||
entry_offset INTEGER,
|
entry_offset INTEGER,
|
||||||
state_old TEXT NOT NULL,
|
state_old TEXT NOT NULL,
|
||||||
state_new TEXT NOT NULL,
|
state_new TEXT NOT NULL,
|
||||||
FOREIGN KEY (file_path, entry_offset)
|
FOREIGN KEY (file_path, entry_offset)
|
||||||
REFERENCES headlines (file_path, headline_offset)
|
REFERENCES logbook (file_path, entry_offset)
|
||||||
|
ON UPDATE CASCADE
|
||||||
ON DELETE CASCADE,
|
ON DELETE CASCADE,
|
||||||
PRIMARY KEY (file_path ASC, entry_offset ASC));"
|
PRIMARY KEY (file_path ASC, entry_offset ASC));"
|
||||||
"Schema to build the state_changes table in the org db.")
|
|
||||||
|
|
||||||
(defconst nd/org-sqlite-planning-changes-schema
|
|
||||||
"CREATE TABLE planning_changes (
|
"CREATE TABLE planning_changes (
|
||||||
file_path TEXT,
|
file_path TEXT,
|
||||||
entry_offset INTEGER,
|
entry_offset INTEGER,
|
||||||
|
@ -2945,11 +3010,10 @@ time_new DATE,
|
||||||
planning_type TEXT CHECK (planning_type = \\\"d\\\" or (planning_type = \\\"s\\\")),
|
planning_type TEXT CHECK (planning_type = \\\"d\\\" or (planning_type = \\\"s\\\")),
|
||||||
FOREIGN KEY (file_path, entry_offset)
|
FOREIGN KEY (file_path, entry_offset)
|
||||||
REFERENCES logbook (file_path, entry_offset)
|
REFERENCES logbook (file_path, entry_offset)
|
||||||
|
ON UPDATE CASCADE
|
||||||
ON DELETE CASCADE,
|
ON DELETE CASCADE,
|
||||||
PRIMARY KEY (file_path ASC, entry_offset ASC));"
|
PRIMARY KEY (file_path ASC, entry_offset ASC));"
|
||||||
"Schema to build the planning_changes table in the org db.")
|
|
||||||
|
|
||||||
(defconst nd/org-sqlite-links-schema
|
|
||||||
"CREATE TABLE links (
|
"CREATE TABLE links (
|
||||||
file_path TEXT,
|
file_path TEXT,
|
||||||
headline_offset INTEGER,
|
headline_offset INTEGER,
|
||||||
|
@ -2959,9 +3023,10 @@ link_text TEXT,
|
||||||
link_type TEXT,
|
link_type TEXT,
|
||||||
FOREIGN KEY (file_path, headline_offset)
|
FOREIGN KEY (file_path, headline_offset)
|
||||||
REFERENCES headlines (file_path, headline_offset)
|
REFERENCES headlines (file_path, headline_offset)
|
||||||
|
ON UPDATE CASCADE
|
||||||
ON DELETE CASCADE,
|
ON DELETE CASCADE,
|
||||||
PRIMARY KEY (file_path ASC, link_offset ASC));"
|
PRIMARY KEY (file_path ASC, link_offset ASC));")
|
||||||
"Schema to build the links table in the org db.")
|
"Table schemas for the org database.")
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
**** org element functions
|
**** org element functions
|
||||||
These are functions that operate on org-element objects to parse for insertion into the db.
|
These are functions that operate on org-element objects to parse for insertion into the db.
|
||||||
|
@ -3092,9 +3157,12 @@ and cdr is the match data."
|
||||||
"Return `org-todo-keywords' as string list w/o selectors.
|
"Return `org-todo-keywords' as string list w/o selectors.
|
||||||
Will likely match the value of `org-todo-keywords-1' in many cases,
|
Will likely match the value of `org-todo-keywords-1' in many cases,
|
||||||
but this has the advantage of being always available and comprehensive."
|
but this has the advantage of being always available and comprehensive."
|
||||||
(mapcar
|
(nd/org-sql->>
|
||||||
(lambda (s) (replace-regexp-in-string "(.*)" "" s))
|
org-todo-keywords
|
||||||
(remove "|" (mapcan #'cdr (copy-tree org-todo-keywords)))))
|
(copy-tree)
|
||||||
|
(mapcan #'cdr)
|
||||||
|
(remove "|")
|
||||||
|
(mapcar (lambda (s) (replace-regexp-in-string "(.*)" "" s)))))
|
||||||
|
|
||||||
(defun nd/org-log-note-headings-matcher ()
|
(defun nd/org-log-note-headings-matcher ()
|
||||||
"Convert `org-log-note-headings' to a regex matcher.
|
"Convert `org-log-note-headings' to a regex matcher.
|
||||||
|
@ -3105,24 +3173,24 @@ of the escapes."
|
||||||
(let* ((escapes '("%u" "%U" "%t" "%T" "%d" "%D" "%s" "%S"))
|
(let* ((escapes '("%u" "%U" "%t" "%T" "%d" "%D" "%s" "%S"))
|
||||||
(todo-list (nd/org-todo-keywords-stripped))
|
(todo-list (nd/org-todo-keywords-stripped))
|
||||||
(todo-regexp (mapconcat #'regexp-quote todo-list "\\|"))
|
(todo-regexp (mapconcat #'regexp-quote todo-list "\\|"))
|
||||||
(ts-or-todo-regexp (concat "\"\\(" org-ts-regexp-inactive "\\|"
|
(ts-or-todo-regexp (format "\"\\(%s\\|%s\\)\""
|
||||||
todo-regexp "\\)\""))
|
org-ts-regexp-inactive
|
||||||
(org-ts-regexp-cap (concat "\\(" org-ts-regexp "\\)"))
|
todo-regexp))
|
||||||
(org-ts-regexp-inactive-cap (concat "\\(" org-ts-regexp-inactive "\\)"))
|
(org-ts-regexp-cap (format "\\(%s\\)" org-ts-regexp))
|
||||||
(re-matchers (list ".*"
|
(org-ts-regexp-inactive-cap (format "\\(%s\\)" org-ts-regexp-inactive))
|
||||||
".*"
|
|
||||||
org-ts-regexp-inactive-cap
|
|
||||||
org-ts-regexp-cap
|
|
||||||
org-ts-regexp-inactive-cap
|
|
||||||
org-ts-regexp-cap
|
|
||||||
ts-or-todo-regexp
|
|
||||||
ts-or-todo-regexp))
|
|
||||||
(re-matchers-pad (mapcar
|
|
||||||
(lambda (s)
|
|
||||||
(concat "[[:space:]]*" s "[[:space:]]*"))
|
|
||||||
re-matchers))
|
|
||||||
(re-no-pad-alist (mapcar* #'cons escapes escapes))
|
(re-no-pad-alist (mapcar* #'cons escapes escapes))
|
||||||
(re-match-alist (mapcar* #'cons escapes re-matchers-pad))
|
(re-match-alist
|
||||||
|
(nd/org-sql->>
|
||||||
|
(list ".*"
|
||||||
|
".*"
|
||||||
|
org-ts-regexp-inactive-cap
|
||||||
|
org-ts-regexp-cap
|
||||||
|
org-ts-regexp-inactive-cap
|
||||||
|
org-ts-regexp-cap
|
||||||
|
ts-or-todo-regexp
|
||||||
|
ts-or-todo-regexp)
|
||||||
|
(mapcar (lambda (s) (concat "[[:space:]]*" s "[[:space:]]*")))
|
||||||
|
(mapcar* #'cons escapes)))
|
||||||
(apply2note
|
(apply2note
|
||||||
(lambda (n f)
|
(lambda (n f)
|
||||||
(let ((note-type (car n))
|
(let ((note-type (car n))
|
||||||
|
@ -3137,17 +3205,18 @@ of the escapes."
|
||||||
(lambda (n)
|
(lambda (n)
|
||||||
(funcall apply2note
|
(funcall apply2note
|
||||||
n
|
n
|
||||||
(lambda (s) (replace-regexp-in-string "\s+" " " s)))))
|
(lambda (s) (replace-regexp-in-string "\s+" " " s))))))
|
||||||
(new org-log-note-headings))
|
(nd/org-sql->>
|
||||||
;; remove padding information by replacing all escape sequences
|
org-log-note-headings
|
||||||
;; with their non-padded version and then removing extra spaces
|
;; remove padding information by replacing all escape sequences
|
||||||
(setq new (mapcar (lambda (n) (funcall replace-esc n re-no-pad-alist)) new))
|
;; with their non-padded version and then removing extra spaces
|
||||||
(setq new (mapcar (lambda (n) (funcall shrink-space n)) new))
|
(mapcar (lambda (n) (funcall replace-esc n re-no-pad-alist)))
|
||||||
;; replace all escape sequences with regexps that match
|
(mapcar (lambda (n) (funcall shrink-space n)))
|
||||||
;; the data to be inserted via the escape sequences
|
;; replace all escape sequences with regexps that match
|
||||||
(setq new (mapcar (lambda (n) (funcall replace-esc n re-match-alist)) new))
|
;; the data to be inserted via the escape sequences
|
||||||
;; filter out anything that is blank (eg default clock-in)
|
(mapcar (lambda (n) (funcall replace-esc n re-match-alist)))
|
||||||
(seq-filter (lambda (s) (not (equal (cdr s) ""))) new)))
|
;; filter out anything that is blank (eg default clock-in)
|
||||||
|
(seq-filter (lambda (s) (not (equal (cdr s) "")))))))
|
||||||
|
|
||||||
(defconst nd/org-log-note-headings-regexp
|
(defconst nd/org-log-note-headings-regexp
|
||||||
(nd/org-log-note-headings-matcher)
|
(nd/org-log-note-headings-matcher)
|
||||||
|
@ -3253,21 +3322,26 @@ The header text is solely used for determining :type and :match-data."
|
||||||
**** org sql db function
|
**** org sql db function
|
||||||
These are the main functions to populate the db.
|
These are the main functions to populate the db.
|
||||||
#+BEGIN_SRC emacs-lisp
|
#+BEGIN_SRC emacs-lisp
|
||||||
(defun nd/org-init-db ()
|
(defmacro nd/org-sql-> (&rest body)
|
||||||
"Make a sqlite database for org archive files if it does not exist already."
|
(let ((result (pop body)))
|
||||||
(unless (file-exists-p nd/org-sqlite-db-path)
|
(dolist (form body result)
|
||||||
(process-file-shell-command (concat "touch " nd/org-sqlite-db-path))
|
(setq result (append (list (car form) result) (cdr form))))))
|
||||||
(nd/sql-cmd nd/org-sqlite-db-path nd/org-sqlite-files-schema)
|
|
||||||
(nd/sql-cmd nd/org-sqlite-db-path nd/org-sqlite-header-schema)
|
|
||||||
(nd/sql-cmd nd/org-sqlite-db-path nd/org-sqlite-properties-schema)
|
|
||||||
(nd/sql-cmd nd/org-sqlite-db-path nd/org-sqlite-tags-schema)
|
|
||||||
(nd/sql-cmd nd/org-sqlite-db-path nd/org-sqlite-clocking-schema)
|
|
||||||
(nd/sql-cmd nd/org-sqlite-db-path nd/org-sqlite-logbook-schema)
|
|
||||||
(nd/sql-cmd nd/org-sqlite-db-path nd/org-sqlite-state-changes-schema)
|
|
||||||
(nd/sql-cmd nd/org-sqlite-db-path nd/org-sqlite-planning-changes-schema)
|
|
||||||
(nd/sql-cmd nd/org-sqlite-db-path nd/org-sqlite-links-schema)))
|
|
||||||
|
|
||||||
(defun nd/org-sql-extract-lb-header (item-part acc)
|
(defmacro nd/org-sql->> (&rest body)
|
||||||
|
(let ((result (pop body)))
|
||||||
|
(dolist (form body result)
|
||||||
|
(setq result (append form (list result))))))
|
||||||
|
|
||||||
|
(defun nd/org-sql-extract (acc fun objs &rest args)
|
||||||
|
"Iterate through OBJS and add them to accumulator ACC using FUN.
|
||||||
|
FUN is a function that takes a single object from OBJS, the accumulator,
|
||||||
|
and ARGS. FUN adds OBJ to ACC and returns a new ACC."
|
||||||
|
(while objs
|
||||||
|
(setq acc (apply fun acc (car objs) args)
|
||||||
|
objs (cdr objs)))
|
||||||
|
acc)
|
||||||
|
|
||||||
|
(defun nd/org-sql-extract-lb-header (acc item-part)
|
||||||
"Add specific data from logbook entry ITEM-PART to accumulator ACC.
|
"Add specific data from logbook entry ITEM-PART to accumulator ACC.
|
||||||
ITEM-PART is a partitions logbook item as described in
|
ITEM-PART is a partitions logbook item as described in
|
||||||
`nd/org-sql-partition-item'. Note headings are parsed according to
|
`nd/org-sql-partition-item'. Note headings are parsed according to
|
||||||
|
@ -3326,7 +3400,7 @@ ITEM-PART is a partitioned logbook item as described in
|
||||||
(set-match-data (alist-get :match-data item-part))
|
(set-match-data (alist-get :match-data item-part))
|
||||||
(nd/org-ts-format-to-iso (match-string time-index header-text)))))
|
(nd/org-ts-format-to-iso (match-string time-index header-text)))))
|
||||||
|
|
||||||
(defun nd/org-sql-extract-lb-entry (item-part acc)
|
(defun nd/org-sql-extract-lb-entry (acc item-part)
|
||||||
"Add data from logbook entry ITEM-PART to accumulator ACC.
|
"Add data from logbook entry ITEM-PART to accumulator ACC.
|
||||||
ITEM-PART is a partitioned logbook item as described in
|
ITEM-PART is a partitioned logbook item as described in
|
||||||
`nd/org-sql-partition-item'."
|
`nd/org-sql-partition-item'."
|
||||||
|
@ -3344,9 +3418,11 @@ ITEM-PART is a partitioned logbook item as described in
|
||||||
:entry_offset item-offset
|
:entry_offset item-offset
|
||||||
:time_logged time-logged
|
:time_logged time-logged
|
||||||
:header hdr-text
|
:header hdr-text
|
||||||
:note note-text))
|
:note note-text)))
|
||||||
(acc* (nd/alist-put acc 'logbook logbook-data)))
|
(nd/org-sql->
|
||||||
(nd/org-sql-extract-lb-header item-part acc*)))
|
acc
|
||||||
|
(nd/alist-put 'logbook logbook-data)
|
||||||
|
(nd/org-sql-extract-lb-header item-part))))
|
||||||
|
|
||||||
(defun nd/org-logbook-parse-timestamp-range (ts)
|
(defun nd/org-logbook-parse-timestamp-range (ts)
|
||||||
"Return start and end of timestamp TS depending on if it is a range.
|
"Return start and end of timestamp TS depending on if it is a range.
|
||||||
|
@ -3364,7 +3440,7 @@ Return value will be a list of two elements if range and one if not."
|
||||||
(cons start end))
|
(cons start end))
|
||||||
`(,(funcall split ts))))))
|
`(,(funcall split ts))))))
|
||||||
|
|
||||||
(defun nd/org-sql-extract-lb-clock (clock acc hl-part &optional item)
|
(defun nd/org-sql-extract-lb-clock (acc clock hl-part &optional item)
|
||||||
"Add data from logbook CLOCK to accumulator ACC.
|
"Add data from logbook CLOCK to accumulator ACC.
|
||||||
HL-PART is an object as returned by `nd/org-sql-partition-headline'
|
HL-PART is an object as returned by `nd/org-sql-partition-headline'
|
||||||
and represents the headline surrounding the clock.
|
and represents the headline surrounding the clock.
|
||||||
|
@ -3390,38 +3466,40 @@ added to the clock, else add it as a normal logbook entry."
|
||||||
(if item-type
|
(if item-type
|
||||||
;; if we know the type, add the clock and note
|
;; if we know the type, add the clock and note
|
||||||
;; separately
|
;; separately
|
||||||
(let ((acc* (nd/alist-put acc 'clocking clock-data)))
|
(nd/org-sql->
|
||||||
(nd/org-sql-extract-lb-entry item-part acc*))
|
acc
|
||||||
|
(nd/alist-put 'clocking clock-data)
|
||||||
|
(nd/org-sql-extract-lb-entry item-part))
|
||||||
;; else add it with the clocking table
|
;; else add it with the clocking table
|
||||||
(let* ((hdr-text (alist-get :header-text item-part))
|
(let* ((hdr-text (alist-get :header-text item-part))
|
||||||
(clock-data* `(,@clock-data :clock_note ,hdr-text)))
|
(clock-data* `(,@clock-data :clock_note ,hdr-text)))
|
||||||
(nd/alist-put acc 'clocking clock-data*)))))))
|
(nd/alist-put acc 'clocking clock-data*)))))))
|
||||||
|
|
||||||
(defun nd/org-sql-extract-lb-items (items acc hl-part)
|
(defun nd/org-sql-extract-lb-items (acc items hl-part)
|
||||||
"Add data from logbook ITEMS to accumulator ACC.
|
"Add data from logbook ITEMS to accumulator ACC.
|
||||||
HL-PART is an object as returned by `nd/org-sql-partition-headline'
|
HL-PART is an object as returned by `nd/org-sql-partition-headline'
|
||||||
and represents the headline surrounding the items."
|
and represents the headline surrounding the items."
|
||||||
(let ((into
|
(let ((from
|
||||||
(lambda (item acc hl-part)
|
(lambda (acc item hl-part)
|
||||||
(let ((item-part (nd/org-sql-partition-item item hl-part)))
|
(let ((item-part (nd/org-sql-partition-item item hl-part)))
|
||||||
(nd/org-sql-extract-lb-entry item-part acc)))))
|
(nd/org-sql-extract-lb-entry acc item-part)))))
|
||||||
(nd/org-sql-extract items into acc hl-part)))
|
(nd/org-sql-extract acc from items hl-part)))
|
||||||
|
|
||||||
(defun nd/org-sql-extract-lb-one (entry acc hl-part)
|
(defun nd/org-sql-extract-lb-one (acc entry hl-part)
|
||||||
"Add data from logbook ENTRY to accumulator ACC.
|
"Add data from logbook ENTRY to accumulator ACC.
|
||||||
HL-PART is an object as returned by `nd/org-sql-partition-headline'
|
HL-PART is an object as returned by `nd/org-sql-partition-headline'
|
||||||
and represents the headline surrounding the entry."
|
and represents the headline surrounding the entry."
|
||||||
(let ((type (org-element-type entry)))
|
(let ((type (org-element-type entry)))
|
||||||
(cond
|
(cond
|
||||||
((eq type 'clock)
|
((eq type 'clock)
|
||||||
(nd/org-sql-extract-lb-clock entry acc hl-part))
|
(nd/org-sql-extract-lb-clock acc entry hl-part))
|
||||||
((eq type 'plain-list)
|
((eq type 'plain-list)
|
||||||
(let ((items (org-element-contents entry)))
|
(let ((items (org-element-contents entry)))
|
||||||
(nd/org-sql-extract-lb-items items acc hl-part)))
|
(nd/org-sql-extract-lb-items acc items hl-part)))
|
||||||
;; TODO add an "UNKNOWN" logbook parser
|
;; TODO add an "UNKNOWN" logbook parser
|
||||||
(t acc))))
|
(t acc))))
|
||||||
|
|
||||||
(defun nd/org-sql-extract-lb-two (entry1 entry2 acc hl-part)
|
(defun nd/org-sql-extract-lb-two (acc entry1 entry2 hl-part)
|
||||||
"Add data from logbook ENTRY1 and ENTRY2 to accumulator ACC.
|
"Add data from logbook ENTRY1 and ENTRY2 to accumulator ACC.
|
||||||
HL-PART is an object as returned by `nd/org-sql-partition-headline'
|
HL-PART is an object as returned by `nd/org-sql-partition-headline'
|
||||||
and represents the headline surrounding the entries. This assumes the
|
and represents the headline surrounding the entries. This assumes the
|
||||||
|
@ -3429,9 +3507,11 @@ entries are org-element types clock and plain-list respectively, and
|
||||||
will check if the first item in ENTRY2 is part of the clock."
|
will check if the first item in ENTRY2 is part of the clock."
|
||||||
(let* ((items (org-element-contents entry2))
|
(let* ((items (org-element-contents entry2))
|
||||||
(first-item (car items))
|
(first-item (car items))
|
||||||
(rem-items (cdr items))
|
(rem-items (cdr items)))
|
||||||
(acc* (nd/org-sql-extract-lb-clock entry1 acc hl-part first-item)))
|
(nd/org-sql->
|
||||||
(nd/org-sql-extract-lb-items rem-items acc* hl-part)))
|
acc
|
||||||
|
(nd/org-sql-extract-lb-clock entry1 hl-part first-item)
|
||||||
|
(nd/org-sql-extract-lb-items rem-items hl-part))))
|
||||||
|
|
||||||
(defun nd/org-sql-find-logbook (contents)
|
(defun nd/org-sql-find-logbook (contents)
|
||||||
"Find the logbook drawer given CONTENTS from section of org headline.
|
"Find the logbook drawer given CONTENTS from section of org headline.
|
||||||
|
@ -3445,50 +3525,45 @@ ignored."
|
||||||
(equal org-log-into-drawer (plist-get (car e) :drawer-name)))
|
(equal org-log-into-drawer (plist-get (car e) :drawer-name)))
|
||||||
contents)))
|
contents)))
|
||||||
|
|
||||||
(defun nd/org-sql-extract-lb (hl-part acc)
|
(defun nd/org-sql-extract-lb (acc hl-part)
|
||||||
"Add logbook data from HL-PART and add to accumulator ACC.
|
"Add logbook data from HL-PART and add to accumulator ACC.
|
||||||
HL-PART is an object as returned by `nd/org-sql-partition-headline'."
|
HL-PART is an object as returned by `nd/org-sql-partition-headline'."
|
||||||
(let* ((sec (alist-get :section hl-part))
|
(let* ((sec (alist-get :section hl-part))
|
||||||
(lb-contents (nd/org-sql-find-logbook sec))
|
(lb-contents (nd/org-sql-find-logbook sec)))
|
||||||
(scan
|
(while lb-contents
|
||||||
(lambda (contents &optional acc)
|
;; Need two of the next entries here because clocks may
|
||||||
(if (not contents)
|
;; have notes associated with them, but the only
|
||||||
acc
|
;; distinguishing characteristic they have is that they
|
||||||
;; Need two of the next entries here because clocks may
|
;; don't match anything in org-log-note-headings. If we
|
||||||
;; have notes associated with them, but the only
|
;; end up processing two entries at once, skip over two
|
||||||
;; distinguishing characteristic they have is that they
|
;; instead of one on the next iteration.
|
||||||
;; don't match anything in org-log-note-headings. If we
|
(let* ((cur1 (car lb-contents))
|
||||||
;; end up processing two entries at once, skip over two
|
(cur2 (cadr lb-contents))
|
||||||
;; instead of one on the next iteration.
|
(type1 (org-element-type cur1))
|
||||||
(let* ((cur1 (car contents))
|
(type2 (org-element-type cur2))
|
||||||
(cur2 (cadr contents))
|
(try-clock-note (and (eq 'clock type1)
|
||||||
(type1 (org-element-type cur1))
|
(eq type2 'plain-list))))
|
||||||
(type2 (org-element-type cur2))
|
(if try-clock-note
|
||||||
(try-clock-note (and (eq 'clock type1)
|
(setq acc (nd/org-sql-extract-lb-two acc cur1 cur2 hl-part)
|
||||||
(eq type2 'plain-list)))
|
lb-contents (cddr lb-contents))
|
||||||
(acc*
|
(setq acc (nd/org-sql-extract-lb-one acc cur1 hl-part)
|
||||||
(if try-clock-note
|
lb-contents (cdr lb-contents)))))
|
||||||
(nd/org-sql-extract-lb-two cur1 cur2 acc hl-part)
|
acc))
|
||||||
(nd/org-sql-extract-lb-one cur1 acc hl-part)))
|
|
||||||
(rem (if try-clock-note (cddr contents) (cdr contents))))
|
|
||||||
(funcall scan rem acc*))))))
|
|
||||||
(funcall scan lb-contents acc)))
|
|
||||||
|
|
||||||
(defun nd/org-sql-parse-ts-maybe (txt)
|
(defun nd/org-sql-parse-ts-maybe (txt)
|
||||||
"If TXT is a timestamp, return it in ISO 8601 format.
|
"If TXT is a timestamp, return it in ISO 8601 format.
|
||||||
Otherwise return it unchanged."
|
Otherwise return it unchanged."
|
||||||
;; assume the iso parser to return nil on failure
|
;; assume the iso parser to return nil on failure
|
||||||
(let ((txt* (nd/org-ts-format-to-iso txt)))
|
(nd/org-sql-> txt (nd/org-ts-format-to-iso) (or txt)))
|
||||||
(if txt* txt* txt)))
|
|
||||||
|
|
||||||
(defun nd/org-sql-extract-properties (hl-part acc)
|
(defun nd/org-sql-extract-properties (acc hl-part)
|
||||||
"Add properties data from HL-PART and add to accumulator ACC.
|
"Add properties data from HL-PART and add to accumulator ACC.
|
||||||
HL-PART is an object as returned by `nd/org-sql-partition-headline'."
|
HL-PART is an object as returned by `nd/org-sql-partition-headline'."
|
||||||
(let* ((sec (alist-get :section hl-part))
|
(let* ((sec (alist-get :section hl-part))
|
||||||
(prop-drawer (assoc 'property-drawer sec))
|
(prop-drawer (assoc 'property-drawer sec))
|
||||||
(node-props (org-element-contents prop-drawer))
|
(node-props (org-element-contents prop-drawer))
|
||||||
(into
|
(from
|
||||||
(lambda (np acc hl-part)
|
(lambda (acc np hl-part)
|
||||||
(let ((key (org-element-property :key np)))
|
(let ((key (org-element-property :key np)))
|
||||||
(if (member key nd/org-sql-ignored-properties)
|
(if (member key nd/org-sql-ignored-properties)
|
||||||
acc
|
acc
|
||||||
|
@ -3506,23 +3581,24 @@ HL-PART is an object as returned by `nd/org-sql-partition-headline'."
|
||||||
;; TODO add inherited flag
|
;; TODO add inherited flag
|
||||||
:inherited nil)))
|
:inherited nil)))
|
||||||
(nd/alist-put acc 'properties prop-data)))))))
|
(nd/alist-put acc 'properties prop-data)))))))
|
||||||
(nd/org-sql-extract node-props into acc hl-part)))
|
(nd/org-sql-extract acc from node-props hl-part)))
|
||||||
|
|
||||||
(defun nd/org-sql-extract-tags (hl-part acc)
|
(defun nd/org-sql-extract-tags (acc hl-part)
|
||||||
"Extract tags data from HL-PART and add to accumulator ACC.
|
"Extract tags data from HL-PART and add to accumulator ACC.
|
||||||
HL-PART is an object as returned by `nd/org-sql-partition-headline'."
|
HL-PART is an object as returned by `nd/org-sql-partition-headline'."
|
||||||
(let* ((hl (alist-get :headline hl-part))
|
(let* ((hl (alist-get :headline hl-part))
|
||||||
;; first retrieve tags and strip text props and whitespace
|
;; first retrieve tags and strip text props and whitespace
|
||||||
(tags (org-element-property :tags hl))
|
(tags (nd/org-sql->> hl
|
||||||
(tags (mapcar #'nd/strip-string tags))
|
(org-element-property :tags)
|
||||||
|
(mapcar #'nd/strip-string)))
|
||||||
;; then retrieve i-tags, optionally going up to parents
|
;; then retrieve i-tags, optionally going up to parents
|
||||||
(i-tags (org-element-property :ARCHIVE_ITAGS hl))
|
(i-tags (org-element-property :ARCHIVE_ITAGS hl))
|
||||||
(i-tags (when i-tags (split-string i-tags)))
|
(i-tags (when i-tags (split-string i-tags)))
|
||||||
(i-tags (if nd/org-sql-use-tag-inheritance
|
(i-tags (if nd/org-sql-use-tag-inheritance
|
||||||
(nd/org-element-get-parent-tags hl i-tags)
|
(nd/org-element-get-parent-tags hl i-tags)
|
||||||
i-tags))
|
i-tags))
|
||||||
(into
|
(from
|
||||||
(lambda (tag acc hl-part &optional inherited)
|
(lambda (acc tag hl-part &optional inherited)
|
||||||
(let* ((hl (alist-get :headline hl-part))
|
(let* ((hl (alist-get :headline hl-part))
|
||||||
(fp (alist-get :filepath hl-part))
|
(fp (alist-get :filepath hl-part))
|
||||||
(offset (org-element-property :begin hl))
|
(offset (org-element-property :begin hl))
|
||||||
|
@ -3531,28 +3607,20 @@ HL-PART is an object as returned by `nd/org-sql-partition-headline'."
|
||||||
:headline_offset offset
|
:headline_offset offset
|
||||||
:tag tag
|
:tag tag
|
||||||
:inherited i)))
|
:inherited i)))
|
||||||
(nd/alist-put acc 'tags tags-data))))
|
(nd/alist-put acc 'tags tags-data)))))
|
||||||
(acc* (nd/org-sql-extract tags into acc hl-part)))
|
(nd/org-sql->
|
||||||
(nd/org-sql-extract i-tags into acc* hl-part t)))
|
acc
|
||||||
|
(nd/org-sql-extract from tags hl-part)
|
||||||
|
(nd/org-sql-extract from i-tags hl-part t))))
|
||||||
|
|
||||||
(defun nd/org-sql-extract (objs fun acc &rest args)
|
|
||||||
"Iterate through OBJS and add them to accumulator ACC using FUN.
|
|
||||||
FUN is a function that takes a single object from OBJS, the accumulator,
|
|
||||||
and ARGS. FUN adds OBJ to ACC and returns a new ACC."
|
|
||||||
(if (not objs)
|
|
||||||
acc
|
|
||||||
(let* ((cur (car objs))
|
|
||||||
(rem (cdr objs))
|
|
||||||
(acc* (apply fun cur acc args)))
|
|
||||||
(apply #'nd/org-sql-extract rem fun acc* args))))
|
|
||||||
|
|
||||||
(defun nd/org-sql-extract-links (hl-part &optional acc)
|
(defun nd/org-sql-extract-links (acc hl-part)
|
||||||
"Add link data from headline HL-PART to accumulator ACC.
|
"Add link data from headline HL-PART to accumulator ACC.
|
||||||
HL-PART is an object as returned by `nd/org-sql-partition-headline'."
|
HL-PART is an object as returned by `nd/org-sql-partition-headline'."
|
||||||
(let* ((sec (alist-get :section hl-part))
|
(let* ((sec (alist-get :section hl-part))
|
||||||
(links (org-element-map sec 'link #'identity))
|
(links (org-element-map sec 'link #'identity))
|
||||||
(into
|
(from
|
||||||
(lambda (ln acc hl-part)
|
(lambda (acc ln hl-part)
|
||||||
(let* ((fp (alist-get :filepath hl-part))
|
(let* ((fp (alist-get :filepath hl-part))
|
||||||
(hl (alist-get :headline hl-part))
|
(hl (alist-get :headline hl-part))
|
||||||
(hl-offset (org-element-property :begin hl))
|
(hl-offset (org-element-property :begin hl))
|
||||||
|
@ -3569,9 +3637,9 @@ HL-PART is an object as returned by `nd/org-sql-partition-headline'."
|
||||||
:link_text ln-text
|
:link_text ln-text
|
||||||
:link_type ln-type)))
|
:link_type ln-type)))
|
||||||
(nd/alist-put acc 'links ln-data)))))
|
(nd/alist-put acc 'links ln-data)))))
|
||||||
(nd/org-sql-extract links into acc hl-part)))
|
(nd/org-sql-extract acc from links hl-part)))
|
||||||
|
|
||||||
(defun nd/org-sql-extract-headline (hl-part &optional acc)
|
(defun nd/org-sql-extract-hl-meta (acc hl-part)
|
||||||
"Add general data from headline HL-PART to accumulator ACC.
|
"Add general data from headline HL-PART to accumulator ACC.
|
||||||
HL-PART is an object as returned by `nd/org-sql-partition-headline'."
|
HL-PART is an object as returned by `nd/org-sql-partition-headline'."
|
||||||
(let* ((fp (alist-get :filepath hl-part))
|
(let* ((fp (alist-get :filepath hl-part))
|
||||||
|
@ -3608,50 +3676,168 @@ HL-PART is an object as returned by `nd/org-sql-partition-headline'."
|
||||||
:content nil)))
|
:content nil)))
|
||||||
(nd/alist-put acc 'headlines hl-data)))
|
(nd/alist-put acc 'headlines hl-data)))
|
||||||
|
|
||||||
(defun nd/org-sql-extract-headlines (headlines acc fp)
|
(defun nd/org-sql-extract-hl (acc headlines fp)
|
||||||
"Extract data from HEADLINES and add to accumulator ACC.
|
"Extract data from HEADLINES and add to accumulator ACC.
|
||||||
FP is the path to the file containing the headlines."
|
FP is the path to the file containing the headlines."
|
||||||
(let ((into
|
(let ((from
|
||||||
(lambda (hl acc fp)
|
(lambda (acc hl fp)
|
||||||
(let* ((hl-part (nd/org-sql-partition-headline hl fp))
|
(let* ((hl-part (nd/org-sql-partition-headline hl fp))
|
||||||
(hl-sub (alist-get :subheadlines hl-part))
|
(hl-sub (alist-get :subheadlines hl-part)))
|
||||||
(acc* (nd/org-sql-extract-headline hl-part acc))
|
(nd/org-sql-> acc
|
||||||
(acc* (nd/org-sql-extract-links hl-part acc*))
|
(nd/org-sql-extract-hl-meta hl-part)
|
||||||
(acc* (nd/org-sql-extract-tags hl-part acc*))
|
(nd/org-sql-extract-links hl-part)
|
||||||
(acc* (nd/org-sql-extract-properties hl-part acc*))
|
(nd/org-sql-extract-tags hl-part)
|
||||||
(acc* (nd/org-sql-extract-lb hl-part acc*)))
|
(nd/org-sql-extract-properties hl-part)
|
||||||
(nd/org-sql-extract-headlines hl-sub acc* fp)))))
|
(nd/org-sql-extract-lb hl-part)
|
||||||
(nd/org-sql-extract headlines into acc fp)))
|
(nd/org-sql-extract-hl hl-sub fp))))))
|
||||||
|
(nd/org-sql-extract acc from headlines fp)))
|
||||||
|
|
||||||
|
(defun nd/org-sql-extract-file (cell acc)
|
||||||
|
"Extract the file in the car of CELL for a sql insertion.
|
||||||
|
The results are accumulated in ACC which is returned on exit."
|
||||||
|
(let* ((fp (car cell))
|
||||||
|
(md5sum (cdr cell))
|
||||||
|
(buf (find-file-noselect fp t))
|
||||||
|
(tree (with-current-buffer buf
|
||||||
|
(org-element-parse-buffer)))
|
||||||
|
(attr (file-attributes fp))
|
||||||
|
(fsize (file-attribute-size attr))
|
||||||
|
(contents (org-element-contents tree))
|
||||||
|
(headlines (if (assoc 'section contents)
|
||||||
|
(cdr contents)
|
||||||
|
contents))
|
||||||
|
(file-data (list :file_path fp
|
||||||
|
:md5 md5sum
|
||||||
|
:size fsize)))
|
||||||
|
(nd/org-sql->
|
||||||
|
acc
|
||||||
|
(nd/alist-put 'files file-data)
|
||||||
|
(nd/org-sql-extract-hl headlines fp))))
|
||||||
|
|
||||||
|
(defun nd/org-sql-mk-insert (cell acc)
|
||||||
|
(nd/org-sql->> (plist-get acc 'insert)
|
||||||
|
(nd/org-sql-extract-file cell)
|
||||||
|
(plist-put acc 'insert)))
|
||||||
|
|
||||||
|
(defun nd/org-sql-mk-update (cell acc)
|
||||||
|
(let ((updt-acc (plist-get acc 'update)))
|
||||||
|
(nd/org-sql->> `((:file_path ,(car cell)) . (:md5 ,(cdr cell)))
|
||||||
|
(nd/alist-put updt-acc 'files)
|
||||||
|
(plist-put acc 'update))))
|
||||||
|
|
||||||
|
(defun nd/org-sql-mk-delete (cell acc)
|
||||||
|
(let ((dlt-acc (plist-get acc 'delete)))
|
||||||
|
(nd/org-sql->> `(:file_path ,(car cell))
|
||||||
|
(nd/alist-put dlt-acc 'files)
|
||||||
|
(plist-put acc 'delete))))
|
||||||
|
|
||||||
|
(defun nd/org-sql-get-updates (cell fp-qry acc)
|
||||||
|
"Returns cell where the car is accumulator ACC and cdr is current fp-qry."
|
||||||
|
;; if perfect match, do nothing
|
||||||
|
(if (find cell fp-qry :test #'equal)
|
||||||
|
(cons acc (remove cell fp-qry))
|
||||||
|
(let* ((match-cells
|
||||||
|
(lambda (a b fun)
|
||||||
|
(let ((car-a (car a))
|
||||||
|
(cdr-a (cdr a))
|
||||||
|
(car-b (car b))
|
||||||
|
(cdr-b (cdr b)))
|
||||||
|
(funcall fun car-a car-b cdr-a cdr-b))))
|
||||||
|
(match-fp
|
||||||
|
(lambda (fp-a fp-b md5-a md5-b)
|
||||||
|
(and (equal fp-a fp-b) (not (equal md5-a md5-b)))))
|
||||||
|
(match-md5
|
||||||
|
(lambda (fp-a fp-b md5-a md5-b)
|
||||||
|
(and (not (equal fp-a fp-b)) (equal md5-a md5-b))))
|
||||||
|
(match-fp*
|
||||||
|
(lambda (b)
|
||||||
|
(funcall match-cells cell b match-fp)))
|
||||||
|
(match-md5*
|
||||||
|
(lambda (b)
|
||||||
|
(funcall match-cells cell b match-md5)))
|
||||||
|
(found-fp (find-if (lambda (q) (funcall match-fp* q)) fp-qry)))
|
||||||
|
(cond
|
||||||
|
;; delete qry in db and insert cell
|
||||||
|
(found-fp
|
||||||
|
(cons (nd/org-sql-mk-insert cell (nd/org-sql-mk-delete found-fp acc))
|
||||||
|
(remove found-fp fp-qry)))
|
||||||
|
;; update fp in db
|
||||||
|
((find-if (lambda (q) (funcall match-md5* q)) fp-qry)
|
||||||
|
(cons (nd/org-sql-mk-update cell acc)
|
||||||
|
(remove-if (lambda (q) (funcall match-md5* q)) fp-qry)))
|
||||||
|
;; insert cell
|
||||||
|
(t
|
||||||
|
(cons (nd/org-sql-mk-insert cell acc) fp-qry))))))
|
||||||
|
|
||||||
|
(defun nd/org-sql-compare-files (fp-dsk fp-qry)
|
||||||
|
(let (acc)
|
||||||
|
(while fp-dsk
|
||||||
|
(let* ((cur (car fp-dsk))
|
||||||
|
(rem (cdr fp-dsk))
|
||||||
|
(found (nd/org-sql-get-updates cur fp-qry acc)))
|
||||||
|
(setq fp-dsk rem
|
||||||
|
acc (car found)
|
||||||
|
fp-qry (cdr found))))
|
||||||
|
(if fp-qry (nd/org-sql-mk-delete fp-qry acc)) acc))
|
||||||
|
|
||||||
|
(defun nd/org-sql-files-from-disk ()
|
||||||
|
"Return alist of metadata for filepaths PATHS."
|
||||||
|
(let ((paths (mapcar (lambda (p) (expand-file-name p org-directory)) nd/org-sql-files))
|
||||||
|
(cons-md5
|
||||||
|
(lambda (fp)
|
||||||
|
(let* ((fp-buf (find-file-noselect fp t)))
|
||||||
|
(cons fp (md5 fp-buf))))))
|
||||||
|
(mapcar (lambda (p) (funcall cons-md5 p)) paths)))
|
||||||
|
|
||||||
|
(defun nd/org-sql-files-from-db ()
|
||||||
|
"Get all files and their metadata from the database.
|
||||||
|
Returns an alist where the each car is file_path and each cdr is
|
||||||
|
the plist of metadata."
|
||||||
|
;; TODO should probably make the table recreate itself if it is
|
||||||
|
;; corrupted or missing
|
||||||
|
(when (file-exists-p nd/org-sqlite-db-path)
|
||||||
|
(nd/org-sql->> '(:file_path :md5)
|
||||||
|
(nd/sql-select nd/org-sqlite-db-path 'files)
|
||||||
|
(mapcar #'nd/sql-plist-get-vals)
|
||||||
|
(mapcar (lambda (q) (cons (car q) (car (cdr q))))))))
|
||||||
|
|
||||||
|
(defun nd/org-sql-get-transactions ()
|
||||||
|
(let ((fp-dsk (nd/org-sql-files-from-disk))
|
||||||
|
(map-trns
|
||||||
|
(lambda (op fun trans)
|
||||||
|
(nd/org-sql->>
|
||||||
|
(plist-get trans op)
|
||||||
|
(mapcar (lambda (s) (funcall fun s)))
|
||||||
|
(nd/org-sql-fmt-trans)
|
||||||
|
(plist-put trans op)))))
|
||||||
|
(nd/org-sql->>
|
||||||
|
(nd/org-sql-files-from-db)
|
||||||
|
(nd/org-sql-compare-files fp-dsk)
|
||||||
|
(funcall map-trns 'insert #'nd/org-sql-fmt-inserts)
|
||||||
|
(funcall map-trns 'update #'nd/org-sql-fmt-updates)
|
||||||
|
(funcall map-trns 'delete #'nd/org-sql-fmt-deletes))))
|
||||||
|
|
||||||
(defvar nd/org-sql-files '("test1.org_archive" "test2.org_archive")
|
(defvar nd/org-sql-files '("test1.org_archive" "test2.org_archive")
|
||||||
"A list of org files to put into sql database.")
|
"A list of org files to put into sql database.")
|
||||||
|
|
||||||
(defun nd/org-sql-extract-files ()
|
(defun nd/org-init-db ()
|
||||||
"Return a plist of data to be inserted into sql database."
|
"Make a sqlite database for org files if it does not exist already."
|
||||||
(let ((paths (mapcar (lambda (p) (expand-file-name p org-directory)) nd/org-sql-files))
|
(unless (file-exists-p nd/org-sqlite-db-path)
|
||||||
(into
|
(process-file-shell-command (concat "touch " nd/org-sqlite-db-path))
|
||||||
(lambda (fp acc)
|
(mapcar (lambda (s) (nd/sql-cmd nd/org-sqlite-db-path s)) nd/org-sql-schemas)))
|
||||||
(let* ((buf (find-file-noselect fp t))
|
|
||||||
(tree (with-current-buffer buf
|
|
||||||
(org-element-parse-buffer)))
|
|
||||||
(md5sum (md5 buf))
|
|
||||||
(attr (file-attributes fp))
|
|
||||||
(fsize (file-attribute-size attr))
|
|
||||||
(contents (org-element-contents tree))
|
|
||||||
(headlines (if (assoc 'section contents)
|
|
||||||
(cdr contents)
|
|
||||||
contents))
|
|
||||||
(file-data (list :file_path fp
|
|
||||||
:md5 md5sum
|
|
||||||
:size fsize))
|
|
||||||
(acc* (nd/alist-put acc 'files file-data)))
|
|
||||||
(nd/org-sql-extract-headlines headlines acc* fp)))))
|
|
||||||
(nd/org-sql-extract paths into nil)))
|
|
||||||
|
|
||||||
(defun nd/org-archive-to-db ()
|
(defun nd/org-archive-to-db (&optional show-err)
|
||||||
"Transfer archive files to sqlite database."
|
"Transfer archive files to sqlite database."
|
||||||
(let ((sql-data (nd/org-sql-extract-files)))
|
(let* ((trans (nd/org-sql-get-transactions))
|
||||||
(nd/sql-insert-multi nd/org-sqlite-db-path sql-data)))
|
(trans-dlt (plist-get trans 'delete))
|
||||||
|
(trans-upd (plist-get trans 'update))
|
||||||
|
(trans-ins (plist-get trans 'insert)))
|
||||||
|
;; note, the order of sql commands matters in transactions,
|
||||||
|
;; so, we need to do deletes, update, then inserts in that order
|
||||||
|
;; `(,(nd/sql-cmd nd/org-sqlite-db-path trans-dlt show-err))))
|
||||||
|
`(,(nd/sql-cmd nd/org-sqlite-db-path trans-dlt show-err t)
|
||||||
|
,(nd/sql-cmd nd/org-sqlite-db-path trans-upd show-err t)
|
||||||
|
,(nd/sql-cmd nd/org-sqlite-db-path trans-ins show-err nil))))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
* tools
|
* tools
|
||||||
** printing
|
** printing
|
||||||
|
|
Loading…
Reference in New Issue