-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathsupertag-virtual-column.el
More file actions
365 lines (322 loc) · 15.3 KB
/
Copy pathsupertag-virtual-column.el
File metadata and controls
365 lines (322 loc) · 15.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
;;; supertag-virtual-column.el --- Virtual column system -*- lexical-binding: t; -*-
;; Copyright (C) 2026
;; Author: Supertag Team
;; Keywords: org-mode, tags, fields
;; Version: 1.0
;;; Commentary:
;; Virtual column system for Supertag.
;; Provides computed fields: rollup, formula, aggregate, reference.
;;; Code:
(require 'cl-lib)
(require 'subr-x)
(require 'supertag-core-store)
(require 'supertag-core-schema)
(require 'supertag-services-formula)
(defgroup supertag-virtual-column nil
"Virtual column settings."
:group 'supertag)
(defcustom supertag-virtual-column-cache-size 1000
"Maximum cached values per node."
:type 'integer
:group 'supertag-virtual-column)
(defcustom supertag-virtual-column-compute-timeout 5
"Max seconds for computation."
:type 'number
:group 'supertag-virtual-column)
(cl-defstruct (supertag-virtual-column-cache
(:constructor supertag-virtual-column-cache--make)
(:copier nil))
value computed-at dependencies dirty-flag)
(defvar supertag--virtual-column-definitions (make-hash-table :test 'equal))
(defvar supertag--virtual-column-cache (make-hash-table :test 'equal))
(defvar supertag--virtual-column-dependency-graph (make-hash-table :test 'equal))
(defvar supertag--virtual-column-compute-stack nil)
(defvar supertag--virtual-column-current-dependencies nil)
(defconst supertag-virtual-column-types
'(:rollup :formula :aggregate :reference))
(defconst supertag-virtual-column-rollup-functions
'(:sum :count :avg :max :min :first :last))
(defun supertag-virtual-column-create (props)
"Create virtual column from PROPS plist with :id :name :type :params."
(let* ((id (or (plist-get props :id)
(error "Virtual column must have :id")))
(name (or (plist-get props :name) id))
(type (plist-get props :type))
(params (plist-get props :params)))
(unless (memq type supertag-virtual-column-types)
(error "Invalid type: %s" type))
(when (gethash id supertag--virtual-column-definitions)
(error "Column '%s' already exists" id))
(let ((def (list :id id :name name :type type :params params
:created-at (current-time))))
(puthash id def supertag--virtual-column-definitions)
(message "Virtual column '%s' created" id)
def)))
(defun supertag-virtual-column-get-definition (column-id)
"Get definition by COLUMN-ID."
(gethash column-id supertag--virtual-column-definitions))
(defun supertag-virtual-column-update (column-id updater)
"Update COLUMN-ID using UPDATER function."
(let ((current (supertag-virtual-column-get-definition column-id)))
(unless current
(error "Column '%s' not found" column-id))
(let* ((updated (funcall updater (copy-tree current)))
(new-def (list :id column-id
:name (or (plist-get updated :name)
(plist-get current :name))
:type (or (plist-get updated :type)
(plist-get current :type))
:params (or (plist-get updated :params)
(plist-get current :params))
:updated-at (current-time)
:created-at (plist-get current :created-at))))
(puthash column-id new-def supertag--virtual-column-definitions)
(supertag-virtual-column--invalidate-all column-id)
(message "Virtual column '%s' updated" column-id)
new-def)))
(defun supertag-virtual-column-delete (column-id)
"Delete COLUMN-ID."
(let ((def (supertag-virtual-column-get-definition column-id)))
(when def
(remhash column-id supertag--virtual-column-definitions)
(supertag-virtual-column--invalidate-all column-id)
(message "Virtual column '%s' deleted" column-id)
def)))
(defun supertag-virtual-column-list ()
"List all columns."
(let (result)
(maphash (lambda (_id def) (push def result))
supertag--virtual-column-definitions)
(sort result (lambda (a b)
(string< (plist-get a :id) (plist-get b :id))))))
(defun supertag-virtual-column--cache-get (node-id column-id)
"Get cache entry."
(let ((node-cache (gethash node-id supertag--virtual-column-cache)))
(when node-cache (gethash column-id node-cache))))
(defun supertag-virtual-column--cache-put (node-id column-id value deps)
"Store VALUE with dependencies DEPS."
(let ((node-cache (or (gethash node-id supertag--virtual-column-cache)
(puthash node-id (make-hash-table :test 'equal)
supertag--virtual-column-cache))))
(puthash column-id
(supertag-virtual-column-cache--make
:value value :computed-at (current-time)
:dependencies deps :dirty-flag nil)
node-cache)
value))
(defun supertag-virtual-column--cache-invalidate (node-id column-id)
"Mark as dirty."
(let ((entry (supertag-virtual-column--cache-get node-id column-id)))
(when entry
(setf (supertag-virtual-column-cache-dirty-flag entry) t))))
(defun supertag-virtual-column--invalidate-all (column-id)
"Invalidate all entries for COLUMN-ID."
(maphash (lambda (node-id node-cache)
(when (gethash column-id node-cache)
(supertag-virtual-column--cache-invalidate node-id column-id)))
supertag--virtual-column-cache))
(defun supertag-virtual-column-get (node-id column-id &optional default)
"Get value for NODE-ID/COLUMN-ID."
(condition-case err
(let* ((def (supertag-virtual-column-get-definition column-id))
(cache (supertag-virtual-column--cache-get node-id column-id)))
(cond
((not def) default)
((and cache (not (supertag-virtual-column-cache-dirty-flag cache)))
(supertag-virtual-column-cache-value cache))
(t
(setq supertag--virtual-column-current-dependencies nil)
(let ((value (supertag-virtual-column--compute node-id def)))
(supertag-virtual-column--cache-put
node-id column-id value
supertag--virtual-column-current-dependencies)
(or value default)))))
(error default)))
(defun supertag-virtual-column--compute (node-id def)
"Compute value for NODE-ID using DEF."
(let* ((type (plist-get def :type))
(params (plist-get def :params))
(stack-item (cons node-id (plist-get def :id))))
(when (member stack-item supertag--virtual-column-compute-stack)
(error "Circular dependency"))
(let ((supertag--virtual-column-compute-stack
(cons stack-item supertag--virtual-column-compute-stack)))
(pcase type
(:rollup (supertag-virtual-column--compute-rollup node-id params))
(:formula (supertag-virtual-column--compute-formula node-id params))
(:aggregate (supertag-virtual-column--compute-aggregate node-id params))
(:reference (supertag-virtual-column--compute-reference node-id params))
(_ (error "Unknown type: %s" type))))))
(declare-function supertag-relation-find-by-from "supertag-ops-relation"
(from-id &optional type kind))
(declare-function supertag-node-get-global-field "supertag-ops-global-field" (node-id field-id &optional default))
(declare-function supertag-find-nodes-by-tag "supertag-core-scan" (tag-name))
(defun supertag-virtual-column--compute-rollup (node-id params)
"Compute rollup."
(let* ((rel (plist-get params :relation))
(field (plist-get params :field))
(func (plist-get params :function))
(relations (when (fboundp 'supertag-relation-find-by-from)
(supertag-relation-find-by-from
node-id (if (keywordp rel) rel nil)
:semantic-edge)))
(values
(when relations
(cl-loop for r in relations
for tid = (plist-get r :to)
when tid
collect (if (fboundp 'supertag-node-get-global-field)
(supertag-node-get-global-field tid field 0)
0)
into vals
finally return vals))))
(supertag-rollup-apply func values)))
(defun supertag-virtual-column--compute-aggregate (_node-id params)
"Compute aggregate across all nodes with specified tag."
(let* ((tag (plist-get params :tag))
(field (plist-get params :field))
(func (plist-get params :function))
(nodes (when (fboundp 'supertag-find-nodes-by-tag)
(supertag-find-nodes-by-tag tag)))
(values
(when nodes
(cl-loop for (node-id . _node-data) in nodes
when node-id
collect (if (fboundp 'supertag-node-get-global-field)
(supertag-node-get-global-field node-id field 0)
0)
into vals
finally return vals))))
(supertag-rollup-apply func values)))
(defun supertag-virtual-column--compute-reference (node-id params)
"Compute reference to another node's field."
(let* ((relation-type (plist-get params :relation))
(field-name (plist-get params :field))
(index (or (plist-get params :index) 0))
(relations (when (fboundp 'supertag-relation-find-by-from)
(supertag-relation-find-by-from
node-id (if (keywordp relation-type) relation-type nil)
:semantic-edge)))
(target-node-id
(when relations
(let* ((rel (if (and index (> index 0))
(nth index relations)
(car relations)))
(target (when rel (plist-get rel :to))))
target))))
(when target-node-id
(if (fboundp 'supertag-node-get-global-field)
(supertag-node-get-global-field target-node-id field-name nil)
nil))))
(defun supertag-virtual-column-refresh (node-id column-id)
"Force refresh."
(interactive (list (read-string "Node: ") (read-string "Column: ")))
(supertag-virtual-column--cache-invalidate node-id column-id)
(supertag-virtual-column-get node-id column-id))
(defun supertag-virtual-column-clear-cache (&optional node-id)
"Clear cache."
(interactive)
(if node-id
(remhash node-id supertag--virtual-column-cache)
(clrhash supertag--virtual-column-cache))
(message "Cache cleared"))
(defun supertag-virtual-column--compute-formula (node-id params)
"Compute formula virtual column via the unified formula service."
(let ((formula (plist-get params :formula)))
(unless formula
(error "Formula column requires :formula parameter"))
(supertag-formula-evaluate
formula (supertag-query-node node-id))))
;; UI Commands for Virtual Columns
(defun supertag-virtual-column-create-interactive ()
"Interactively create a new virtual column."
(interactive)
(let* ((id (read-string "Virtual column ID: "))
(name (read-string "Display name: " id))
(type (intern (completing-read "Type: " '("rollup" "formula" "aggregate" "reference") nil t))))
(when (gethash id supertag--virtual-column-definitions)
(error "Column '%s' already exists" id))
(let ((params
(pcase type
(:rollup (supertag-virtual-column--read-rollup-params))
(:formula (supertag-virtual-column--read-formula-params))
(:aggregate (supertag-virtual-column--read-aggregate-params))
(:reference (supertag-virtual-column--read-reference-params))
(_ (error "Unknown type: %s" type)))))
(supertag-virtual-column-create
(list :id id :name name :type type :params params))
(message "Created virtual column '%s'" id))))
(defun supertag-virtual-column--read-rollup-params ()
"Read params for rollup type."
(let ((relation (read-string "Relation type (e.g., children): "))
(field (read-string "Field to aggregate: "))
(function (intern (completing-read "Function: "
'("sum" "count" "avg" "max" "min" "first" "last")
nil t))))
(list :relation relation :field field :function function)))
(defun supertag-virtual-column--read-formula-params ()
"Read params for formula type."
(let ((formula (read-string "Formula (e.g., (done / total) * 100): ")))
(list :formula formula)))
(defun supertag-virtual-column--read-aggregate-params ()
"Read params for aggregate type."
(let ((tag (read-string "Tag name (e.g., project): "))
(field (read-string "Field to aggregate: "))
(function (intern (completing-read "Function: "
'("sum" "count" "avg" "max" "min" "first" "last")
nil t))))
(list :tag tag :field field :function function)))
(defun supertag-virtual-column--read-reference-params ()
"Read params for reference type."
(let ((relation (read-string "Relation type (e.g., parent): "))
(field (read-string "Field to reference: "))
(index (read-number "Index (0=first, default 0): " 0)))
(list :relation relation :field field :index index)))
(defun supertag-virtual-column-edit-interactive ()
"Interactively edit a virtual column."
(interactive)
(let* ((columns (supertag-virtual-column-list))
(ids (mapcar (lambda (c) (plist-get c :id)) columns))
(id (completing-read "Edit virtual column: " ids nil t))
(current (supertag-virtual-column-get-definition id)))
(unless current
(error "Column '%s' not found" id))
(let* ((current-name (plist-get current :name))
(new-name (read-string "New name (empty to keep): " current-name))
(updater (lambda (_)
(when (and new-name (not (string-empty-p new-name)))
(list :name new-name)))))
(supertag-virtual-column-update id updater)
(message "Updated virtual column '%s'" id))))
(defun supertag-virtual-column-delete-interactive ()
"Interactively delete a virtual column."
(interactive)
(let* ((columns (supertag-virtual-column-list))
(ids (mapcar (lambda (c) (plist-get c :id)) columns))
(id (completing-read "Delete virtual column: " ids nil t)))
(when (yes-or-no-p (format "Delete virtual column '%s'? " id))
(supertag-virtual-column-delete id)
(message "Deleted virtual column '%s'" id))))
(defun supertag-virtual-column-list-interactive ()
"Display list of virtual columns."
(interactive)
(with-output-to-temp-buffer "*Virtual Columns*"
(princ "Virtual Columns\n")
(princ "===============\n\n")
(let ((columns (supertag-virtual-column-list)))
(if (null columns)
(princ "No virtual columns defined.\n")
(dolist (col columns)
(princ (format "ID: %s\n" (plist-get col :id)))
(princ (format " Name: %s\n" (plist-get col :name)))
(princ (format " Type: %s\n" (plist-get col :type)))
(princ (format " Params: %s\n\n" (plist-get col :params)))))))
(pop-to-buffer "*Virtual Columns*"))
(defun supertag-virtual-column-init ()
"Initialize."
(interactive)
(clrhash supertag--virtual-column-cache)
(clrhash supertag--virtual-column-dependency-graph)
(message "Virtual column system initialized"))
(provide 'supertag-virtual-column)
;;; supertag-virtual-column.el ends here