Skip to content

Commit

Permalink
Initial import
Browse files Browse the repository at this point in the history
  • Loading branch information
Nikos410 committed Mar 12, 2019
0 parents commit 51d9edd
Show file tree
Hide file tree
Showing 6 changed files with 391 additions and 0 deletions.
25 changes: 25 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
\#*\#
.\#*
/target
/.nrepl-port

### Intellij ###

# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf

# Generated files
.idea/**/contentModel.xml

# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Firebird driver for metabase

This driver enables metabase to connect to FirebirdSQL databases.
19 changes: 19 additions & 0 deletions project.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
(defproject evosec/firebird-driver "1.0.0"
:min-lein-version "2.5.0"

:dependencies
[[org.firebirdsql.jdbc/jaybird-jdk18 "3.0.5"]]

:profiles
{:provided
{:dependencies
[[org.clojure/clojure "1.10.0"]
[metabase-core "1.0.0-SNAPSHOT"]]}

:uberjar
{:auto-cleam true
:aot :all
:omit-source true
:javac-options ["-target" "1.8", "-source" "1.8"]
:target-path "target/%s"
:uberjar-name "firebird.metabase-driver.jar"}})
29 changes: 29 additions & 0 deletions resources/metabase-plugin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
info:
name: Metabase FirebirdSQL Driver
version: 1.0.0
description: Allows Metabase to connect to FirebirdSQL databases.
driver:
name: firebird
display-name: FirebirdSQL
lazy-load: true
parent: sql-jdbc
connection-properties:
- host
- merge:
- port
- default: 3050
- merge:
- dbname
- name: db
placeholder: BirdsOfTheWorld
- user
- password
- merge:
- additional-options
- placeholder: "blobBufferSize=2048"
connection-properties-include-tunnel-config: false
init:
- step: load-namespace
namespace: metabase.driver.firebird
- step: register-jdbc-driver
class: org.firebirdsql.jdbc.FBDriver
203 changes: 203 additions & 0 deletions src/metabase/driver/firebird.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
(ns metabase.driver.firebird
(:require [clojure
[set :as set]
[string :as str]]
[clojure.java.jdbc :as jdbc]
[honeysql.core :as hsql]
[metabase.driver :as driver]
[metabase.driver.common :as driver.common]
[metabase.driver.sql-jdbc
[common :as sql-jdbc.common]
[connection :as sql-jdbc.conn]
[sync :as sql-jdbc.sync]]
[metabase.driver.sql.query-processor :as sql.qp]
[metabase.util
[honeysql-extensions :as hx]
[ssh :as ssh]])
(:import [java.sql DatabaseMetaData Time]))

(driver/register! :firebird, :parent :sql-jdbc)

(defn- firebird->spec
"Create a database specification for a FirebirdSQL database."
[{:keys [host port db jdbc-flags]
:or {host "localhost", port 3050, db "", jdbc-flags ""}
:as opts}]
(merge {:classname "org.firebirdsql.jdbc.FBDriver"
:subprotocol "firebirdsql"
:subname (str "//" host ":" port "/" db jdbc-flags)}
(dissoc opts :host :port :db :jdbc-flags)))

;; Obtain connection properties for connection to a Firebird database.
(defmethod sql-jdbc.conn/connection-details->spec :firebird [_ details]
(-> details
(update :port (fn [port]
(if (string? port)
(Integer/parseInt port)
port)))
(set/rename-keys {:dbname :db})
firebird->spec
(sql-jdbc.common/handle-additional-options details)))

;; Use "SELECT 1 FROM RDB$DATABASE" instead of "SELECT 1"
(defmethod driver/can-connect? :firebird [driver details]
(let [connection (sql-jdbc.conn/connection-details->spec driver (ssh/include-ssh-tunnel details))]
(= 1 (first (vals (first (jdbc/query connection ["SELECT 1 FROM RDB$DATABASE"])))))))

