diff --git a/CHANGELOG.md b/CHANGELOG.md index e2d6037..3e03964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Only accretive/fixative changes will be made from now on. * Fix [#232](https://github.com/seancorfield/next-jdbc/issues/232) by using `as-cols` in `insert-multi!` SQL builder. Thanks to @changsu-farmmorning for spotting that bug! * Fix [#229](https://github.com/seancorfield/next-jdbc/issues/229) by adding `next.jdbc.connect/uri->db-spec` which converts a URI string to a db-spec hash map; in addition, if `DriverManager/getConnection` fails, it assumes it was passed a URI instead of a JDBC URL, and retries after calling that function and then recreating the JDBC URL (which should have the effect of moving the embedded user/password credentials into the properties structure instead of the URL). * Address [#228](https://github.com/seancorfield/next-jdbc/issues/228) by adding `PreparedStatement` caveat to the Oracle **Tips & Tricks** section. + * Address [#226](https://github.com/seancorfield/next-jdbc/issues/226) by adding a section on exception handling to **Tips & Tricks** (TL;DR: it's all horribly vendor-specific!). * Add `on-connection` to exported `clj-kondo` configuration. * Switch `run-test` from `sh` to `bb`. diff --git a/doc/tips-and-tricks.md b/doc/tips-and-tricks.md index 1ee6d3e..126a623 100644 --- a/doc/tips-and-tricks.md +++ b/doc/tips-and-tricks.md @@ -38,6 +38,42 @@ Consult the [java.sql.Blob documentation](https://docs.oracle.com/javase/8/docs/ > Note: the standard MySQL JDBC driver seems to return `BLOB` data as `byte[]` instead of `java.sql.Blob`. +## Exceptions + +A lot of JDBC operations can fail with an exception. JDBC 4.0 has a +[well-defined hierarchy of exception types](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/package-tree.html) +and you can often catch a specific type of exception to do useful handling +of various error conditions that you might "expect" when working with a +database. + +A good example is [SQLIntegrityConstraintViolationException](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/SQLIntegrityConstraintViolationException.html) +which typically represents an index/key constraint violation such as a +duplicate primary key insertion attempt. + +However, like some other areas when dealing with JDBC, the reality can +be very database-specific. Some database drivers **don't** use the hierarchy +above -- notably PostgreSQL, which has a generic `PSQLException` type +with its own subclasses and semantics. See [PostgreSQL JDBC issue #963](https://github.com/pgjdbc/pgjdbc/issues/963) +for a discussion of the difficulty in adopting the standard JDBC hierarchy +(dating back five years). + +The `java.sql.SQLException` class provides `.getErrorCode()` and +`.getSQLState()` methods but the values returned by those are +explicitly vendor-specific (error code) or only partly standardized (state). +In theory, the SQL state should follow either the X/Open (Open Group) or +ANSI SQL 2003 conventions, both of which were behind paywalls(!). The most +complete public listing is probably the IBM DB2 +[SQL State](https://www.ibm.com/docs/en/db2woc?topic=messages-sqlstate) +document. +See also this [Stack Overflow post about SQL State](https://stackoverflow.com/questions/1399574/what-are-all-the-possible-values-for-sqlexception-getsqlstate) +for more references and links. Not all database drivers follow either of +these conventions for SQL State so you may still have to consult your +vendor's specific documentation. + +All of this makes writing _generic_ error handling, that works across +multiple databases, very hard indeed. You can't rely on the JDBC `SQLException` +hierarchy; you can sometimes rely on a subset of SQL State values. + ## Handling Timeouts JDBC provides a number of ways in which you can decide how long an operation should run before it times out. Some of these timeouts are specified in seconds and some are in milliseconds. Some are handled via connection properties (or JDBC URL parameters), some are handled via methods on various JDBC objects. diff --git a/test/next/jdbc_test.clj b/test/next/jdbc_test.clj index 99fe187..bf888bb 100644 --- a/test/next/jdbc_test.clj +++ b/test/next/jdbc_test.clj @@ -9,8 +9,8 @@ [next.jdbc.connection :as c] [next.jdbc.test-fixtures :refer [with-test-db db ds column - default-options stored-proc? - derby? hsqldb? jtds? mssql? mysql? postgres? sqlite?]] + default-options stored-proc? + derby? hsqldb? jtds? mssql? mysql? postgres? sqlite?]] [next.jdbc.prepare :as prep] [next.jdbc.result-set :as rs] [next.jdbc.specs :as specs] @@ -440,6 +440,29 @@ VALUES ('Pear', 'green', 49, 47) (is (= 4 (count (jdbc/execute! con ["select * from fruit"])))) (is (= ac (.getAutoCommit con)))))))))) +#_ +(deftest duplicate-insert-test + ;; this is primarily a look at exception types/information for #226 + (try + (jdbc/execute! (ds) [" + INSERT INTO fruit (id, name, appearance, cost, grade) + VALUES (1234, '1234', '1234', 1234, 1234) + "]) + (try + (jdbc/execute! (ds) [" + INSERT INTO fruit (id, name, appearance, cost, grade) + VALUES (1234, '1234', '1234', 1234, 1234) + "]) + (println (:dbtype (db)) "allowed duplicate insert") + (catch java.sql.SQLException t + (println (:dbtype (db)) "duplicate insert threw" (type t) + "error" (.getErrorCode t) "state" (.getSQLState t) + "\n\t" (ex-message t)))) + (catch java.sql.SQLException t + (println (:dbtype (db)) "will not allow specific ID" (type t) + "error" (.getErrorCode t) "state" (.getSQLState t) + "\n\t" (ex-message t))))) + (deftest bool-tests (doseq [[n b] [["zero" 0] ["one" 1] ["false" false] ["true" true]] :let [v-bit (if (number? b) b (if b 1 0))