branch: elpa/gnosis
commit 5501672a1e9bc6c63fe414b80e5b7bbec0742b0f
Author: Thanos Apollo <[email protected]>
Commit: Thanos Apollo <[email protected]>

    [Refactor] rewrite db migration to sequential step based system.
---
 gnosis.el | 194 +++++++++++++++++++++++++++++++++++---------------------------
 1 file changed, 110 insertions(+), 84 deletions(-)

diff --git a/gnosis.el b/gnosis.el
index c19b8b23b2..ea32ae43ae 100644
--- a/gnosis.el
+++ b/gnosis.el
@@ -832,12 +832,10 @@ If you only require a tag prompt, refer to 
`gnosis-tags--prompt'."
         (org-set-tags (append input current-tags))))))
 
 (defun gnosis-tags-refresh ()
-  "Refresh tags value."
+  "Refresh tags table from current themata tags."
   (let ((tags (gnosis-get-tags--unique)))
-    ;; Delete all values from tags table.
-    (gnosis--delete 'tags nil)
-    ;; Insert all unique tags from themata.
-    (emacsql-with-transaction gnosis-db
+    (emacsql-with-transaction (gnosis--ensure-db)
+      (gnosis--delete 'tags)
       (cl-loop for tag in tags
               do (gnosis--insert-into 'tags `[,tag])))))
 