;; Use pattern matching because some parameters can have a length parameter, e.g. VARCHAR(255)
(def ^:private database-type->base-type
(sql-jdbc.sync/pattern-based-database-type->base-type
[[#"INT64" :type/BigInteger]
[#"DECIMAL" :type/Decimal]
[#"FLOAT" :type/Float]
[#"BLOB" :type/*]
[#"INTEGER" :type/Integer]
[#"NUMERIC" :type/Decimal]
[#"DOUBLE" :type/Float]
[#"SMALLINT" :type/Integer]
[#"CHAR" :type/Text]
[#"BIGINT" :type/BigInteger]
[#"TIMESTAMP" :type/DateTime]
[#"DATE" :type/Date]
[#"TIME" :type/Time]
[#"BLOB SUB_TYPE 0" :type/*]
[#"BLOB SUB_TYPE 1" :type/Text]
[#"DOUBLE PRECISION" :type/Float]
[#"BOOLEAN" :type/Boolean]]))

;; Map Firebird data types to base types
(defmethod sql-jdbc.sync/database-type->base-type :firebird [_ database-type]
(database-type->base-type database-type))

;; Use "FIRST" instead of "LIMIT"
(defmethod sql.qp/apply-top-level-clause [:firebird :limit] [_ _ honeysql-form {value :limit}]
(assoc honeysql-form :modifiers [(format "FIRST %d" value)]))

;; Use "SKIP" instead of "OFFSET"
(defmethod sql.qp/apply-top-level-clause [:firebird :page] [_ _ honeysql-form {{:keys [items page]} :page}]
(assoc honeysql-form :modifiers [(format "FIRST %d SKIP %d"
items
(* items (dec page)))]))

;; Firebird stores table names as CHAR(31), so names with < 31 characters get padded with spaces.
;; This confuses everyone, including metabase, so we trim the table names here
(defn post-filtered-trimmed-active-tables
"Alternative implementation of `ISQLDriver/active-tables` best suited for DBs with little or no support for schemas.
Fetch *all* Tables, then filter out ones whose schema is in `excluded-schemas` Clojure-side."
[driver, ^DatabaseMetaData metadata, & [db-name-or-nil]]
(set (for [table (sql-jdbc.sync/post-filtered-active-tables driver metadata db-name-or-nil)]
{:name (str/trim (:name table))
:description (:description table)
:schema (:schema table)})))

(defmethod sql-jdbc.sync/active-tables :firebird [& args]
(apply post-filtered-trimmed-active-tables args))

;; Convert unix time to a timestamp
(defmethod sql.qp/unix-timestamp->timestamp [:firebird :seconds] [_ _ expr]
(hsql/call :DATEADD (hsql/raw "SECOND") expr (hx/cast :TIMESTAMP (hx/literal "01-01-1970 00:00:00"))))

;; Helpers for Date extraction
;; TODO: This can probably simplified a lot by using String concentation instead of
;; replacing parts of the format recursively

;; Specifies what Substring to replace for a given time unit
(defn- get-unit-placeholder [unit]
(case unit
:SECOND :ss
:MINUTE :mm
:HOUR :hh
:DAY :DD
:MONTH :MM
:YEAR :YYYY))

(defn- get-unit-name [unit]
(case unit
0 :SECOND
1 :MINUTE
2 :HOUR
3 :DAY
4 :MONTH
5 :YEAR))
;; Replace the specified part of the timestamp
(defn- replace-timestamp-part [input unit expr]
(hsql/call :replace input (hx/literal (get-unit-placeholder unit)) (hsql/call :extract unit expr)))

(defn- format-step [expr input step wanted-unit]
(if (> step wanted-unit)
(format-step expr (replace-timestamp-part input (get-unit-name step) expr) (- step 1) wanted-unit)
(replace-timestamp-part input (get-unit-name step) expr)))

(defn- format-timestamp [expr format-template wanted-unit]
(format-step expr (hx/literal format-template) 5 wanted-unit))

;; Firebird doesn't have a date_trunc function, so use a workaround: First format the timestamp to a
;; string of the wanted resulution, then convert it back to a timestamp
(defn- timestamp-trunc [expr format-str wanted-unit]
(hx/cast :TIMESTAMP (format-timestamp expr format-str wanted-unit)))

(defn- date-trunc [expr format-str wanted-unit]
(hx/cast :DATE (format-timestamp expr format-str wanted-unit)))

(defmethod sql.qp/date [:firebird :default] [_ _ expr] expr)
;; Cast to TIMESTAMP if we need minutes or hours, since expr might be a DATE
(defmethod sql.qp/date [:firebird :minute] [_ _ expr] (timestamp-trunc (hx/cast :TIMESTAMP expr) "YYYY-MM-DD hh:mm:00" 1))
(defmethod sql.qp/date [:firebird :minute-of-hour] [_ _ expr] (hsql/call :extract :MINUTE (hx/cast :TIMESTAMP expr)))
(defmethod sql.qp/date [:firebird :hour] [_ _ expr] (timestamp-trunc (hx/cast :TIMESTAMP expr) "YYYY-MM-DD hh:00:00" 2))
(defmethod sql.qp/date [:firebird :hour-of-day] [_ _ expr] (hsql/call :extract :HOUR (hx/cast :TIMESTAMP expr)))
;; Cast to DATE to get rid of anything smaller than day
(defmethod sql.qp/date [:firebird :day] [_ _ expr] (hx/cast :DATE expr))
;; Firebird DOW is 0 (Sun) - 6 (Sat); increment this to be consistent with Java, H2, MySQL, and Mongo (1-7)
(defmethod sql.qp/date [:firebird :day-of-week] [_ _ expr] (hx/+ (hsql/call :extract :WEEKDAY (hx/cast :DATE expr)) 1))
(defmethod sql.qp/date [:firebird :day-of-month] [_ _ expr] (hsql/call :extract :DAY expr))
;; Firebird YEARDAY starts from 0; increment this
(defmethod sql.qp/date [:firebird :day-of-year] [_ _ expr] (hx/+ (hsql/call :extract :YEARDAY expr) 1))
;; Cast to DATE because we do not want units smaller than days
;; Use hsql/raw for DAY in dateadd because the keyword :WEEK gets surrounded with quotations
(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)))
(defmethod sql.qp/date [:firebird :week-of-year] [_ _ expr] (hsql/call :extract :WEEK expr))
(defmethod sql.qp/date [:firebird :month] [_ _ expr] (date-trunc expr "YYYY-MM-01" 4))
(defmethod sql.qp/date [:firebird :month-of-year] [_ _ expr] (hsql/call :extract :MONTH expr))
;; Use hsql/raw for MONTH in dateadd because the keyword :MONTH gets surrounded with quotations
(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)))
(defmethod sql.qp/date [:firebird :quarter-of-year] [_ _ expr] (hx/+ (hx// (hx/- (hsql/call :extract :MONTH expr) 1) 3) 1))
(defmethod sql.qp/date [:firebird :year] [_ _ expr] (hsql/call :extract :YEAR expr))

(defmethod driver/date-interval :firebird [driver unit amount]
(if (= unit :quarter)
(recur driver :month (hx/* amount 3))
(hsql/call :dateadd (hsql/raw (name unit)) amount (hx/cast :timestamp (hx/literal :now)))))

(defmethod sql.qp/current-datetime-fn :firebird [_]
(hx/cast :timestamp (hx/literal :now)))

(defmethod driver.common/current-db-time-date-formatters :firebird [_]
(driver.common/create-db-time-formatters "yyyy-MM-dd HH:mm:ss.SSSS"))

(defmethod driver.common/current-db-time-native-query :firebird [_]
"SELECT CAST(CAST('Now' AS TIMESTAMP) AS VARCHAR(24)) FROM RDB$DATABASE")

(defmethod driver/current-db-time :firebird [& args]
(apply driver.common/current-db-time args))

(defmethod sql.qp/->honeysql [:firebird :stddev]
[driver [_ field]]
(hsql/call :stddev_samp (sql.qp/->honeysql driver field)))

(defmethod driver/supports? [:firebird :basic-aggregations] [_ _] true)

(defmethod driver/supports? [:firebird :expression-aggregations] [_ _] true)

(defmethod driver/supports? [:firebird :standard-deviation-aggregations] [_ _] true)

(defmethod driver/supports? [:firebird :foreign-keys] [_ _] true)

(defmethod driver/supports? [:firebird :nested-fields] [_ _] false)

(defmethod driver/supports? [:firebird :set-timezone] [_ _] false)

(defmethod driver/supports? [:firebird :nested-queries] [_ _] false)

(defmethod driver/supports? [:firebird :binning] [_ _] false)

(defmethod driver/supports? [:firebird :case-sensitivity-string-filter-options] [_ _] false)
112 changes: 112 additions & 0 deletions test/metabase/test/data/firebird.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
(ns metabase.test.data.firebird
"Code for creating / destroying a FirebirdSQL database from a `DatabaseDefinition`."
(:require [clojure.string :as str]
[clojure.java.jdbc :as jdbc]
[metabase.driver.sql-jdbc.connection :as sql-jdbc.conn]
[metabase.test.data
[interface :as tx]
[sql :as sql.tx]
[sql-jdbc :as sql-jdbc.tx]]
[metabase.test.data.sql-jdbc
[load-data :as load-data]
[execute :as execute]]
[metabase.util :as u]))

(sql-jdbc.tx/add-test-extensions! :firebird)

(defmethod tx/dbdef->connection-details :firebird [_ context {:keys [database-name]}]
{:host (tx/db-test-env-var-or-throw :firebird :host "localhost")
:port (tx/db-test-env-var-or-throw :firebird :port 3050)
:user (tx/db-test-env-var-or-throw :firebird :user "SYSDBA")
:password (tx/db-test-env-var-or-throw :firebird :password "masterkey")
:db (tx/db-test-env-var-or-throw :firebird :db "metabase-testing")
:additional-options (tx/db-test-env-var-or-throw :firebird :additional-options "charSet=UTF-8")})

(defmethod sql.tx/field-base-type->sql-type [:firebird :type/BigInteger] [_ _] "BIGINT")
;; Boolean was added in firebird 3, maybe use smallint for firebird 2 compatibility?
(defmethod sql.tx/field-base-type->sql-type [:firebird :type/Boolean] [_ _] "BOOLEAN")
(defmethod sql.tx/field-base-type->sql-type [:firebird :type/Date] [_ _] "DATE")
(defmethod sql.tx/field-base-type->sql-type [:firebird :type/DateTime] [_ _] "TIMESTAMP")
(defmethod sql.tx/field-base-type->sql-type [:firebird :type/Decimal] [_ _] "DECIMAL")
(defmethod sql.tx/field-base-type->sql-type [:firebird :type/Float] [_ _] "FLOAT")
(defmethod sql.tx/field-base-type->sql-type [:firebird :type/Integer] [_ _] "INTEGER")
(defmethod sql.tx/field-base-type->sql-type [:firebird :type/Text] [_ _] "VARCHAR(255)")
(defmethod sql.tx/field-base-type->sql-type [:firebird :type/Time] [_ _] "TIME")

(defmethod sql.tx/pk-sql-type :firebird [_] "INTEGER GENERATED BY DEFAULT AS IDENTITY")

(defmethod sql.tx/pk-field-name :firebird [_] "id")

;; Use RECREATE TABLE to drop a table if it exists, in case some tables have not been dropped before
;; running tests
(defmethod sql.tx/create-table-sql :firebird
[driver {:keys [database-name], :as dbdef} {:keys [table-name field-definitions table-comment]}]
(let [pk-field-name (sql.tx/pk-field-name driver)]
(format "RECREATE TABLE %s (\"%s\" %s PRIMARY KEY, %s)"
(sql.tx/qualify+quote-name driver database-name table-name)
pk-field-name
(sql.tx/pk-sql-type driver)
(->> field-definitions
(map (fn [{:keys [field-name base-type field-comment]}]
(format "\"%s\" %s"
field-name
(if (map? base-type)
(:native base-type)
(sql.tx/field-base-type->sql-type driver base-type)))))
(interpose ", ")
(apply str)))))

(defmethod sql.tx/drop-table-if-exists-sql :firebird [& _] nil)

(defmethod load-data/load-data! :firebird [& args]
(apply load-data/load-data-one-at-a-time! args))

(defmethod execute/execute-sql! :firebird [& args]
(apply execute/sequentially-execute-sql! args))

;; Firebird cannot create or drop databases on runtime
(defmethod sql.tx/create-db-sql :firebird [& _] nil)
(defmethod sql.tx/drop-db-if-exists-sql :firebird [& _] nil)

(defmethod sql.tx/qualified-name-components :firebird
([_ db-name] [db-name])
([_ db-name table-name] [(tx/db-qualified-table-name db-name table-name)])
([_ db-name table-name field-name] [(tx/db-qualified-table-name db-name table-name) field-name]))

;; Drop all tables that are not system tables before running test
(defmethod tx/before-run :firebird [_]
(let [connection-spec (sql-jdbc.conn/connection-details->spec :firebird
(tx/dbdef->connection-details :firebird :server nil))
foreign-keys (jdbc/query
connection-spec
(str "select r.rdb$relation_name, r.rdb$constraint_name from rdb$relation_constraints r where (r.rdb$constraint_type='FOREIGN KEY')"))
leftover-tables (map :rdb$relation_name (jdbc/query
connection-spec
(str "SELECT RDB$RELATION_NAME "
"FROM RDB$RELATIONS "
"WHERE RDB$VIEW_BLR IS NULL "
"AND (RDB$SYSTEM_FLAG IS NULL OR RDB$SYSTEM_FLAG = 0);")))]
;; First, kill all connections and statements that are still running
(println "Killing all open connections to test db... ")
(jdbc/execute! connection-spec ["DELETE FROM MON$ATTACHMENTS WHERE MON$ATTACHMENT_ID <> CURRENT_CONNECTION"])
(println "[ok]")
(println "Killing all running statements in test db... ")
(jdbc/execute! connection-spec ["DELETE FROM MON$STATEMENTS WHERE MON$ATTACHMENT_ID <> CURRENT_CONNECTION"])
(println "[ok]")
;; Second, remove all foreign keys, so tables can be properly dropped
(doseq [constraint foreign-keys]
(u/ignore-exceptions
(printf "Dropping constraint \"%s\" on table \"%s\"...\n"
(:rdb$constraint_name constraint)
(:rdb$relation_name constraint))
(jdbc/execute! connection-spec [(format "ALTER TABLE \"%s\" DROP CONSTRAINT \"%s\";"
(:rdb$relation_name constraint)
(:rdb$constraint_name constraint))])
(println "[ok]")))
;; Third, drop all leftover tables
(doseq [table leftover-tables]
(u/ignore-exceptions
(printf "Dropping leftover Firebird table \"%s\"...\n" (str/trimr table))
(jdbc/execute! connection-spec [(format "DROP TABLE \"%s\";" (str/trimr table))])
(println "[ok]")))
(println "Destroyed all leftover tables.")))

0 comments on commit 51d9edd

Please sign in to comment.