Compare commits

...

8 Commits

Author SHA1 Message Date
Ben Grabow
cb525c79a5 Fix jar splice 2026-06-26 17:00:57 -05:00
Ben Grabow
dc63a440f3 Splice H2 lib into metabase jar before running 2026-06-26 15:43:44 -05:00
Ben Grabow
01c9771b31 Allow hardcoded :h2 driver name refs in tests (at least for now) 2026-06-26 15:08:26 -05:00
Ben Grabow
05548699e4 Include H2 driver source on path for tests 2026-06-25 16:21:26 -05:00
Ben Grabow
14cf562376 Work around buggy linter result 2026-06-25 16:20:44 -05:00
Ben Grabow
2c30d66bf0 Move H2 driver out of core 2026-06-25 09:13:10 -05:00
Ben Grabow
c103dde120 Sort deps 2026-06-24 16:02:46 -05:00
dan sutton
8605b43a63 Make H2 an optional driver (OSS-bundled, EE-supplied)
H2 is no longer required on the classpath. The OSS uberjar bundles the H2
JDBC JAR; the EE uberjar does not. EE users who want H2 (quick trials, the
dump-to-h2/load-from-h2 tasks, or an H2 app DB) supply the JAR themselves via
the plugins dir or `-cp`. SQLite, not H2, is the sample DB, so H2 isn't needed
by default.

Mechanics, mirroring the oracle/vertica optional-driver pattern:

- deps.edn: H2 moved from a top-level dep to the `:oss` alias only (plus `:dev`
  for the REPL); the `:ee` basis no longer includes it.
- build/uberjar.clj: exclude `metabase.driver.h2` from AOT (its ns `:import`s
  org.h2.* classes that may be absent at EE build time) and inject it into the
  jar as source, compiled lazily at runtime. Driven by a dedicated
  `core-driver-namespaces-excluded-from-aot` set.
- config.core/h2-available?: runtime check for `org.h2.Driver` on the classpath
  (re-checked at boot, so a JAR added via -cp is seen).
- driver.init: load metabase.driver.h2 only `(when (config/h2-available?))`,
  via a runtime require, instead of an unconditional ns require.
- No core (AOT-compiled) namespace references org.h2.* at compile time anymore.
  The H2-class-typed registrations (IResultSetReadColumn for JdbcClob/JdbcBlob,
  cache results-as-bytes for JdbcBlob, query-canceled?* :h2) moved into
  metabase.driver.h2; the owning multimethods were made public and exposed in
  the app-db / query-processor module :api sets. search.appdb.index detects the
  H2 syntax-error exception by class-name string instead of importing it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:03:10 -05:00
18 changed files with 233 additions and 47 deletions

View File

@@ -333,6 +333,9 @@
:api #{metabase.app-db.cluster-lock ; TODO FIXME
metabase.app-db.core
metabase.app-db.init
;; exposes `query-canceled-exception?*` so the optional H2 driver can register its `:h2` method; H2 classes
;; can't live in core (AOT) namespaces. See `metabase.config.core/h2-available?`.
metabase.app-db.query-cancelation
metabase.app-db.setup
metabase.app-db.transient-error}
:uses #{auth-provider
@@ -837,7 +840,9 @@
metabase.driver.common
metabase.driver.connection
metabase.driver.ddl.interface
metabase.driver.h2
;; NOTE: `metabase.driver.h2` is intentionally NOT here. H2 is an optional driver loaded via a runtime
;; `require` (see `metabase.driver.init` / `metabase.config.core/h2-available?`), so nothing references it
;; at compile time and the module checker treats it as extraneous in `:api`.
metabase.driver.impl
metabase.driver.init
metabase.driver.mysql
@@ -1803,6 +1808,9 @@
metabase.query-processor.init
metabase.query-processor.interface
metabase.query-processor.metadata
;; exposes the `results-as-bytes` multimethod so the optional H2 driver can register its `JdbcBlob`
;; method; H2 classes can't live in core (AOT) namespaces. See `metabase.config.core/h2-available?`.
metabase.query-processor.middleware.cache-backend.db
metabase.query-processor.middleware.constraints
metabase.query-processor.middleware.enterprise
metabase.query-processor.middleware.permissions

