Skip to content

Commit 51d9edd

Browse files
committed
Initial import
0 parents  commit 51d9edd

File tree

6 files changed

+391
-0
lines changed

6 files changed

+391
-0
lines changed

.gitignore

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
\#*\#
2+
.\#*
3+
/target
4+
/.nrepl-port
5+
6+
### Intellij ###
7+
8+
# User-specific stuff
9+
.idea/**/workspace.xml
10+
.idea/**/tasks.xml
11+
.idea/**/usage.statistics.xml
12+
.idea/**/dictionaries
13+
.idea/**/shelf
14+
15+
# Generated files
16+
.idea/**/contentModel.xml
17+
18+
# Sensitive or high-churn files
19+
.idea/**/dataSources/
20+
.idea/**/dataSources.ids
21+
.idea/**/dataSources.local.xml
22+
.idea/**/sqlDataSources.xml
23+
.idea/**/dynamic.xml
24+
.idea/**/uiDesigner.xml
25+
.idea/**/dbnavigator.xml

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Firebird driver for metabase
2+
3+
This driver enables metabase to connect to FirebirdSQL databases.

project.clj

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
(defproject evosec/firebird-driver "1.0.0"
2+
:min-lein-version "2.5.0"
3+
4+
:dependencies
5+
[[org.firebirdsql.jdbc/jaybird-jdk18 "3.0.5"]]
6+
7+
:profiles
8+
{:provided
9+
{:dependencies
10+
[[org.clojure/clojure "1.10.0"]
11+
[metabase-core "1.0.0-SNAPSHOT"]]}
12+
13+
:uberjar
14+
{:auto-cleam true
15+
:aot :all
16+
:omit-source true
17+
:javac-options ["-target" "1.8", "-source" "1.8"]
18+
:target-path "target/%s"
19+
:uberjar-name "firebird.metabase-driver.jar"}})

resources/metabase-plugin.yaml

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
info:
2+
name: Metabase FirebirdSQL Driver
3+
version: 1.0.0
4+
description: Allows Metabase to connect to FirebirdSQL databases.
5+
driver:
6+
name: firebird
7+
display-name: FirebirdSQL
8+
lazy-load: true
9+
parent: sql-jdbc
10+
connection-properties:
11+
- host
12+
- merge:
13+
- port
14+
- default: 3050
15+
- merge:
16+
- dbname
17+
- name: db
18+
placeholder: BirdsOfTheWorld
19+
- user
20+
- password
21+
- merge:
22+
- additional-options
23+
- placeholder: "blobBufferSize=2048"
24+
connection-properties-include-tunnel-config: false
25+
init:
26+
- step: load-namespace
27+
namespace: metabase.driver.firebird
28+
- step: register-jdbc-driver
29+
class: org.firebirdsql.jdbc.FBDriver

src/metabase/driver/firebird.clj

