summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThanos Apollo <public@thanosapollo.org>2026-04-26 17:33:10 +0300
committerThanos Apollo <public@thanosapollo.org>2026-04-26 17:33:10 +0300
commit61ff98cafbdc4d8d9285045a37e2fa3285799e97 (patch)
tree5bb4047ffe607c3f3eb7bd5ea47cb9bcc1d0e14a
parentd0bc5cefb807fcdcc03027ac4fbfe68e7ff677b7 (diff)
db: table driven-driven filter builder for get-issues
-rw-r--r--lisp/forgejo-db.el169
1 files changed, 95 insertions, 74 deletions
diff --git a/lisp/forgejo-db.el b/lisp/forgejo-db.el
index 8b798d8..aa25c68 100644
--- a/lisp/forgejo-db.el
+++ b/lisp/forgejo-db.el
@@ -103,7 +103,7 @@
endpoint TEXT NOT NULL,
last_synced TEXT,
PRIMARY KEY (host, owner, repo, endpoint))"
-)
+ )
"SQL statements to initialize the database schema.")
;;; Connection management
@@ -132,7 +132,7 @@ Detects stale schema and rebuilds the database if necessary."
(condition-case nil
(let ((cols (mapcar #'cadr
(sqlite-select forgejo-db
- "PRAGMA table_info(issues)"))))
+ "PRAGMA table_info(issues)"))))
(and cols (member "body_html" cols)))
(error nil)))
@@ -209,20 +209,20 @@ When IS-PULL is non-nil, mark all entries as pull requests regardless of
whether a `pull_request' field is present in the data."
(forgejo-db--with-transaction
(let ((db (forgejo-db--ensure)))
- (dolist (issue issues)
- (let-alist issue
- (let ((body (forgejo-db--nullable .body))
- (labels (forgejo-db--nullable .labels))
- (milestone (forgejo-db--nullable .milestone))
- (assignees (forgejo-db--nullable .assignees))
- (closed (forgejo-db--nullable .closed_at))
- (pr (or is-pull (forgejo-db--nullable .pull_request))))
- ;; Track edits before overwriting
- (when body
- (forgejo-db--track-edit db host owner repo .number body))
- (sqlite-execute
- db
- "INSERT INTO issues
+ (dolist (issue issues)
+ (let-alist issue
+ (let ((body (forgejo-db--nullable .body))
+ (labels (forgejo-db--nullable .labels))
+ (milestone (forgejo-db--nullable .milestone))
+ (assignees (forgejo-db--nullable .assignees))
+ (closed (forgejo-db--nullable .closed_at))
+ (pr (or is-pull (forgejo-db--nullable .pull_request))))
+ ;; Track edits before overwriting
+ (when body
+ (forgejo-db--track-edit db host owner repo .number body))
+ (sqlite-execute
+ db
+ "INSERT INTO issues
(id, host, owner, repo, number, title, state, body,
user, labels, milestone, assignees, comments_count,
created_at, updated_at, closed_at, is_pull, read)
@@ -233,61 +233,82 @@ whether a `pull_request' field is present in the data."
assignees=excluded.assignees, comments_count=excluded.comments_count,
updated_at=excluded.updated_at, closed_at=excluded.closed_at,
is_pull=excluded.is_pull, read=0"
- (list .id host owner repo .number .title .state body
- (alist-get 'login .user)
- (forgejo-db--encode-json (when (listp labels) labels))
- (when (listp milestone) (alist-get 'title milestone))
- (forgejo-db--encode-json
- (when (listp assignees)
- (mapcar (lambda (a) (alist-get 'login a)) assignees)))
- .comments
- .created_at .updated_at closed
- (if pr 1 0)))))))))
+ (list .id host owner repo .number .title .state body
+ (alist-get 'login .user)
+ (forgejo-db--encode-json (when (listp labels) labels))
+ (when (listp milestone) (alist-get 'title milestone))
+ (forgejo-db--encode-json
+ (when (listp assignees)
+ (mapcar (lambda (a) (alist-get 'login a)) assignees)))
+ .comments
+ .created_at .updated_at closed
+ (if pr 1 0)))))))))
+
+(defun forgejo-db--like (s)
+ "Wrap S in SQL LIKE wildcards."
+ (format "%%%s%%" s))
+
+(defun forgejo-db--read-val (v)
+ "Convert read filter V to integer."
+ (if (equal v "yes") 1 0))
+
+(defun forgejo-db--type-clause (v)
+ "Return bare SQL clause for type filter V."
+ (if (equal v "pr") "is_pull = 1" "is_pull = 0"))
+
+(defconst forgejo-db--filter-specs
+ '((:state "state = ?" identity)
+ (:label "labels LIKE ?" forgejo-db--like)
+ (:labels "labels LIKE ?" forgejo-db--like)
+ (:milestone "milestone = ?" identity)
+ (:author "user LIKE ?" forgejo-db--like)
+ (:query "title LIKE ?" forgejo-db--like)
+ (:read "read = ?" forgejo-db--read-val)
+ (:type nil forgejo-db--type-clause)
+ (:is-pull "is_pull = 1" nil)
+ (:no-pulls "is_pull = 0" nil))
+ "Filter specs: (KEY SQL-CLAUSE VALUE-TRANSFORM).
+When SQL-CLAUSE is nil, TRANSFORM returns the clause itself (bare SQL).
+When TRANSFORM is nil, the clause is bare SQL with no bound argument.")
+
+(defun forgejo-db--build-filter-clauses (filters)
+ "Build (CLAUSES . ARGS) from FILTERS plist.
+CLAUSES is a list of SQL WHERE fragments, ARGS is a list of
+bound parameter values."
+ (cl-loop for (key clause transform) in forgejo-db--filter-specs
+ for val = (plist-get filters key)
+ when val
+ if (null transform)
+ collect clause into clauses
+ else if (null clause)
+ collect (funcall transform val) into clauses
+ else
+ collect clause into clauses
+ and collect (funcall transform val) into args
+ finally return (cons clauses args)))
(defun forgejo-db-get-issues (host owner repo &optional filters)
"Query cached issues for HOST/OWNER/REPO with optional FILTERS.
FILTERS is a plist with keys:
- :state - \"open\", \"closed\", or nil for all
- :label - label name substring to match
- :milestone - milestone title to match
- :author - author/poster login to match
- :query - search string for title
- :read - \"yes\" or \"no\" for read/unread status
- :is-pull - when non-nil, filter to pull requests only
- :no-pulls - when non-nil, exclude pull requests"
- (let ((where (list "host = ?" "owner = ?" "repo = ?"))
- (args (list host owner repo)))
- (when-let* ((state (plist-get filters :state)))
- (setq where (append where (list "state = ?"))
- args (append args (list state))))
- (when-let* ((label (or (plist-get filters :labels)
- (plist-get filters :label))))
- (setq where (append where (list "labels LIKE ?"))
- args (append args (list (format "%%%s%%" label)))))
- (when-let* ((milestone (plist-get filters :milestone)))
- (setq where (append where (list "milestone = ?"))
- args (append args (list milestone))))
- (when-let* ((author (plist-get filters :author)))
- (setq where (append where (list "user LIKE ?"))
- args (append args (list (format "%%%s%%" author)))))
- (when-let* ((query (plist-get filters :query)))
- (setq where (append where (list "title LIKE ?"))
- args (append args (list (format "%%%s%%" query)))))
- (when-let* ((read-val (plist-get filters :read)))
- (setq where (append where (list "read = ?"))
- args (append args (list (if (equal read-val "yes") 1 0)))))
- (when-let* ((type (plist-get filters :type)))
- (setq where (append where (list (if (equal type "pr")
- "is_pull = 1"
- "is_pull = 0")))))
- (when (plist-get filters :is-pull)
- (setq where (append where (list "is_pull = 1"))))
- (when (plist-get filters :no-pulls)
- (setq where (append where (list "is_pull = 0"))))
+ :state \"open\", \"closed\", or nil for all
+ :label label name substring to match
+ :labels alias for :label
+ :milestone milestone title to match
+ :author author/poster login to match
+ :query search string for title
+ :read \"yes\" or \"no\" for read/unread status
+ :type \"pr\" or \"issue\"
+ :is-pull when non-nil, filter to pull requests only
+ :no-pulls when non-nil, exclude pull requests"
+ (let* ((filter-result (forgejo-db--build-filter-clauses filters))
+ (clauses (append (list "host = ?" "owner = ?" "repo = ?")
+ (car filter-result)))
+ (args (append (list host owner repo)
+ (cdr filter-result))))
(forgejo-db--select
(format "SELECT %s FROM issues WHERE %s ORDER BY updated_at DESC"
forgejo-db--issue-columns
- (mapconcat #'identity where " AND "))
+ (mapconcat #'identity clauses " AND "))
args)))
;;; Timeline events
@@ -298,10 +319,10 @@ FILTERS is a plist with keys:
(forgejo-db--with-transaction
(let ((db (forgejo-db--ensure)))
(dolist (event events)
- (let-alist event
- (sqlite-execute
- db
- "INSERT INTO timeline_events
+ (let-alist event
+ (sqlite-execute
+ db
+ "INSERT INTO timeline_events
(id, host, owner, repo, issue_number, type, body,
user, created_at, data)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
@@ -309,12 +330,12 @@ FILTERS is a plist with keys:
type=excluded.type, body=excluded.body,
user=excluded.user, created_at=excluded.created_at,
data=excluded.data"
- (list .id host owner repo number
- (forgejo-db--nullable .type)
- (forgejo-db--nullable .body)
- (alist-get 'login (forgejo-db--nullable .user))
- (forgejo-db--nullable .created_at)
- (forgejo-db--encode-json event)))))))))
+ (list .id host owner repo number
+ (forgejo-db--nullable .type)
+ (forgejo-db--nullable .body)
+ (alist-get 'login (forgejo-db--nullable .user))
+ (forgejo-db--nullable .created_at)
+ (forgejo-db--encode-json event)))))))))
(defun forgejo-db-get-timeline (host owner repo number)
"Get cached timeline events for issue NUMBER in HOST/OWNER/REPO."