Method impls for [[metabase.driver.sql-jdbc.actions]] for | (ns metabase.driver.postgres.actions (:require [clojure.java.jdbc :as jdbc] [clojure.string :as str] [metabase.actions.core :as actions] [metabase.driver.sql-jdbc.actions :as sql-jdbc.actions] [metabase.driver.sql-jdbc.connection :as sql-jdbc.conn] [metabase.util :as u] [metabase.util.i18n :refer [deferred-trun tru]])) |
(set! *warn-on-reflection* true) | |
Given a constraint with TODO -- we should probably be TTL caching this information. Otherwise parsing 100 errors for a bulk action will result in 100 identical data warehouse queries. It's not like constraint columns are something we would expect to change regularly anyway. | (defn- constraint->column-names
[database constraint-name]
(let [jdbc-spec (sql-jdbc.conn/db->pooled-connection-spec (u/the-id database))
sql-args ["select column_name from information_schema.constraint_column_usage where constraint_name = ?" constraint-name]]
(into []
(map :column_name)
(jdbc/reducible-query jdbc-spec sql-args {:identifers identity, :transaction? false})))) |
(defmethod sql-jdbc.actions/maybe-parse-sql-error [:postgres actions/violate-not-null-constraint]
[_driver error-type _database _action-type error-message]
(when-let [[_ column]
(re-find #"null value in column \"([^\"]+)\".*violates not-null constraint" error-message)]
{:type error-type
:message (tru "{0} must have values." (str/capitalize column))
:errors {column (tru "You must provide a value.")}})) | |
(defmethod sql-jdbc.actions/maybe-parse-sql-error [:postgres actions/violate-unique-constraint]
[_driver error-type database _action-type error-message]
(when-let [[_match constraint _value]
(re-find #"duplicate key value violates unique constraint \"([^\"]+)\"" error-message)]
(let [columns (constraint->column-names database constraint)]
{:type error-type
:message (tru "{0} already {1}." (u/build-sentence (map str/capitalize columns) :stop? false) (deferred-trun "exists" "exist" (count columns)))
:errors (reduce (fn [acc col]
(assoc acc col (tru "This {0} value already exists." (str/capitalize col))))
{}
columns)}))) | |
(defmethod sql-jdbc.actions/maybe-parse-sql-error [:postgres actions/violate-foreign-key-constraint]
[_driver error-type _database action-type error-message]
(or (when-let [[_match _table _constraint _ref-table column _value _ref-table-2]
(re-find #"update or delete on table \"([^\"]+)\" violates foreign key constraint \"([^\"]+)\" on table \"([^\"]+)\"\n Detail: Key \((.*?)\)=\((.*?)\) is still referenced from table \"([^\"]+)\"" error-message)]
(merge {:type error-type}
(case action-type
:row/delete
{:message (tru "Other tables rely on this row so it cannot be deleted.")
:errors {}}
:row/update
{:message (tru "Unable to update the record.")
:errors {column (tru "This {0} does not exist." (str/capitalize column))}})))
(when-let [[_match _table _constraint column _value _ref-table]
(re-find #"insert or update on table \"([^\"]+)\" violates foreign key constraint \"([^\"]+)\"\n Detail: Key \((.*?)\)=\((.*?)\) is not present in table \"([^\"]+)\"" error-message)]
{:type error-type
:message (case action-type
:row/create
(tru "Unable to create a new record.")
:row/update
(tru "Unable to update the record."))
:errors {column (tru "This {0} does not exist." (str/capitalize column))}}))) | |
(defmethod sql-jdbc.actions/maybe-parse-sql-error [:postgres actions/incorrect-value-type]
[_driver error-type _database _action-type error-message]
(when-let [[_] (re-find #"invalid input syntax for .*" error-message)]
{:type error-type
:message (tru "Some of your values aren’t of the correct type for the database.")
:errors {}})) | |
(defmethod sql-jdbc.actions/base-type->sql-type-map :postgres
[_driver]
{:type/BigInteger "BIGINT"
:type/Boolean "BOOL"
:type/Date "DATE"
:type/DateTime "TIMESTAMP"
:type/DateTimeWithTZ "TIMESTAMP WITH TIME ZONE"
:type/DateTimeWithLocalTZ "TIMESTAMP WITH TIME ZONE"
:type/Decimal "DECIMAL"
:type/Float "FLOAT"
:type/Integer "INTEGER"
:type/IPAddress "INET"
:type/JSON "JSON"
:type/Text "TEXT"
:type/Time "TIME"
:type/TimeWithTZ "TIME WITH TIME ZONE"
:type/UUID "UUID"}) | |
For Postgres creating a Savepoint and rolling it back on error seems to be enough to let the parent transaction proceed if some particular statement encounters an error. | (defmethod sql-jdbc.actions/do-nested-transaction :postgres
[_driver ^java.sql.Connection conn thunk]
(let [savepoint (.setSavepoint conn)]
(try
(thunk)
(catch Throwable e
(.rollback conn savepoint)
(throw e))
(finally
(.releaseSavepoint conn savepoint))))) |
Add returning * so that we don't have to make an additional query. | (defmethod sql-jdbc.actions/prepare-query* [:postgres :row/create] [_driver _action hsql-query] (assoc hsql-query :returning [:*])) |
Result is already the created row. | (defmethod sql-jdbc.actions/select-created-row :postgres [_driver _create-hsql _conn result] result) |