Functions for updating an H2 v1.x database to v2.x | (ns metabase.db.update-h2 (:require [clj-http.client :as http] [clojure.java.io :as io] [clojure.java.jdbc :as jdbc] [clojure.java.shell :as sh] [clojure.string :as str] [metabase.util.files :as u.files] [metabase.util.log :as log]) (:import (java.nio.file Files))) |
(set! *warn-on-reflection* true) | |
Generic utils | |
Returns seq of first n bytes of file at path | (defn- head
[path n]
(let [f (io/file path)
bytes (byte-array n)]
(with-open [input (io/input-stream f)]
(take (.read input bytes) bytes)))) |
Tries to parse and return x as char, else nil | (defn- try-char [x] (try (char x) (catch IllegalArgumentException _ nil))) |
H2-specific utils | |
Returns H2 database base path from JDBC URL, i.e. without .mv.db | (defn- h2-base-path [jdbc-url] (second (re-matches #"jdbc:h2:file:(.*)$" jdbc-url))) |
Returns the H2 major version number of H2 MV database file at path, or nil if no file exists | (defn- db-version!
[jdbc-url]
;; The H2 database version is indicated in the "format:" key of the MV file header, which is 4096 bytes
;; See: https://www.h2database.com/html/mvstore.html
(when-let [path (str (h2-base-path jdbc-url) ".mv.db")]
(when (.exists (io/file path))
(let [header (str/join (map try-char (head path 4096)))
format-key "format:"]
(when-not (.startsWith header "H:2")
(throw (IllegalArgumentException. "File does not appear to be an H2 MV database file")))
(Integer/parseInt (str (nth header (+ (.indexOf header format-key) (count format-key))))))))) |
Migration constants/utils | |
(def ^:private v1-jar-url "https://repo1.maven.org/maven2/com/h2database/h2/1.4.197/h2-1.4.197.jar") | |
(defn- tmp-path [& components] (str (apply u.files/get-path (System/getProperty "java.io.tmpdir") components))) | |
(def ^:private jar-path (tmp-path (last (.split ^String v1-jar-url "/")))) | |
(def ^:private migration-sql-path (tmp-path "metabase-migrate-h2-db-v1-v2.sql")) | |
Migration logic | |
Updates existing H2 v1 database to H2 v2 | (defn- update!
[jdbc-url]
(when-not (.exists (io/file jar-path))
(log/info "Downloading" v1-jar-url)
(io/copy (:body (http/get v1-jar-url {:as :stream})) (io/file jar-path)))
(log/info "Creating v1 database backup at" migration-sql-path)
(let [result (sh/sh "java" "-cp" jar-path "org.h2.tools.Script" "-url" jdbc-url "-script" migration-sql-path)]
(when-not (= 0 (:exit result))
(throw (ex-info "Dumping H2 database failed." {:result result}))))
(let [base-path (h2-base-path jdbc-url)
backup-path (str base-path ".v1-backup.mv.db")]
(log/info "Moving old app database to" backup-path)
(Files/move (u.files/get-path (str base-path ".mv.db"))
(u.files/get-path backup-path)
(into-array java.nio.file.CopyOption [])))
(log/info "Restoring backup into v2 database")
(jdbc/execute! {:connection-uri jdbc-url} ["RUNSCRIPT FROM ? FROM_1X" migration-sql-path])
(log/info "Backup restored into H2 v2 database. Update complete!")) |
(def ^:private h2-lock (Object.)) | |
(defn- update-needed? [jdbc-url] (= 1 (db-version! jdbc-url))) | |
Updates H2 database at db-path from version 1.x to 2.x if jdbc-url points to version 1 H2 database. | (defn update-if-needed!
[jdbc-url]
(when (and (h2-base-path jdbc-url) (update-needed? jdbc-url))
(locking h2-lock
;; the database may have been upgraded while we waited for the lock
(when (update-needed? jdbc-url)
(log/info "H2 v1 database detected, updating...")
(try
(update! jdbc-url)
(catch Exception e
(log/error e "Failed to update H2 database:")
(throw e))))))) |