View File

@@ -49,7 +49,10 @@
:deps
vals
(keep (comp keyword :local/root))
(into #{}))]
(into #{})
;; :h2 is intentionally excluded from the linter set (it's the default test driver, so
;; hardcoding it is fine and ubiquitous), even though it now lives in modules/drivers.
(#(disj % :h2)))]
(doseq [driver driver-modules]
(is (contains? driver-keywords driver)
(format "hooks.clojure.test/driver-keywords should contain %s, please add it" driver))))))

View File

@@ -82,10 +82,12 @@
ns-decls))
(def ^:private drivers-excluded-from-aot
"Drivers whose JDBC dependencies are not bundled due to licensing restrictions (users must supply the JDBC driver JAR
themselves). These drivers are included as source on the classpath and compiled lazily at runtime when their JDBC
driver is present in the plugins directory."
#{"oracle" "vertica"})
"Names of `modules/drivers/*` driver modules whose JDBC dependency may be absent from the build classpath, so they
must NOT be AOT-compiled (their ns forms `:import` JDBC classes that aren't present). They ship as source (see
[[add-non-aot-driver-sources!]]) and are compiled lazily at runtime once their JDBC driver is on the classpath.
`oracle`/`vertica` JARs are user-supplied due to licensing; the `h2` JAR is bundled in OSS builds but EE-supplied
(see `metabase.config.core/h2-available?`)."
#{"h2" "oracle" "vertica"})
(defn- all-drivers []
(->> (.listFiles (io/file (u/filename u/project-root-directory "modules" "drivers")))
@@ -227,11 +229,20 @@
(.write (manifest) os)
(.flush os))
(defn- copy-source-file-into-jar!
"Copy the source file `f` (under source root `src-dir`) into the open uberjar filesystem `fs`, preserving its
classpath-relative path so it's loadable as source at runtime."
[fs ^File src-dir ^File f]
(let [rel-path (.toString (.relativize (.toPath src-dir) (.toPath f)))
target (u/get-path-in-filesystem fs rel-path)]
(Files/createDirectories (.getParent target) (misc/varargs java.nio.file.attribute.FileAttribute))
(Files/copy (.toPath f) target (misc/varargs java.nio.file.CopyOption))))
(defn- add-non-aot-driver-sources!
"Inject source files for drivers excluded from AOT directly into the uberjar.
These drivers can't be AOT-compiled (their ns forms reference JDBC classes not bundled due to licensing), so they
ship as source and are compiled lazily at runtime. We add them after b/uber because uber strips all .clj files from
class-dir."
"Inject source files for the [[drivers-excluded-from-aot]] driver modules directly into the uberjar.
These drivers can't be AOT-compiled (their ns forms `:import` JDBC classes that may be absent from the build
classpath), so they ship as source and are compiled lazily at runtime. We add them after b/uber because uber strips
all .clj files from class-dir."
[]
(u/step "Add non-AOT driver sources to uberjar"
(u/with-open-jar-file-system [fs uberjar-filename]
@@ -241,10 +252,7 @@
(u/step (format "Add source for %s" driver)
(doseq [^File f (file-seq src-dir)
:when (.isFile f)]
(let [rel-path (.toString (.relativize (.toPath src-dir) (.toPath f)))
target (u/get-path-in-filesystem fs rel-path)]
(Files/createDirectories (.getParent target) (misc/varargs java.nio.file.attribute.FileAttribute))
(Files/copy (.toPath f) target (misc/varargs java.nio.file.CopyOption)))))))))
(copy-source-file-into-jar! fs src-dir f)))))))
(defn update-manifest!
"Start a build step that updates the manifest.

View File

@@ -50,8 +50,6 @@
com.google.auth/google-auth-library-credentials {:mvn/version "1.40.0"} ; dep for BigQuery, Databricks, and Snowflake. Require here rather than letting different dep versions stomp on each other — see #70908
com.google.code.gson/gson {:mvn/version "2.12.1"} ; dep for BigQuery, Databricks, and Snowflake. Pin here so driver plugin JARs all use the same version — see #73736
com.google.guava/guava {:mvn/version "33.5.0-jre"} ; dep for BigQuery, Spark, and GA. Require here rather than letting different dep versions stomp on each other — see comments on #9697
com.h2database/h2 ^:antq/exclude ; https://metaboat.slack.com/archives/CKZEMT1MJ/p1727191010259979
{:mvn/version "2.1.214"} ; embedded SQL database
com.ibm.icu/icu4j {:mvn/version "78.1"} ; Used to transliterate Unicode characters into Latin characters
com.knuddels/jtokkit {:mvn/version "1.1.0"} ; Java tokenizer library for OpenAI models, used for token counting in semantic search
com.mchange/c3p0 {:mvn/version "0.12.0"} ; DB connection pool
@@ -263,6 +261,8 @@
{:mvn/version "0.4.0"} ; Enables easy memory measurement
com.gfredericks/test.chuck {:mvn/version "0.2.15"} ; generating strings from regexes (useful with malli)
com.github.erosb/json-sKema {:mvn/version "0.25.0"} ; validating MBQL JSON Schema
com.h2database/h2 ^:antq/exclude ; https://metaboat.slack.com/archives/CKZEMT1MJ/p1727191010259979
{:mvn/version "2.1.214"} ; embedded SQL database
criterium/criterium {:mvn/version "0.4.6"} ; benchmarking library
djblue/portal {:mvn/version "0.62.0"} ; ui for inspecting values
integrant/integrant {:mvn/version "1.0.1"} ; component lifecycle management
@@ -294,7 +294,11 @@
ring/ring-devel {:mvn/version "1.15.3"} ; reload clojure files on the fly
ring/ring-mock {:mvn/version "0.6.2"} ; creating Ring request maps for testing purposes
talltale/talltale {:mvn/version "0.5.14"}} ; generates fake data, useful for prototyping or load testing
:extra-paths ["dev/src" "local/src" "test" "test_resources" ".clj-kondo/src" ".clj-kondo/test" "dev/resources"]
;; H2 is the default dev/test data driver but now lives in its own driver module, so its source isn't on the core
;; classpath. Include it here (the H2 JAR is already a `:dev` dep) so every `:dev`/test run can load it without
;; pulling in all of `:drivers`. `:check` and the uberjar build don't use `:dev`, so they still exclude it.
:extra-paths ["dev/src" "local/src" "test" "test_resources" ".clj-kondo/src" ".clj-kondo/test" "dev/resources"
"modules/drivers/h2/src"]
:jvm-opts ["-Dmb.run.mode=dev"
"-Dmb.field.filter.operators.enabled=true"
"-Dmb.test.env.setting=ABCDEFG"
@@ -437,7 +441,10 @@
;; these aliases exist for symmetry with the ee aliases. Empty for now.
:oss
{}
{:extra-deps
{com.h2database/h2 ^:antq/exclude ; https://metaboat.slack.com/archives/CKZEMT1MJ/p1727191010259979
;; embedded SQL database
{:mvn/version "2.1.214"}}}
:oss-dev
{}
@@ -561,6 +568,7 @@
"modules/drivers/databricks/test"
"modules/drivers/druid-jdbc/src"
"modules/drivers/druid-jdbc/test"
"modules/drivers/h2/src"
"modules/drivers/hive-like/src"
"modules/drivers/hive-like/test"
"modules/drivers/mongo/src"
@@ -621,6 +629,7 @@
"modules/drivers/clickhouse/src"
"modules/drivers/databricks/src"
"modules/drivers/druid-jdbc/src"
"modules/drivers/h2/src"
"modules/drivers/hive-like/src"
"modules/drivers/mongo/src"
"modules/drivers/oracle/src"

View File

@@ -14,6 +14,22 @@ function getJvmOptsFromDepsEdn(alias = "e2e") {
return execSync(cmd, { encoding: "utf8" }).trim().toString();
}
// E2E always uses the H2 sample database, but the H2 driver JAR is bundled only in OSS uberjars (EE supplies it
// separately). Resolve the JAR from the local Maven repo so we can put it on the classpath explicitly when launching
// the jar -- otherwise the EE backend can't open the H2 sample DB and every test fails.
function getH2JarPath() {
const cmd =
"clojure -Spath -Sdeps '{:deps {com.h2database/h2 {:mvn/version \"2.1.214\"}}}'";
const cp = execSync(cmd, { encoding: "utf8" }).trim();
const h2 = cp
.split(path.delimiter)
.find((p) => /[/\\]h2-[\d.]+\.jar$/.test(p));
if (!h2) {
throw new Error("Could not resolve the H2 JAR for the E2E classpath");
}
return h2;
}
// Ensure that the only two required env vars have values
process.env.MB_DB_FILE = process.env.MB_DB_FILE || tempDbPath;
process.env.MB_JETTY_PORT = process.env.MB_JETTY_PORT || 4000;
@@ -47,11 +63,17 @@ const CypressBackend = {
}
if (!this.server.process) {
process.env.JDK_JAVA_OPTIONS = getJvmOptsFromDepsEdn();
this.server.process = spawn("java", ["-jar", jarPath], {
env: process.env,
stdio: process.env.CI ? "ignore" : "inherit",
detached: true,
});
// Launch via `-cp` (not `-jar`) so we can append the H2 driver JAR; `java -jar` ignores the classpath.
const classpath = [jarPath, getH2JarPath()].join(path.delimiter);
this.server.process = spawn(
"java",
["-cp", classpath, "metabase.core.bootstrap"],
{
env: process.env,
stdio: process.env.CI ? "ignore" : "inherit",
detached: true,
},
);
await waitUntilReady(this.server);
if (process.env.CI) {
this.server.process.unref(); // detach console

View File

@@ -1,4 +1,6 @@
import { execSync } from "child_process";
import fs from "fs";
import os from "os";
import * as path from "path";
import { ROOT_FOLDER_PATH } from "../../../constants/paths";
@@ -12,6 +14,10 @@ const EMBEDDING_SDK_DIST_PATH = path.join(
const LOCAL_DIST_PATH = "./local-dist";
// Keep in sync with the H2 version pinned in the root deps.edn (com.h2database/h2).
const H2_VERSION = "2.1.214";
const H2_JAR_URL = `https://repo1.maven.org/maven2/com/h2database/h2/${H2_VERSION}/h2-${H2_VERSION}.jar`;
export function copyExampleEnvFile({
rootPath,
dockerEnvExamplePath,
@@ -33,6 +39,52 @@ export function copyLocalMetabaseJar(rootPath: string) {
fs.cpSync(METABASE_JAR_DIST_PATH, destinationPath, { recursive: true });
}
// EE uberjars don't bundle the H2 driver (it's OSS-only; EE expects users to supply it), but the sample apps boot
// Metabase with its default H2 application database. The container launches via the official image's `java -jar`,
// which ignores `-cp`, so we fold the H2 classes directly into the copied metabase.jar.
//
// H2's own META-INF is dropped so its manifest can't clobber the uberjar's (Main-Class) and its signatures don't
// trip jar verification -- but we then MERGE `org.h2.Driver` into the uberjar's existing
// `META-INF/services/java.sql.Driver`. That service registration is required, not cosmetic: the H2 application-DB
// connection is acquired on a c3p0 pool thread, and `java.sql.DriverManager` only hands out drivers whose class is
// visible to the *caller's* classloader. H2 registered lazily by `metabase.driver.h2` (Clojure's dynamic
// classloader) is invisible there, so the pool silently fails to acquire a connection and Metabase aborts boot with
// a 30s checkout timeout. A services entry registers H2 under the system classloader, which the pool thread can see.
// We append (not overwrite) so the uberjar's other bundled driver registrations survive.
//
// Uses curl + zip (no JDK/clojure) because the sample-app CI job doesn't set up a backend toolchain.
export function bundleH2DriverIntoJar(rootPath: string) {
const jarPath = path.join(rootPath, LOCAL_DIST_PATH, "metabase.jar");
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "h2-bundle-"));
const h2Jar = path.join(workDir, "h2.jar");
const h2Contents = path.join(workDir, "contents");
const svcRoot = path.join(workDir, "svc");
const svcRelPath = "META-INF/services/java.sql.Driver";
try {
execSync(`curl -fsSL -o "${h2Jar}" "${H2_JAR_URL}"`, { stdio: "inherit" });
fs.mkdirSync(h2Contents);
execSync(`unzip -oq "${h2Jar}" -d "${h2Contents}"`, { stdio: "inherit" });
fs.rmSync(path.join(h2Contents, "META-INF"), {
recursive: true,
force: true,
});
execSync(`cd "${h2Contents}" && zip -rgq "${jarPath}" .`, {
stdio: "inherit",
});
const svcFile = path.join(svcRoot, svcRelPath);
fs.mkdirSync(path.dirname(svcFile), { recursive: true });
const existing = execSync(`unzip -p "${jarPath}" ${svcRelPath}`).toString();
fs.writeFileSync(svcFile, `${existing}\norg.h2.Driver\n`);
execSync(`cd "${svcRoot}" && zip -gq "${jarPath}" ${svcRelPath}`, {
stdio: "inherit",
});
} finally {
fs.rmSync(workDir, { recursive: true, force: true });
}
}
export function copyLocalEmbeddingSdkPackage(rootPath: string) {
const destinationPath = path.join(rootPath, LOCAL_DIST_PATH, "embedding-sdk");

View File

@@ -6,6 +6,7 @@ import { SAMPLE_APP_SETUP_CONFIGS } from "./constants/sample-app-setup-configs";
import { copyShoppyMetabaseAppDBDump } from "./helpers/copy-shoppy-metabase-app-db-dump";
import { fetchApp } from "./helpers/fetch-app";
import {
bundleH2DriverIntoJar,
copyExampleEnvFile,
copyLocalEmbeddingSdkPackage,
copyLocalMetabaseJar,
@@ -60,6 +61,8 @@ export async function startSampleAppContainers(
copyLocalMetabaseJar(rootPath);
bundleH2DriverIntoJar(rootPath);
if (embeddingSdkVersion === "local") {
copyLocalEmbeddingSdkPackage(rootPath);
}

View File

@@ -12,6 +12,7 @@
metabase/clickhouse {:local/root "clickhouse"}
metabase/databricks {:local/root "databricks"}
metabase/druid-jdbc {:local/root "druid-jdbc"}
metabase/h2 {:local/root "h2"}
metabase/hive-like {:local/root "hive-like"}
metabase/mongo {:local/root "mongo"}
metabase/oracle {:local/root "oracle"}

View File

@@ -0,0 +1 @@
{:paths ["src"]}

View File

@@ -5,6 +5,7 @@
[clojure.math.combinatorics :as math.combo]
[clojure.string :as str]
[java-time.api :as t]
[metabase.app-db.query-cancelation :as app-db.query-cancelation]
[metabase.config.core :as config]
[metabase.driver :as driver]
[metabase.driver-api.core :as driver-api]
@@ -25,6 +26,7 @@
[metabase.driver.util :as driver.u]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.lib.schema.metadata :as lib.schema.metadata]
[metabase.query-processor.middleware.cache-backend.db :as cache-backend.db]
[metabase.util :as u]
[metabase.util.honey-sql-2 :as h2x]
[metabase.util.i18n :refer [deferred-tru tru]]
@@ -34,8 +36,10 @@
(:import
(java.sql Clob Connection ResultSet ResultSetMetaData SQLException Statement Types)
(java.time OffsetTime)
(org.h2.api ErrorCode)
(org.h2.command CommandInterface Parser)
(org.h2.engine SessionLocal)
(org.h2.jdbc JdbcBlob JdbcClob)
(org.h2.util StringUtils)))
(set! *warn-on-reflection* true)
@@ -48,6 +52,34 @@
;;; this will prevent the H2 driver from showing up in the list of options when adding a new Database.
(defmethod driver/superseded-by :h2 [_driver] :deprecated)
;;; --------------------------------------- H2 app-database support glue ----------------------------------------------
;;;
;;; The registrations below name H2-specific JDBC classes (`org.h2.jdbc.JdbcClob`/`JdbcBlob`, `org.h2.api.ErrorCode`),
;;; so they MUST live here in [[metabase.driver.h2]] rather than in the core app-db/query-processor namespaces that own
;;; these multimethods. H2 is an optional driver: this namespace ships as *source* and is compiled lazily at runtime
;;; only when the H2 JAR is present (see [[metabase.driver.init]] and `metabase.config.core/h2-available?`), whereas
;;; the core namespaces are AOT-compiled into the uberjar where H2 may be absent.
;; Reading H2 CLOB/BLOB columns out of the *application* database (e.g. cached query results, serialized values).
(extend-protocol jdbc/IResultSetReadColumn
JdbcClob
(result-set-read-column [clob _ _]
(driver-api/clob->str clob))
JdbcBlob
(result-set-read-column [^JdbcBlob blob _ _]
(.getBytes blob 0 (.length blob))))
;; The query cache stores results as an H2 BLOB when H2 is the app DB; convert it back to a byte array.
(defmethod cache-backend.db/results-as-bytes JdbcBlob
[{:keys [^JdbcBlob results]}]
(.getBytes results 1 (.length results)))
;; Recognize H2's "statement was canceled" error so query cancelation works when H2 is the app DB.
(defmethod app-db.query-cancelation/query-canceled-exception?* :h2
[_db-type ^SQLException e]
(= (.getErrorCode e) ErrorCode/STATEMENT_WAS_CANCELED))
(defn- get-field
"Returns value of private field. This function is used to bypass field protection to instantiate
a low-level H2 Parser object in order to detect DDL statements in queries."

View File

@@ -77,7 +77,11 @@
(defn clob->str
"Convert an H2 clob to a String."
^String [^org.h2.jdbc.JdbcClob clob]
;; hint as the standard `java.sql.Clob` interface (which H2's `JdbcClob` implements) rather than the concrete
;; `org.h2.jdbc.JdbcClob` class. H2 is an optional driver, so even though this fn is only ever reached for H2 clobs,
;; avoiding the `org.h2.*` type hint keeps the namespace loadable when H2 is absent. See
;; [[metabase.config.core/h2-available?]].
^String [^java.sql.Clob clob]
(when clob
(letfn [(->str [^BufferedReader buffered-reader]
(loop [acc []]
@@ -93,15 +97,12 @@
(extend-protocol jdbc/IResultSetReadColumn
org.postgresql.util.PGobject
(result-set-read-column [clob _ _]
(.getValue clob))
(.getValue clob)))
org.h2.jdbc.JdbcClob
(result-set-read-column [clob _ _]
(clob->str clob))
org.h2.jdbc.JdbcBlob
(result-set-read-column [^org.h2.jdbc.JdbcBlob blob _ _]
(.getBytes blob 0 (.length blob))))
;; NOTE: H2 returns CLOB/BLOB columns as `org.h2.jdbc.JdbcClob`/`JdbcBlob`. The `IResultSetReadColumn` extensions for
;; those (which call [[clob->str]]) live in `metabase.driver.h2` rather than here -- H2 is an optional driver, so any
;; code that names `org.h2.*` classes must stay out of core (AOT-compiled) namespaces and ship as source. See
;; `metabase.config.core/h2-available?`.
(defmulti ^:private read-column
{:arglists '([rs rsmeta i])}

View File

@@ -2,15 +2,16 @@
(set! *warn-on-reflection* true)
(defmulti ^:private query-canceled-exception?*
;; NOTE: not private -- the `:h2` method is registered from `metabase.driver.h2`, since it references
;; `org.h2.api.ErrorCode` and H2 is an optional driver whose classes must not appear in core (AOT-compiled)
;; namespaces. See `metabase.config.core/h2-available?`.
(defmulti query-canceled-exception?*
"Whether `e` (a `java.sql.SQLException`) represents a query cancelation for app-db type `db-type`, dispatching on
`(keyword db-type)`. The `:h2` method is registered from `metabase.driver.h2` (see the note above)."
{:arglists '([db-type ^java.sql.SQLException e])}
(fn [db-type _e]
(keyword db-type)))
(defmethod query-canceled-exception?* :h2
[_db-type ^java.sql.SQLException e]
(= (.getErrorCode e) org.h2.api.ErrorCode/STATEMENT_WAS_CANCELED))
(defn- sql-state
[^java.sql.SQLException e]
(loop [exception e]

View File

@@ -120,6 +120,36 @@
(getResource ".keep-me")
getProtocol)))
;;; H2 driver availability
;;;
;;; H2 is an *optional* driver: the OSS build bundles the H2 JDBC JAR, but the EE build does not (per licensing, and
;;; because SQLite -- not H2 -- is the sample database). EE users who want H2 (e.g. for a quick local trial, or to run
;;; the dump-to-h2/load-from-h2 tasks) must supply the H2 JAR themselves, either by dropping it in the plugins
;;; directory or putting it on the classpath explicitly.
;;;
;;; Consequences for the code base:
;;;
;;; 1. No *core* namespace (anything AOT-compiled into the uberjar) may reference an `org.h2.*` class at load/compile
;;; time -- doing so would fail the EE build (H2 absent at build time) and `NoClassDefFoundError` the app on boot.
;;; All such code lives in [[metabase.driver.h2]] (and namespaces it requires), which ships as *source* and is
;;; compiled lazily at runtime, only when H2 is actually present -- exactly like the `oracle`/`vertica` drivers.
;;;
;;; 2. The decision of whether to load [[metabase.driver.h2]] must be made at *runtime* via [[h2-available?]], NOT at
;;; macroexpansion/compile time. An EE user can add the H2 JAR after the jar was built (via `-cp` or the plugins
;;; dir), so the check has to run when the app boots, not when it was compiled.
(def ^:private h2-driver-class-name "org.h2.Driver")
(defn h2-available?
"True iff the H2 JDBC driver (`org.h2.Driver`) is on the classpath right now. H2 is bundled in OSS builds but not EE
builds; EE users must supply the H2 JAR themselves (see the comment above). Checked at runtime against the current
thread's context classloader so that JARs added after build time (via `-cp` or the plugins directory) are seen."
[]
(try
(some? (Class/forName h2-driver-class-name false (.getContextClassLoader (Thread/currentThread))))
(catch Throwable _
false)))
;;; Version stuff
(defn- version-info-from-properties-file []

View File

@@ -13,7 +13,6 @@
[metabase.core.config-from-file :as config-from-file]
[metabase.core.init]
[metabase.core.perf :as perf]
[metabase.driver.h2]
[metabase.driver.mysql]
[metabase.driver.postgres]
[metabase.embedding.settings :as embed.settings]
@@ -46,8 +45,9 @@
(comment
metabase.core.init/keep-me
;; Load up the drivers shipped as part of the main codebase, so they will show up in the list of available DB types
metabase.driver.h2/keep-me
;; Load up the drivers shipped as part of the main codebase, so they will show up in the list of available DB types.
;; H2 is *not* here -- it's an optional driver loaded conditionally in [[metabase.driver.init]] when its JAR is on the
;; classpath. See [[metabase.config.core/h2-available?]].
metabase.driver.mysql/keep-me
metabase.driver.postgres/keep-me
;; Make sure the custom Metabase logger code gets loaded up so we use our custom logger for performance reasons.

View File

@@ -3,15 +3,24 @@
See https://metaboat.slack.com/archives/CKZEMT1MJ/p1736556522733279 for rationale behind this pattern."
(:require
[metabase.config.core :as config]
;; for the default [[metabase.driver/table-rows-sample]] implementation
[metabase.driver.common.table-rows-sample]
[metabase.driver.events.driver-notifications]
[metabase.driver.events.report-timezone-updated]
;; Load up the drivers shipped as part of the main codebase, so they will show up in the list of available DB types
[metabase.driver.h2]
[metabase.driver.mysql]
[metabase.driver.postgres]
[metabase.driver.settings]
;; for the `:sql-jdbc` implementation of [[metabase.driver/incorporate-ssh-tunnel-details]]
[metabase.driver.sql-jdbc.connection.ssh-tunnel]
[metabase.driver.sqlite]))
;; H2 is an *optional* driver: the OSS build bundles the H2 JDBC JAR but the EE build does not. Load
;; [[metabase.driver.h2]] (so H2 shows up as an available DB type, and so its app-db/cache/search glue gets
;; registered) only when the H2 JAR is on the classpath. That namespace `:import`s `org.h2.*` classes, so it ships as
;; *source* and is compiled here lazily, at runtime -- requiring it unconditionally (or AOT-compiling it) would break
;; boot when H2 is absent. The check is at *runtime*, not compile time, so EE users who supply the H2 JAR after the
;; jar was built (via the plugins dir or `-cp`) still get H2 support. See [[metabase.config.core/h2-available?]].
(when (config/h2-available?)
(require 'metabase.driver.h2))

View File

@@ -25,10 +25,9 @@
{:arglists '([cached])}
(comp type :results))
;; H2 returns a blob type
(defmethod results-as-bytes org.h2.jdbc.JdbcBlob
[{:keys [^org.h2.jdbc.JdbcBlob results]}]
(.getBytes results 1 (.length results)))
;; NOTE: H2 returns a blob (`org.h2.jdbc.JdbcBlob`); the `results-as-bytes` method for that dispatch value is
;; registered from `metabase.driver.h2`, since H2 is an optional driver whose classes must not appear in core
;; (AOT-compiled) namespaces. See `metabase.config.core/h2-available?`.
;; MySQL/Mariadb/Postgresql return a byte array
(defmethod results-as-bytes :default

View File

@@ -22,7 +22,6 @@
[metabase.util.string :as string]
[toucan2.core :as t2])
(:import
(org.h2.jdbc JdbcSQLSyntaxErrorException)
(org.postgresql.util PSQLException)))
(comment
@@ -284,11 +283,19 @@
(dissoc :native_query)
(merge (specialization/extra-entry-fields entity)))))
(defn- h2-syntax-error?
"Whether `e` is an H2 `org.h2.jdbc.JdbcSQLSyntaxErrorException`, matched by class name rather than `instance?`. H2 is
an optional driver, so we can't reference its class here (it may be absent from the classpath); a name check keeps
this namespace loadable without H2 while preserving the original H2-only matching. See
[[metabase.config.core/h2-available?]]."
[e]
(= "org.h2.jdbc.JdbcSQLSyntaxErrorException" (some-> e class .getName)))
(defn- table-not-found-exception? [e]
;; Use with care, obviously this can give false positives if used with a query that's *actually* malformed.
;; TODO we should handle the MySQL and MariaDB flavors here too
(or (instance? PSQLException (ex-cause e))
(instance? JdbcSQLSyntaxErrorException (ex-cause e))))
(h2-syntax-error? (ex-cause e))))
(defn- retry-upsert-ex [table-type table-name-before table-name-after e-before e-after]
(ex-info "Failed retrying search index batch upsert"