|
| 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) |
0 commit comments