perf(db): batch tag inserts with prepared statements in set-media-tags! (P2)

ober

7960831beaf8141a33c434045369ab9bd5106668

diff --git a/src/imagesite/db.ss b/src/imagesite/db.ss
index 6ee71d8..4c4174e 100644
--- a/src/imagesite/db.ss
+++ b/src/imagesite/db.ss
@@ -3,7 +3,10 @@
                 sqlite-finalize sqlite-step sqlite-query sqlite-execute)
         (only (std db sqlite)
               sqlite-open sqlite-close sqlite-exec sqlite-eval sqlite-query
-              sqlite-last-insert-rowid)
+              sqlite-last-insert-rowid
+              sqlite-prepare sqlite-step sqlite-reset sqlite-finalize
+              sqlite-bind! sqlite-column-value
+              SQLITE_ROW SQLITE_DONE)
         (only (jsqlite value) sql-null sql-null?)
         (only (imagesite util)
               safe-relative-path? relative-dirname path-basename
@@ -332,14 +335,49 @@
 
 (def (set-media-tags! db media-id tags)
   (sqlite-eval db "DELETE FROM media_tags WHERE media_id = ?" media-id)
-  (for-each
-    (lambda (tag)
-      (when (string? tag)
-        (let ((tag-id (ensure-tag! db tag)))
-          (when tag-id
-            (sqlite-eval db "INSERT OR IGNORE INTO media_tags (media_id, tag_id) VALUES (?, ?)"
-                         media-id tag-id)))))
-    tags))
+  (let ((tag-insert (sqlite-prepare db "INSERT OR IGNORE INTO tags (name, slug) VALUES (?, ?)"))
+        (tag-select (sqlite-prepare db "SELECT id FROM tags WHERE name = ?"))
+        (tag-select-slug (sqlite-prepare db "SELECT id FROM tags WHERE slug = ?"))
+        (mt-insert (sqlite-prepare db "INSERT OR IGNORE INTO media_tags (media_id, tag_id) VALUES (?, ?)")))
+    (unwind-protect
+      (for-each
+        (lambda (tag)
+          (when (string? tag)
+            (let ((clean (string-trim tag)))
+              (unless (string-blank? clean)
+                (let ((slug (slugify clean)))
+                  (sqlite-bind! tag-select 1 clean)
+                  (let ((tag-id (if (= (sqlite-step tag-select) SQLITE_ROW)
+                                  (sqlite-column-value tag-select 0)
+                                  #f)))
+                    (sqlite-reset tag-select)
+                    (unless tag-id
+                      (sqlite-bind! tag-insert 1 clean)
+                      (sqlite-bind! tag-insert 2 slug)
+                      (sqlite-step tag-insert)
+                      (sqlite-reset tag-insert)
+                      (sqlite-bind! tag-select 1 clean)
+                      (set! tag-id (if (= (sqlite-step tag-select) SQLITE_ROW)
+                                     (sqlite-column-value tag-select 0)
+                                     #f))
+                      (sqlite-reset tag-select)
+                      (unless tag-id
+                        (sqlite-bind! tag-select-slug 1 slug)
+                        (set! tag-id (if (= (sqlite-step tag-select-slug) SQLITE_ROW)
+                                       (sqlite-column-value tag-select-slug 0)
+                                       #f))
+                        (sqlite-reset tag-select-slug)))
+                    (when tag-id
+                      (sqlite-bind! mt-insert 1 media-id)
+                      (sqlite-bind! mt-insert 2 tag-id)
+                      (sqlite-step mt-insert)
+                      (sqlite-reset mt-insert))))))))
+        tags)
+      (begin
+        (sqlite-finalize tag-insert)
+        (sqlite-finalize tag-select)
+        (sqlite-finalize tag-select-slug)
+        (sqlite-finalize mt-insert)))))
 
 (def (album-row->hash row)
   (object
diff --git a/tests/imagesite/db-test.ss b/tests/imagesite/db-test.ss
index 52cf6e9..a46c4f8 100644
--- a/tests/imagesite/db-test.ss
+++ b/tests/imagesite/db-test.ss
@@ -68,6 +68,23 @@
       (chk (length (media-by-kind db "video")) => 1)
       (mark-all-media-missing! db)
       (chk (hash-get (counts db) "media") => 0)
+      (let ((r3 (upsert-media! db "family/tagged.jpg" 300 126)))
+        (chk (car r3) => 'inserted)
+        (let ((mid (cdr r3)))
+          (set-media-tags! db mid '("alpha" "beta" "gamma" "delta" "epsilon"))
+          (chk (length (media-tags db mid)) => 5)
+          (chk (media-tags db mid) => '("alpha" "beta" "delta" "epsilon" "gamma"))
+          (set-media-tags! db mid '("alpha" "zeta"))
+          (chk (length (media-tags db mid)) => 2)
+          (chk (media-tags db mid) => '("alpha" "zeta"))
+          (set-media-tags! db mid '())
+          (chk (length (media-tags db mid)) => 0)
+          (with-imagesite-transaction
+            db
+            (lambda ()
+              (set-media-tags! db mid '("txn-a" "txn-b" "txn-c"))))
+          (chk (length (media-tags db mid)) => 3)
+          (chk (media-tags db mid) => '("txn-a" "txn-b" "txn-c"))))
       (imagesite-close db))
     (delete-if-exists path)
     (delete-if-exists "var/test/db-test.db-wal")