Fix "Too many open files" error

This commit is contained in:
contrapunctus 2022-03-25 11:31:28 +05:30
parent fc47683184
commit ab1dc18795
2 changed files with 171 additions and 144 deletions

View File

@ -27,79 +27,92 @@
(defclass chronometrist-sqlite-backend (chronometrist-backend chronometrist-file-backend-mixin)
((extension :initform "sqlite"
:accessor chronometrist-backend-ext
:custom 'string)))
:custom 'string)
(connection :initform nil
:initarg :connection
:accessor chronometrist-backend-connection)))
(chronometrist-register-backend
:sqlite "Store records in SQLite database."
(make-instance 'chronometrist-sqlite-backend :path chronometrist-file))
(cl-defmethod initialize-instance :after ((backend chronometrist-sqlite-backend)
&rest _initargs)
"Initialize connection for BACKEND based on its file."
(with-slots (file connection) backend
(when (and file (not connection))
(setf connection (emacsql-sqlite file)))))
(cl-defmethod chronometrist-create-file ((backend chronometrist-sqlite-backend) &optional file)
"Create file for BACKEND if it does not already exist.
Return the emacsql-sqlite connection object."
(let ((file (or file (chronometrist-backend-file backend))))
(if (file-exists-p file)
nil
(cl-loop with db = (emacsql-sqlite file)
for query in
'(;; Properties are user-defined key-values stored as JSON.
[:create-table properties
([(prop-id integer :primary-key)
(properties text :unique :not-null)])]
;; An event is a timestamp with a name and optional properties.
[:create-table event-names
([(name-id integer :primary-key)
(name text :unique :not-null)])]
[:create-table events
([(event-id integer :primary-key)
(name-id integer :not-null :references event-names [name-id])])]
;; An interval is a time range with a name and optional properties.
[:create-table interval-names
([(name-id integer :primary-key)
(name text :unique :not-null)])]
[:create-table intervals
([(interval-id integer :primary-key)
(name-id integer :not-null :references interval-names [name-id])
(start-time integer :not-null)
;; The latest interval may be ongoing, so the stop time may be NULL.
(stop-time integer)
(prop-id integer :references properties [prop-id])]
(:unique [name-id start-time stop-time]))]
;; A date contains one or more events and intervals. It may
;; also contain properties.
[:create-table dates
([(date-id integer :primary-key)
(date integer :unique :not-null)
(prop-id integer :references properties [prop-id])])]
[:create-table date-events
([(date-id integer :not-null :references dates [date-id])
(event-id integer :not-null :references events [event-id])])]
[:create-table date-intervals
([(date-id integer :not-null :references dates [date-id])
(interval-id integer :not-null :references intervals [interval-id])])])
do (emacsql db query)
finally return db))))
Return the connection object from `emacsql-sqlite'."
(let* ((file (or file (chronometrist-backend-file backend)))
(db (or (chronometrist-backend-connection backend)
(setf (chronometrist-backend-connection backend)
(emacsql-sqlite file)))))
(cl-loop
for query in
'(;; Properties are user-defined key-values stored as JSON.
[:create-table properties
([(prop-id integer :primary-key)
(properties text :unique :not-null)])]
;; An event is a timestamp with a name and optional properties.
[:create-table event-names
([(name-id integer :primary-key)
(name text :unique :not-null)])]
[:create-table events
([(event-id integer :primary-key)
(name-id integer :not-null :references event-names [name-id])])]
;; An interval is a time range with a name and optional properties.
[:create-table interval-names
([(name-id integer :primary-key)
(name text :unique :not-null)])]
[:create-table intervals
([(interval-id integer :primary-key)
(name-id integer :not-null :references interval-names [name-id])
(start-time integer :not-null)
;; The latest interval may be ongoing, so the stop time may be NULL.
(stop-time integer)
(prop-id integer :references properties [prop-id])]
(:unique [name-id start-time stop-time]))]
;; A date contains one or more events and intervals. It may
;; also contain properties.
[:create-table dates
([(date-id integer :primary-key)
(date integer :unique :not-null)
(prop-id integer :references properties [prop-id])])]
[:create-table date-events
([(date-id integer :not-null :references dates [date-id])
(event-id integer :not-null :references events [event-id])])]
[:create-table date-intervals
([(date-id integer :not-null :references dates [date-id])
(interval-id integer :not-null :references intervals [interval-id])])])
do (emacsql db query)
finally return db)))
(defun chronometrist-iso-to-unix (timestamp)
(truncate (float-time (parse-iso8601-time-string timestamp))))
(cl-defmethod chronometrist-to-file (hash-table (backend chronometrist-sqlite-backend) file)
(delete-file file)
(chronometrist-create-file backend file)
(cl-loop with db = (emacsql-sqlite file)
for date in (sort (hash-table-keys hash-table) #'string-lessp) do
;; insert date if it does not exist
(-let* ((date-unix (chronometrist-iso-to-unix date))
((date-results &as (date-id))
(emacsql db [:select [date-id] :from dates :where (= date $s1)]
date-unix)))
(unless date-results
(emacsql db [:insert-into dates [date] :values [$s1]] date-unix))
;; XXX - insert date properties
(cl-loop for plist in (gethash date hash-table) do
(chronometrist-insert backend plist)
;; XXX - insert events
))
))
(with-slots (connection) backend
(delete-file file)
(emacsql-close connection)
(setf connection nil)
(chronometrist-create-file backend file)
(cl-loop for date in (sort (hash-table-keys hash-table) #'string-lessp) do
;; insert date if it does not exist
(-let* ((date-unix (chronometrist-iso-to-unix date))
((date-results &as (date-id))
(emacsql connection [:select [date-id] :from dates :where (= date $s1)]
date-unix)))
(unless date-results
(emacsql connection [:insert-into dates [date] :values [$s1]] date-unix))
;; XXX - insert date properties
(cl-loop for plist in (gethash date hash-table) do
(chronometrist-insert backend plist)
;; XXX - insert events
))
)))
;; predicate to find prop-id for property if it exists
;; insert property if it does not exist (procedure)
@ -113,9 +126,8 @@ Properties are key-values excluding :name, :start, and :stop.
Insert nothing if the properties already exist. Return the
prop-id of the inserted or existing property."
(with-slots (file) backend
(-let* ((db (emacsql-sqlite file))
(props-json (json-encode
(with-slots (file connection) backend
(-let* ((props-json (json-encode
;; `json-encode' throws an error for alists,
;; so we convert any cons cells to lists
(-tree-map (lambda (elt)
@ -123,17 +135,17 @@ prop-id of the inserted or existing property."
(list (car elt) (cdr elt))
elt))
(chronometrist-plist-key-values plist)))))
(emacsql db [:insert-or-ignore-into properties [properties]
:values [$s1]]
(emacsql connection
[:insert-or-ignore-into properties [properties] :values [$s1]]
props-json)
(caar (emacsql db [:select (funcall max prop-id) :from properties])))))
(caar (emacsql connection [:select (funcall max prop-id) :from properties])))))
(cl-defmethod chronometrist-insert ((backend chronometrist-sqlite-backend) plist)
(-let [(plist-1 plist-2) (chronometrist-split-plist plist)]
(cl-loop with db = (emacsql-sqlite (chronometrist-backend-file backend))
for plist in (if (and plist-1 plist-2)
(list plist-1 plist-2)
(list plist))
(-let (((plist-1 plist-2) (chronometrist-split-plist plist))
(db (chronometrist-backend-connection backend)))
(cl-loop for plist in (if (and plist-1 plist-2)
(list plist-1 plist-2)
(list plist))
do
(-let* (((&plist :name name :start start :stop stop) plist)
(date-unix (chronometrist-iso-to-unix (chronometrist-iso-to-date start)))

View File

@ -45,60 +45,74 @@
(defclass chronometrist-sqlite-backend (chronometrist-backend chronometrist-file-backend-mixin)
((extension :initform "sqlite"
:accessor chronometrist-backend-ext
:custom 'string)))
:custom 'string)
(connection :initform nil
:initarg :connection
:accessor chronometrist-backend-connection)))
(chronometrist-register-backend
:sqlite "Store records in SQLite database."
(make-instance 'chronometrist-sqlite-backend :path chronometrist-file))
#+END_SRC
** initialize-instance :method:
#+BEGIN_SRC emacs-lisp
(cl-defmethod initialize-instance :after ((backend chronometrist-sqlite-backend)
&rest _initargs)
"Initialize connection for BACKEND based on its file."
(with-slots (file connection) backend
(when (and file (not connection))
(setf connection (emacsql-sqlite file)))))
#+END_SRC
** create-file
#+BEGIN_SRC emacs-lisp
(cl-defmethod chronometrist-create-file ((backend chronometrist-sqlite-backend) &optional file)
"Create file for BACKEND if it does not already exist.
Return the emacsql-sqlite connection object."
(let ((file (or file (chronometrist-backend-file backend))))
(if (file-exists-p file)
nil
(cl-loop with db = (emacsql-sqlite file)
for query in
'(;; Properties are user-defined key-values stored as JSON.
[:create-table properties
([(prop-id integer :primary-key)
(properties text :unique :not-null)])]
;; An event is a timestamp with a name and optional properties.
[:create-table event-names
([(name-id integer :primary-key)
(name text :unique :not-null)])]
[:create-table events
([(event-id integer :primary-key)
(name-id integer :not-null :references event-names [name-id])])]
;; An interval is a time range with a name and optional properties.
[:create-table interval-names
([(name-id integer :primary-key)
(name text :unique :not-null)])]
[:create-table intervals
([(interval-id integer :primary-key)
(name-id integer :not-null :references interval-names [name-id])
(start-time integer :not-null)
;; The latest interval may be ongoing, so the stop time may be NULL.
(stop-time integer)
(prop-id integer :references properties [prop-id])]
(:unique [name-id start-time stop-time]))]
;; A date contains one or more events and intervals. It may
;; also contain properties.
[:create-table dates
([(date-id integer :primary-key)
(date integer :unique :not-null)
(prop-id integer :references properties [prop-id])])]
[:create-table date-events
([(date-id integer :not-null :references dates [date-id])
(event-id integer :not-null :references events [event-id])])]
[:create-table date-intervals
([(date-id integer :not-null :references dates [date-id])
(interval-id integer :not-null :references intervals [interval-id])])])
do (emacsql db query)
finally return db))))
Return the connection object from `emacsql-sqlite'."
(let* ((file (or file (chronometrist-backend-file backend)))
(db (or (chronometrist-backend-connection backend)
(setf (chronometrist-backend-connection backend)
(emacsql-sqlite file)))))
(cl-loop
for query in
'(;; Properties are user-defined key-values stored as JSON.
[:create-table properties
([(prop-id integer :primary-key)
(properties text :unique :not-null)])]
;; An event is a timestamp with a name and optional properties.
[:create-table event-names
([(name-id integer :primary-key)
(name text :unique :not-null)])]
[:create-table events
([(event-id integer :primary-key)
(name-id integer :not-null :references event-names [name-id])])]
;; An interval is a time range with a name and optional properties.
[:create-table interval-names
([(name-id integer :primary-key)
(name text :unique :not-null)])]
[:create-table intervals
([(interval-id integer :primary-key)
(name-id integer :not-null :references interval-names [name-id])
(start-time integer :not-null)
;; The latest interval may be ongoing, so the stop time may be NULL.
(stop-time integer)
(prop-id integer :references properties [prop-id])]
(:unique [name-id start-time stop-time]))]
;; A date contains one or more events and intervals. It may
;; also contain properties.
[:create-table dates
([(date-id integer :primary-key)
(date integer :unique :not-null)
(prop-id integer :references properties [prop-id])])]
[:create-table date-events
([(date-id integer :not-null :references dates [date-id])
(event-id integer :not-null :references events [event-id])])]
[:create-table date-intervals
([(date-id integer :not-null :references dates [date-id])
(interval-id integer :not-null :references intervals [interval-id])])])
do (emacsql db query)
finally return db)))
#+END_SRC
** to-file :method:
@ -121,23 +135,25 @@ Return the emacsql-sqlite connection object."
(truncate (float-time (parse-iso8601-time-string timestamp))))
(cl-defmethod chronometrist-to-file (hash-table (backend chronometrist-sqlite-backend) file)
(delete-file file)
(chronometrist-create-file backend file)
(cl-loop with db = (emacsql-sqlite file)
for date in (sort (hash-table-keys hash-table) #'string-lessp) do
;; insert date if it does not exist
(-let* ((date-unix (chronometrist-iso-to-unix date))
((date-results &as (date-id))
(emacsql db [:select [date-id] :from dates :where (= date $s1)]
date-unix)))
(unless date-results
(emacsql db [:insert-into dates [date] :values [$s1]] date-unix))
;; XXX - insert date properties
(cl-loop for plist in (gethash date hash-table) do
(chronometrist-insert backend plist)
;; XXX - insert events
))
))
(with-slots (connection) backend
(delete-file file)
(emacsql-close connection)
(setf connection nil)
(chronometrist-create-file backend file)
(cl-loop for date in (sort (hash-table-keys hash-table) #'string-lessp) do
;; insert date if it does not exist
(-let* ((date-unix (chronometrist-iso-to-unix date))
((date-results &as (date-id))
(emacsql connection [:select [date-id] :from dates :where (= date $s1)]
date-unix)))
(unless date-results
(emacsql connection [:insert-into dates [date] :values [$s1]] date-unix))
;; XXX - insert date properties
(cl-loop for plist in (gethash date hash-table) do
(chronometrist-insert backend plist)
;; XXX - insert events
))
)))
#+END_SRC
** insert
@ -154,9 +170,8 @@ Properties are key-values excluding :name, :start, and :stop.
Insert nothing if the properties already exist. Return the
prop-id of the inserted or existing property."
(with-slots (file) backend
(-let* ((db (emacsql-sqlite file))
(props-json (json-encode
(with-slots (file connection) backend
(-let* ((props-json (json-encode
;; `json-encode' throws an error for alists,
;; so we convert any cons cells to lists
(-tree-map (lambda (elt)
@ -164,17 +179,17 @@ prop-id of the inserted or existing property."
(list (car elt) (cdr elt))
elt))
(chronometrist-plist-key-values plist)))))
(emacsql db [:insert-or-ignore-into properties [properties]
:values [$s1]]
(emacsql connection
[:insert-or-ignore-into properties [properties] :values [$s1]]
props-json)
(caar (emacsql db [:select (funcall max prop-id) :from properties])))))
(caar (emacsql connection [:select (funcall max prop-id) :from properties])))))
(cl-defmethod chronometrist-insert ((backend chronometrist-sqlite-backend) plist)
(-let [(plist-1 plist-2) (chronometrist-split-plist plist)]
(cl-loop with db = (emacsql-sqlite (chronometrist-backend-file backend))
for plist in (if (and plist-1 plist-2)
(list plist-1 plist-2)
(list plist))
(-let (((plist-1 plist-2) (chronometrist-split-plist plist))
(db (chronometrist-backend-connection backend)))
(cl-loop for plist in (if (and plist-1 plist-2)
(list plist-1 plist-2)
(list plist))
do
(-let* (((&plist :name name :start start :stop stop) plist)
(date-unix (chronometrist-iso-to-unix (chronometrist-iso-to-date start)))