-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 51d9edd
Showing
6 changed files
with
391 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"}}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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."))) |