+203
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
(ns metabase.driver.firebird
2+
(:require [clojure
3+
[set :as set]
4+
[string :as str]]
5+
[clojure.java.jdbc :as jdbc]
6+
[honeysql.core :as hsql]
7+
[metabase.driver :as driver]
8+
[metabase.driver.common :as driver.common]
9+
[metabase.driver.sql-jdbc
10+
[common :as sql-jdbc.common]
11+
[connection :as sql-jdbc.conn]
12+
[sync :as sql-jdbc.sync]]
13+
[metabase.driver.sql.query-processor :as sql.qp]
14+
[metabase.util
15+
[honeysql-extensions :as hx]
16+
[ssh :as ssh]])
17+
(:import [java.sql DatabaseMetaData Time]))
18+
19+
(driver/register! :firebird, :parent :sql-jdbc)
20+
21+
(defn- firebird->spec
22+
"Create a database specification for a FirebirdSQL database."
23+
[{:keys [host port db jdbc-flags]
24+
:or {host "localhost", port 3050, db "", jdbc-flags ""}
25+
:as opts}]
26+
(merge {:classname "org.firebirdsql.jdbc.FBDriver"
27+
:subprotocol "firebirdsql"
28+
:subname (str "//" host ":" port "/" db jdbc-flags)}
29+
(dissoc opts :host :port :db :jdbc-flags)))
30+
31+
;; Obtain connection properties for connection to a Firebird database.
32+
(defmethod sql-jdbc.conn/connection-details->spec :firebird [_ details]
33+
(-> details
34+
(update :port (fn [port]
35+
(if (string? port)
36+
(Integer/parseInt port)
37+
port)))
38+
(set/rename-keys {:dbname :db})
39+
firebird->spec
40+
(sql-jdbc.common/handle-additional-options details)))
41+
42+
;; Use "SELECT 1 FROM RDB$DATABASE" instead of "SELECT 1"
43+
(defmethod driver/can-connect? :firebird [driver details]
44+
(let [connection (sql-jdbc.conn/connection-details->spec driver (ssh/include-ssh-tunnel details))]
45+
(= 1 (first (vals (first (jdbc/query connection ["SELECT 1 FROM RDB$DATABASE"])))))))
46+
47+
;; Use pattern matching because some parameters can have a length parameter, e.g. VARCHAR(255)
48+
(def ^:private database-type->base-type
49+
(sql-jdbc.sync/pattern-based-database-type->base-type
50+
[[#"INT64" :type/BigInteger]
51+
[#"DECIMAL" :type/Decimal]
52+
[#"FLOAT" :type/Float]
53+
[#"BLOB" :type/*]
54+
[#"INTEGER" :type/Integer]
55+
[#"NUMERIC" :type/Decimal]
56+
[#"DOUBLE" :type/Float]
57+
[#"SMALLINT" :type/Integer]
58+
[#"CHAR" :type/Text]
59+
[#"BIGINT" :type/BigInteger]
60+
[#"TIMESTAMP" :type/DateTime]
61+
[#"DATE" :type/Date]
62+
[#"TIME" :type/Time]
63+
[#"BLOB SUB_TYPE 0" :type/*]
64+
[#"BLOB SUB_TYPE 1" :type/Text]
65+
[#"DOUBLE PRECISION" :type/Float]
66+
[#"BOOLEAN" :type/Boolean]]))
67+
68+
;; Map Firebird data types to base types
69+
(defmethod sql-jdbc.sync/database-type->base-type :firebird [_ database-type]
70+
(database-type->base-type database-type))
71+
72+
;; Use "FIRST" instead of "LIMIT"
73+
(defmethod sql.qp/apply-top-level-clause [:firebird :limit] [_ _ honeysql-form {value :limit}]
74+
(assoc honeysql-form :modifiers [(format "FIRST %d" value)]))
75+
76+
;; Use "SKIP" instead of "OFFSET"
77+
(defmethod sql.qp/apply-top-level-clause [:firebird :page] [_ _ honeysql-form {{:keys [items page]} :page}]
78+
(assoc honeysql-form :modifiers [(format "FIRST %d SKIP %d"
79+
items
80+
(* items (dec page)))]))
81+
82+
;; Firebird stores table names as CHAR(31), so names with < 31 characters get padded with spaces.
83+
;; This confuses everyone, including metabase, so we trim the table names here
84+
(defn post-filtered-trimmed-active-tables
85+
"Alternative implementation of `ISQLDriver/active-tables` best suited for DBs with little or no support for schemas.
86+
Fetch *all* Tables, then filter out ones whose schema is in `excluded-schemas` Clojure-side."
87+
[driver, ^DatabaseMetaData metadata, & [db-name-or-nil]]
88+
(set (for [table (sql-jdbc.sync/post-filtered-active-tables driver metadata db-name-or-nil)]
89+
{:name (str/trim (:name table))
90+
:description (:description table)
91+
:schema (:schema table)})))
92+
93+
(defmethod sql-jdbc.sync/active-tables :firebird [& args]
94+
(apply post-filtered-trimmed-active-tables args))
95+
96+
;; Convert unix time to a timestamp
97+
(defmethod sql.qp/unix-timestamp->timestamp [:firebird :seconds] [_ _ expr]
98+
(hsql/call :DATEADD (hsql/raw "SECOND") expr (hx/cast :TIMESTAMP (hx/literal "01-01-1970 00:00:00"))))
99+
100+
;; Helpers for Date extraction
101+
;; TODO: This can probably simplified a lot by using String concentation instead of
102+
;; replacing parts of the format recursively
103+
104+
;; Specifies what Substring to replace for a given time unit
105+
(defn- get-unit-placeholder [unit]
106+
(case unit
107+
:SECOND :ss
108+
:MINUTE :mm
109+
:HOUR :hh
110+
:DAY :DD
111+
:MONTH :MM
112+
:YEAR :YYYY))
113+
114+
(defn- get-unit-name [unit]
115+
(case unit
116+
0 :SECOND
117+
1 :MINUTE
118+
2 :HOUR
119+
3 :DAY
120+
4 :MONTH
121+
5 :YEAR))
122+
;; Replace the specified part of the timestamp
123+
(defn- replace-timestamp-part [input unit expr]
124+
(hsql/call :replace input (hx/literal (get-unit-placeholder unit)) (hsql/call :extract unit expr)))
125+
126+
(defn- format-step [expr input step wanted-unit]
127+
(if (> step wanted-unit)
128+
(format-step expr (replace-timestamp-part input (get-unit-name step) expr) (- step 1) wanted-unit)
129+
(replace-timestamp-part input (get-unit-name step) expr)))
130+
131+
(defn- format-timestamp [expr format-template wanted-unit]
132+
(format-step expr (hx/literal format-template) 5 wanted-unit))
133+
134+
;; Firebird doesn't have a date_trunc function, so use a workaround: First format the timestamp to a
135+
;; string of the wanted resulution, then convert it back to a timestamp
136+
(defn- timestamp-trunc [expr format-str wanted-unit]
137+
(hx/cast :TIMESTAMP (format-timestamp expr format-str wanted-unit)))
138+
139+
(defn- date-trunc [expr format-str wanted-unit]
140+
(hx/cast :DATE (format-timestamp expr format-str wanted-unit)))
141+
142+
(defmethod sql.qp/date [:firebird :default] [_ _ expr] expr)
143+
;; Cast to TIMESTAMP if we need minutes or hours, since expr might be a DATE
144+
(defmethod sql.qp/date [:firebird :minute] [_ _ expr] (timestamp-trunc (hx/cast :TIMESTAMP expr) "YYYY-MM-DD hh:mm:00" 1))
145+
(defmethod sql.qp/date [:firebird :minute-of-hour] [_ _ expr] (hsql/call :extract :MINUTE (hx/cast :TIMESTAMP expr)))
146+
(defmethod sql.qp/date [:firebird :hour] [_ _ expr] (timestamp-trunc (hx/cast :TIMESTAMP expr) "YYYY-MM-DD hh:00:00" 2))
147+
(defmethod sql.qp/date [:firebird :hour-of-day] [_ _ expr] (hsql/call :extract :HOUR (hx/cast :TIMESTAMP expr)))
148+
;; Cast to DATE to get rid of anything smaller than day
149+
(defmethod sql.qp/date [:firebird :day] [_ _ expr] (hx/cast :DATE expr))
150+
;; Firebird DOW is 0 (Sun) - 6 (Sat); increment this to be consistent with Java, H2, MySQL, and Mongo (1-7)
151+
(defmethod sql.qp/date [:firebird :day-of-week] [_ _ expr] (hx/+ (hsql/call :extract :WEEKDAY (hx/cast :DATE expr)) 1))
152+
(defmethod sql.qp/date [:firebird :day-of-month] [_ _ expr] (hsql/call :extract :DAY expr))
153+
;; Firebird YEARDAY starts from 0; increment this
154+
(defmethod sql.qp/date [:firebird :day-of-year] [_ _ expr] (hx/+ (hsql/call :extract :YEARDAY expr) 1))
155+
;; Cast to DATE because we do not want units smaller than days
156+
;; Use hsql/raw for DAY in dateadd because the keyword :WEEK gets surrounded with quotations
157+
(defmethod sql.qp/date [:firebird :week] [_ _ expr] (hsql/call :dateadd (hsql/raw "DAY") (hx/- 0 (hsql/call :extract :WEEKDAY (hx/cast :DATE expr))) (hx/cast :DATE expr)))
158+
(defmethod sql.qp/date [:firebird :week-of-year] [_ _ expr] (hsql/call :extract :WEEK expr))
159+
(defmethod sql.qp/date [:firebird :month] [_ _ expr] (date-trunc expr "YYYY-MM-01" 4))
160+
(defmethod sql.qp/date [:firebird :month-of-year] [_ _ expr] (hsql/call :extract :MONTH expr))
161+
;; Use hsql/raw for MONTH in dateadd because the keyword :MONTH gets surrounded with quotations
162+
(defmethod sql.qp/date [:firebird :quarter] [_ _ expr] (hsql/call :dateadd (hsql/raw "MONTH") (hx/* (hx// (hx/- (hsql/call :extract :MONTH expr) 1) 3) 3) (date-trunc expr "YYYY-01-01" 5)))
163+
(defmethod sql.qp/date [:firebird :quarter-of-year] [_ _ expr] (hx/+ (hx// (hx/- (hsql/call :extract :MONTH expr) 1) 3) 1))
164+
(defmethod sql.qp/date [:firebird :year] [_ _ expr] (hsql/call :extract :YEAR expr))
165+
166+
(defmethod driver/date-interval :firebird [driver unit amount]
167+
(if (= unit :quarter)
168+
(recur driver :month (hx/* amount 3))
169+
(hsql/call :dateadd (hsql/raw (name unit)) amount (hx/cast :timestamp (hx/literal :now)))))
170+
171+
(defmethod sql.qp/current-datetime-fn :firebird [_]
172+
(hx/cast :timestamp (hx/literal :now)))
173+
174+
(defmethod driver.common/current-db-time-date-formatters :firebird [_]
175+
(driver.common/create-db-time-formatters "yyyy-MM-dd HH:mm:ss.SSSS"))
176+
177+
(defmethod driver.common/current-db-time-native-query :firebird [_]
178+
"SELECT CAST(CAST('Now' AS TIMESTAMP) AS VARCHAR(24)) FROM RDB$DATABASE")
179+
180+
(defmethod driver/current-db-time :firebird [& args]
181+
(apply driver.common/current-db-time args))
182+
183+
(defmethod sql.qp/->honeysql [:firebird :stddev]
184+
[driver [_ field]]
185+
(hsql/call :stddev_samp (sql.qp/->honeysql driver field)))
186+
187+
(defmethod driver/supports? [:firebird :basic-aggregations] [_ _] true)
188+
189+
(defmethod driver/supports? [:firebird :expression-aggregations] [_ _] true)
190+
191+
(defmethod driver/supports? [:firebird :standard-deviation-aggregations] [_ _] true)
192+
193+
(defmethod driver/supports? [:firebird :foreign-keys] [_ _] true)
194+
195+
(defmethod driver/supports? [:firebird :nested-fields] [_ _] false)
196+
197+
(defmethod driver/supports? [:firebird :set-timezone] [_ _] false)
198+
199+
(defmethod driver/supports? [:firebird :nested-queries] [_ _] false)
200+
201+
(defmethod driver/supports? [:firebird :binning] [_ _] false)
202+
203+
(defmethod driver/supports? [:firebird :case-sensitivity-string-filter-options] [_ _] false)

test/metabase/test/data/firebird.clj

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
(ns metabase.test.data.firebird
2+
"Code for creating / destroying a FirebirdSQL database from a `DatabaseDefinition`."
3+
(:require [clojure.string :as str]
4+
[clojure.java.jdbc :as jdbc]
5+
[metabase.driver.sql-jdbc.connection :as sql-jdbc.conn]
6+
[metabase.test.data
7+
[interface :as tx]
8+
[sql :as sql.tx]
9+
[sql-jdbc :as sql-jdbc.tx]]
10+
[metabase.test.data.sql-jdbc
11+
[load-data :as load-data]
12+
[execute :as execute]]
13+
[metabase.util :as u]))
14+
15+
(sql-jdbc.tx/add-test-extensions! :firebird)
16+
17+
(defmethod tx/dbdef->connection-details :firebird [_ context {:keys [database-name]}]
18+
{:host (tx/db-test-env-var-or-throw :firebird :host "localhost")
19+
:port (tx/db-test-env-var-or-throw :firebird :port 3050)
20+
:user (tx/db-test-env-var-or-throw :firebird :user "SYSDBA")
21+
:password (tx/db-test-env-var-or-throw :firebird :password "masterkey")
22+
:db (tx/db-test-env-var-or-throw :firebird :db "metabase-testing")
23+
:additional-options (tx/db-test-env-var-or-throw :firebird :additional-options "charSet=UTF-8")})
24+
25+
(defmethod sql.tx/field-base-type->sql-type [:firebird :type/BigInteger] [_ _] "BIGINT")
26+
;; Boolean was added in firebird 3, maybe use smallint for firebird 2 compatibility?
27+
(defmethod sql.tx/field-base-type->sql-type [:firebird :type/Boolean] [_ _] "BOOLEAN")
28+
(defmethod sql.tx/field-base-type->sql-type [:firebird :type/Date] [_ _] "DATE")
29+
(defmethod sql.tx/field-base-type->sql-type [:firebird :type/DateTime] [_ _] "TIMESTAMP")
30+
(defmethod sql.tx/field-base-type->sql-type [:firebird :type/Decimal] [_ _] "DECIMAL")
31+
(defmethod sql.tx/field-base-type->sql-type [:firebird :type/Float] [_ _] "FLOAT")
32+
(defmethod sql.tx/field-base-type->sql-type [:firebird :type/Integer] [_ _] "INTEGER")
33+
(defmethod sql.tx/field-base-type->sql-type [:firebird :type/Text] [_ _] "VARCHAR(255)")
34+
(defmethod sql.tx/field-base-type->sql-type [:firebird :type/Time] [_ _] "TIME")
35+
36+
(defmethod sql.tx/pk-sql-type :firebird [_] "INTEGER GENERATED BY DEFAULT AS IDENTITY")
37+
38+
(defmethod sql.tx/pk-field-name :firebird [_] "id")
39+
40+
;; Use RECREATE TABLE to drop a table if it exists, in case some tables have not been dropped before
41+
;; running tests
42+
(defmethod sql.tx/create-table-sql :firebird
43+
[driver {:keys [database-name], :as dbdef} {:keys [table-name field-definitions table-comment]}]
44+
(let [pk-field-name (sql.tx/pk-field-name driver)]
45+
(format "RECREATE TABLE %s (\"%s\" %s PRIMARY KEY, %s)"
46+
(sql.tx/qualify+quote-name driver database-name table-name)
47+
pk-field-name
48+
(sql.tx/pk-sql-type driver)
49+
(->> field-definitions
50+
(map (fn [{:keys [field-name base-type field-comment]}]
51+
(format "\"%s\" %s"
52+
field-name
53+
(if (map? base-type)
54+
(:native base-type)
55+
(sql.tx/field-base-type->sql-type driver base-type)))))
56+
(interpose ", ")
57+
(apply str)))))
58+
59+
(defmethod sql.tx/drop-table-if-exists-sql :firebird [& _] nil)
60+
61+
(defmethod load-data/load-data! :firebird [& args]
62+
(apply load-data/load-data-one-at-a-time! args))
63+
64+
(defmethod execute/execute-sql! :firebird [& args]
65+
(apply execute/sequentially-execute-sql! args))
66+
67+
;; Firebird cannot create or drop databases on runtime
68+
(defmethod sql.tx/create-db-sql :firebird [& _] nil)
69+
(defmethod sql.tx/drop-db-if-exists-sql :firebird [& _] nil)
70+
71+
(defmethod sql.tx/qualified-name-components :firebird
72+
([_ db-name] [db-name])
73+
([_ db-name table-name] [(tx/db-qualified-table-name db-name table-name)])
74+
([_ db-name table-name field-name] [(tx/db-qualified-table-name db-name table-name) field-name]))
75+
76+
;; Drop all tables that are not system tables before running test
77+
(defmethod tx/before-run :firebird [_]
78+
(let [connection-spec (sql-jdbc.conn/connection-details->spec :firebird
79+
(tx/dbdef->connection-details :firebird :server nil))
80+
foreign-keys (jdbc/query
81+
connection-spec
82+
(str "select r.rdb$relation_name, r.rdb$constraint_name from rdb$relation_constraints r where (r.rdb$constraint_type='FOREIGN KEY')"))
83+
leftover-tables (map :rdb$relation_name (jdbc/query
84+
connection-spec
85+
(str "SELECT RDB$RELATION_NAME "
86+
"FROM RDB$RELATIONS "
87+
"WHERE RDB$VIEW_BLR IS NULL "
88+
"AND (RDB$SYSTEM_FLAG IS NULL OR RDB$SYSTEM_FLAG = 0);")))]
89+
;; First, kill all connections and statements that are still running
90+
(println "Killing all open connections to test db... ")
91+
(jdbc/execute! connection-spec ["DELETE FROM MON$ATTACHMENTS WHERE MON$ATTACHMENT_ID <> CURRENT_CONNECTION"])
92+
(println "[ok]")
93+
(println "Killing all running statements in test db... ")
94+
(jdbc/execute! connection-spec ["DELETE FROM MON$STATEMENTS WHERE MON$ATTACHMENT_ID <> CURRENT_CONNECTION"])
95+
(println "[ok]")
96+
;; Second, remove all foreign keys, so tables can be properly dropped
97+
(doseq [constraint foreign-keys]
98+
(u/ignore-exceptions
99+
(printf "Dropping constraint \"%s\" on table \"%s\"...\n"
100+
(:rdb$constraint_name constraint)
101+
(:rdb$relation_name constraint))
102+
(jdbc/execute! connection-spec [(format "ALTER TABLE \"%s\" DROP CONSTRAINT \"%s\";"
103+
(:rdb$relation_name constraint)
104+
(:rdb$constraint_name constraint))])
105+
(println "[ok]")))
106+
;; Third, drop all leftover tables
107+
(doseq [table leftover-tables]
108+
(u/ignore-exceptions
109+
(printf "Dropping leftover Firebird table \"%s\"...\n" (str/trimr table))
110+
(jdbc/execute! connection-spec [(format "DROP TABLE \"%s\";" (str/trimr table))])
111+
(println "[ok]")))
112+
(println "Destroyed all leftover tables.")))

0 commit comments

Comments
 (0)