@@ -959,21 +957,19 @@ LINKS: List of id links."
 
 If gnosis ID does not exist, create it anew.
 When `gnosis--id-cache' is bound, uses hash table for existence check."
-  (let ((id (if (stringp id) (string-to-number id) id)))
+  (let* ((id (if (stringp id) (string-to-number id) id))
+        (current-type (gnosis-get 'type 'themata `(= id ,id)))
+        (current-deck (gnosis-get 'deck-id 'themata `(= id ,id))))
     (if (if gnosis--id-cache
            (gethash id gnosis--id-cache)
          (member id (gnosis-select 'id 'themata nil t)))
-       (emacsql-with-transaction gnosis-db
-         ;; Single UPDATE for core themata fields
-         (gnosis-update 'themata `(= keimenon ,keimenon) `(= id ,id))
-         (gnosis-update 'themata `(= hypothesis ',hypothesis) `(= id ,id))
-         (gnosis-update 'themata `(= answer ',answer) `(= id ,id))
-         (gnosis-update 'themata `(= tags ',tags) `(= id ,id))
-         (when type
-           (gnosis-update 'themata `(= type ,type) `(= id ,id)))
-         (when deck-id
-           (gnosis-update 'themata `(= deck-id ,deck-id) `(= id ,id)))
-         ;; Single UPDATE for extras table
+       (emacsql-with-transaction (gnosis--ensure-db)
+         ;; Single multi-column UPDATE for themata
+         (emacsql (gnosis--ensure-db)
+           "UPDATE themata SET keimenon = $s1, hypothesis = $s2, answer = $s3, 
tags = $s4, type = $s5, deck_id = $s6 WHERE id = $s7"
+           keimenon hypothesis answer tags
+           (or type current-type) (or deck-id current-deck) id)
+         ;; Single UPDATE for extras
          (gnosis-update 'extras `(= parathema ,parathema) `(= id ,id))
          ;; Re-sync links
          (gnosis--delete 'links `(= source ,id))
@@ -1498,11 +1494,11 @@ Return thema ids for themata that match QUERY."
 ;;; Database Schemas
 (defconst gnosis-db--schemata
   '((decks
-     ([(id integer :primary-key :autoincrement)
+     ([(id integer :primary-key)
        (name text :not-null)]
       (:unique [name])))
     (themata
-     ([(id integer :primary-key :autoincrement)
+     ([(id integer :primary-key)
        (type text :not-null)
        (keimenon text :not-null)
        (hypothesis text :not-null)
@@ -1549,91 +1545,121 @@ Return thema ids for themata that match QUERY."
                    :on-delete :cascade)
       (:unique [source dest])))))
 
-(defun gnosis-update--make-list (column)
+(defun gnosis--db-version ()
+  "Return the current user_version pragma from the database."
+  (caar (emacsql (gnosis--ensure-db) [:pragma user-version])))
+
+(defun gnosis--db-set-version (version)
+  "Set the database user_version pragma to VERSION."
+  (emacsql (gnosis--ensure-db) `[:pragma (= user-version ,version)]))
+
+(defun gnosis--db-create-tables ()
+  "Create all tables and set version to current.
+Used for fresh databases only."
+  (emacsql-with-transaction (gnosis--ensure-db)
+    (pcase-dolist (`(,table ,schema) gnosis-db--schemata)
+      (emacsql (gnosis--ensure-db) [:create-table $i1 $S2] table schema))
+    (gnosis--db-set-version gnosis-db-version)))
+
+(defun gnosis--db-has-tables-p ()
+  "Return non-nil if the database has user tables."
+  (let ((tables (emacsql (gnosis--ensure-db)
+                        [:select name :from sqlite-master
+                         :where (= type table)])))
+    (length> tables 0)))
+
+;;; Migration helpers
+
+(defun gnosis--migrate-make-list (column)
   "Make COLUMN values into a list."
-  (let ((results (emacsql gnosis-db `[:select [id ,column] :from themata])))
+  (let ((results (emacsql (gnosis--ensure-db)
+                         `[:select [id ,column] :from themata])))
     (dolist (row results)
       (let ((id (car row))
             (old-value (cadr row)))
-       ;; Update each entry, converting the value to a list representation
        (unless (listp old-value)
-         (emacsql gnosis-db `[:update themata
-                                      :set (= ,column $s1)
-                                      :where (= id $s2)]
-                  (list old-value)
-                  id)
-         (message "Update Thema: %d" id))))))
-
-(defun gnosis-db-update-v4 ()
-  "Update to databse version v4."
-  (let ((tags (gnosis-get-tags--unique)))
+         (emacsql (gnosis--ensure-db)
+                  `[:update themata :set (= ,column $s1) :where (= id $s2)]
+                  (list old-value) id))))))
+
+(defun gnosis-db--migrate-v1 ()
+  "Migration v1: rename notes table to themata."
+  (emacsql (gnosis--ensure-db) [:alter-table notes :rename-to themata])
+  (gnosis--db-set-version 1))
+
+(defun gnosis-db--migrate-v2 ()
+  "Migration v2: column renames, tags/links tables, data conversions."
+  (let ((db (gnosis--ensure-db))
+       (tags (gnosis-get-tags--unique)))
+    ;; Create tags and links tables
     (pcase-dolist (`(,table ,schema) (seq-filter (lambda (schema)
                                                   (member (car schema) '(links 
tags)))
                                                 gnosis-db--schemata))
-      (emacsql gnosis-db [:create-table :if-not-exists $i1 $S2] table schema))
+      (emacsql db [:create-table :if-not-exists $i1 $S2] table schema))
     (cl-loop for tag in tags
             do (gnosis--insert-into 'tags `[,tag]))
-    (emacsql gnosis-db [:alter-table themata :rename-column main :to keimenon])
-    (emacsql gnosis-db [:alter-table themata :rename-column options :to 
hypothesis])
-    (emacsql gnosis-db [:alter-table extras :rename-column extra-themata :to 
parathema])
-    (emacsql gnosis-db [:alter-table extras :rename-column images :to 
review-image])
-    (emacsql gnosis-db [:alter-table extras :drop-column extra-image])
+    ;; Column renames
+    (emacsql db [:alter-table themata :rename-column main :to keimenon])
+    (emacsql db [:alter-table themata :rename-column options :to hypothesis])
+    (emacsql db [:alter-table extras :rename-column extra-themata :to 
parathema])
+    (emacsql db [:alter-table extras :rename-column images :to review-image])
+    (emacsql db [:alter-table extras :drop-column extra-image])
     ;; Make sure all hypothesis & answer values are lists
-    (gnosis-update--make-list 'hypothesis)
-    (gnosis-update--make-list 'answer)
+    (gnosis--migrate-make-list 'hypothesis)
+    (gnosis--migrate-make-list 'answer)
     ;; Fix MCQs
     (cl-loop for thema in (gnosis-select 'id 'themata '(= type "mcq") t)
-            do (funcall
-                (lambda (id)
-                  (let* ((data (gnosis-select '[hypothesis answer] 'themata 
`(= id ,id) t))
-                         (hypothesis (nth 0 data))
-                         (old-answer (car (nth 1 data)))
-                         (new-answer (when (integerp old-answer)
-                                       (list (nth (- 1 old-answer) 
hypothesis)))))
-                    (when (integerp old-answer)
-                      (gnosis-update 'themata `(= answer ',new-answer) `(= id 
,id)))))
-                thema))
+            do (let* ((data (gnosis-select '[hypothesis answer] 'themata
+                                           `(= id ,thema) t))
+                      (hypothesis (nth 0 data))
+                      (old-answer (car (nth 1 data)))
+                      (new-answer (when (integerp old-answer)
+                                    (list (nth (- 1 old-answer) hypothesis)))))
+                 (when (integerp old-answer)
+                   (gnosis-update 'themata `(= answer ',new-answer)
+                                  `(= id ,thema)))))
     ;; Replace y-or-n with MCQ
     (cl-loop for thema in (gnosis-select 'id 'themata '(= type "y-or-n") t)
-            do (funcall (lambda (id)
-                          (let ((data (gnosis-select '[type hypothesis answer]
-                                                     'themata `(= id ,id) t)))
-                            (when (string= (nth 0 data) "y-or-n")
-                              (gnosis-update 'themata '(= type "mcq") `(= id 
,id))
-                              (gnosis-update 'themata '(= hypothesis '("Yes" 
"No"))
-                                             `(= id ,id))
-                              (if (= (car (nth 2 data)) 121)
-                                  (gnosis-update 'themata '(= answer '("Yes"))
-                                                 `(= id ,id))
-                                (gnosis-update 'themata '(= answer '("No"))
-                                               `(= id ,id))))))
-                        thema))
+            do (let ((data (gnosis-select '[type hypothesis answer]
+                                          'themata `(= id ,thema) t)))
+                 (when (string= (nth 0 data) "y-or-n")
+                   (gnosis-update 'themata '(= type "mcq") `(= id ,thema))
+                   (gnosis-update 'themata '(= hypothesis '("Yes" "No"))
+                                  `(= id ,thema))
+                   (if (= (car (nth 2 data)) 121)
+                       (gnosis-update 'themata '(= answer '("Yes"))
+                                      `(= id ,thema))
+                     (gnosis-update 'themata '(= answer '("No"))
+                                    `(= id ,thema))))))
     ;; Replace - with _, org does not support tags with dash.
     (cl-loop for tag in (gnosis-get-tags--unique)
-            ;; Replaces dashes to underscores.
             if (string-match-p "-" tag)
-            do (gnosis-tag-rename tag (replace-regexp-in-string "-" "_" 
tag)))))
-
-(defun gnosis-db-update-v5 ()
-  "Update database v5."
-  (emacsql gnosis-db [:alter-table notes :rename-to themata])
-  (emacsql gnosis-db `[:pragma (= user-version ,gnosis-db-version)]))
+            do (gnosis-tag-rename tag (replace-regexp-in-string "-" "_" tag))))
+  (gnosis--db-set-version 2))
+
+(defconst gnosis-db--migrations
+  `((1 . gnosis-db--migrate-v1)
+    (2 . gnosis-db--migrate-v2))
+  "Alist of (VERSION . FUNCTION).
+Each migration brings the DB from VERSION-1 to VERSION.")
+
+(defun gnosis--db-run-migrations (current-version)
+  "Run all pending migrations from CURRENT-VERSION to `gnosis-db-version'."
+  (cl-loop for (version . func) in gnosis-db--migrations
+          when (> version current-version)
+          do (progn
+               (message "Gnosis: running migration to v%d..." version)
+               (funcall func)
+               (message "Gnosis: migration to v%d complete" version))))
 
 (defun gnosis-db-init ()
-  "Create essential directories & database."
-  (let ((gnosis-curr-version (caar (emacsql gnosis-db  [:pragma 
user-version]))))
-    (unless (length> (emacsql gnosis-db [:select name :from sqlite-master
-                                                :where (= type table)])
-                    3)
-      (emacsql-with-transaction gnosis-db
-       (pcase-dolist (`(,table ,schema) gnosis-db--schemata)
-         (emacsql gnosis-db [:create-table $i1 $S2] table schema))
-        (emacsql gnosis-db `[:pragma (= user-version ,gnosis-db-version)])))
-    ;; Update database schema for version
-    (cond ((= gnosis-curr-version 2)
-          (gnosis-db-update-v4))
-         ((< gnosis-curr-version 3)
-          (gnosis-db-update-v5)))))
+  "Initialize database: create tables if fresh, run pending migrations."
+  (let ((version (gnosis--db-version)))
+    (if (and (zerop version) (not (gnosis--db-has-tables-p)))
+       ;; Fresh database: create all tables at current version
+       (gnosis--db-create-tables)
+      ;; Existing database: run any pending migrations
+      (gnosis--db-run-migrations version))))
 
 ;; VC functions ;;
 ;;;;;;;;;;;;;;;;;;

Reply via email to