Skip to content

Commit 9000493

Browse files
rlbdvaustb
authored andcommitted
Set bytea PreparedStatement parameters via .setBytes and .setArray
In 42.7.7 (pgjdbc/pgjdbc#3757) pgjdbc broke parameters set via (.setObject stmt i (doto (PGobject.) (.setType "bytea") (.setValue "\x42abcdef))) which is the way pdb has handled them, whether explicitly, or indirectly via clojure.java.jdbc and next.jdbc, which both default to (.setObject ...) for parameter types without a more specific protocol extension. While this is intended to work (see the issue thread), the pgjdbc documentation specifies `.setBytes` for bytea parameters, and `.setArray` for arrays of bytea: https://jdbc.postgresql.org/documentation/binary-data/ https://jdbc.postgresql.org/documentation/server-prepare/#arrays Add new PDBBytea and VecPDBBytea classes for the bytea and bytea[] parameters and extend the relevant clojure.java.jdbc and next.jdbc protocols to call the appropriate PreparedStatement method for each, rather than the `.setObject` default. Note that we cannot use defrecords because (by default) honeysql would treat them as unrecognized extensions. [[email protected]: remove unused import, fix typo, add hashCode method to PDBBytea] Reviewed-by: Austin Blatt <[email protected]>
1 parent e1e0e59 commit 9000493

File tree

7 files changed

+128
-28
lines changed

7 files changed

+128
-28
lines changed

project.clj

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,23 @@
7878
"puppetserver"
7979
"vendor"]))
8080

81+
(def pdb-aot-classes
82+
;; Compile classes first for now:
83+
;; https://codeberg.org/leiningen/leiningen/issues/99
84+
'[puppetlabs.puppetdb.jdbc.PDBBytea puppetlabs.puppetdb.jdbc.VecPDBBytea])
85+
8186
(def pdb-aot-namespaces
82-
(apply vector
83-
#"puppetlabs\.puppetdb\..*"
84-
(->> "resources/puppetlabs/puppetdb/bootstrap.cfg"
85-
clojure.java.io/reader
86-
line-seq
87-
(map clojure.string/trim)
88-
(remove #(re-matches #"#.*" %)) ;; # comments
89-
(remove #(re-matches #"puppetlabs\.puppetdb\.." %))
90-
(map #(clojure.string/replace % #"(.*)/[^/]+$" "$1"))
91-
(map symbol))))
87+
(into []
88+
(concat pdb-aot-classes
89+
[#"puppetlabs\.puppetdb\..*"]
90+
(->> "resources/puppetlabs/puppetdb/bootstrap.cfg"
91+
clojure.java.io/reader
92+
line-seq
93+
(map clojure.string/trim)
94+
(remove #(re-matches #"#.*" %)) ;; # comments
95+
(remove #(re-matches #"puppetlabs\.puppetdb\.." %))
96+
(map #(clojure.string/replace % #"(.*)/[^/]+$" "$1"))
97+
(map symbol)))))
9298

9399
;; Avoid startup reflection warnings due to
94100
;; https://clojure.atlassian.net/browse/CLJ-2066
@@ -216,6 +222,8 @@
216222
;; "test"]. See the :testutils profile below.
217223
:classifiers {:test :testutils}
218224

225+
:aot ~pdb-aot-classes
226+
219227
:profiles {:defaults {:resource-paths ["test-resources"]
220228
:dependencies ~pdb-dev-deps
221229
:injections [(do
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
(ns puppetlabs.puppetdb.jdbc.PDBBytea
2+
"Carrier for bytea parameters, to support clojure.java.jdbc and next.jdbc
3+
protocol extensions. Essentially just a typed wrapper around a byte[]."
4+
(:import
5+
(java.util Arrays))
6+
(:gen-class
7+
;;:extends org.postgresql.util.PGobject
8+
:state ^"[B" data
9+
:init init
10+
:constructors {["[B"] []}))
11+
12+
(def ^:private warn-on-reflection-orig *warn-on-reflection*)
13+
(set! *warn-on-reflection* true)
14+
15+
(defn -init [data] [[] data])
16+
17+
;; Implemented for now because edge-replacement-differential test, for
18+
;; example, uses munge-hash-for-storage to create the test values that
19+
;; it compares for equality -- and who knows what else. Apparently
20+
;; PGobject bytea values, which PDBBytea replaces, compare content for
21+
;; equality, and since we use bytea for hashes, best to be
22+
;; conservative.
23+
(defn -equals [^puppetlabs.puppetdb.jdbc.PDBBytea this x]
24+
(and (instance? puppetlabs.puppetdb.jdbc.PDBBytea x)
25+
(Arrays/equals ^"[B" (.data this)
26+
^"[B" (.data ^puppetlabs.puppetdb.jdbc.PDBBytea x))))
27+
28+
(defn -hashCode [^puppetlabs.puppetdb.jdbc.PDBBytea this]
29+
(Arrays/hashCode ^"[B" (.data this)))
30+
31+
(set! *warn-on-reflection* warn-on-reflection-orig)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
(ns puppetlabs.puppetdb.jdbc.VecPDBBytea
2+
"Carrier for bytea[] parameters, to support clojure.java.jdbc and next.jdbc
3+
protocol extensions. Essentially just a typed wrapper around a byte[][]."
4+
;; It doesn't look like java.sql.Arrays, which is what this
5+
;; replaces, define content-based equals(), so we don't need one.
6+
(:gen-class
7+
:state ^"[[B" data
8+
:init init
9+
:constructors {["[[B"] []}))
10+
11+
(def ^:private warn-on-reflection-orig *warn-on-reflection*)
12+
(set! *warn-on-reflection* true)
13+
14+
(defn -init [data] [[] data])
15+
16+
(set! *warn-on-reflection* warn-on-reflection-orig)

src/puppetlabs/puppetdb/scf/migrate.clj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
(java.time LocalDateTime LocalDate ZonedDateTime Instant)
7272
(java.time.temporal ChronoUnit)
7373
(java.time.format DateTimeFormatter)
74-
(org.postgresql.util PGobject)))
74+
(puppetlabs.puppetdb.jdbc PDBBytea)))
7575

7676
(defn init-through-2-3-8
7777
[]
@@ -1432,7 +1432,7 @@
14321432
from (select unnest(?) as id, unnest(?) as hash) in_data
14331433
where fact_values.id = in_data.id"
14341434
[(sutils/array-to-param "bigint" Long ids)
1435-
(sutils/array-to-param "bytea" PGobject hashes)])))))
1435+
(sutils/array-to-param "bytea" PDBBytea hashes)])))))
14361436

14371437
(log/info (trs "[7/8] Indexing fact_values table..."))
14381438
(jdbc/do-commands

src/puppetlabs/puppetdb/scf/storage.clj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
(java.time ZoneId ZonedDateTime)
5959
(java.util Arrays)
6060
(org.joda.time Period)
61-
(org.postgresql.util PGobject)))
61+
(puppetlabs.puppetdb.jdbc PDBBytea)))
6262

6363
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
6464
;;; Schemas
@@ -475,7 +475,7 @@
475475
(if (seq resource-hashes)
476476
(let [resource-array (->> (vec resource-hashes)
477477
(map sutils/munge-hash-for-storage)
478-
(sutils/array-to-param "bytea" org.postgresql.util.PGobject))
478+
(sutils/array-to-param "bytea" PDBBytea))
479479
query (format "SELECT DISTINCT %s as resource FROM resource_params_cache WHERE resource=ANY(?)"
480480
(sutils/sql-hash-as-str "resource"))
481481
sql-params [query resource-array]]
@@ -1039,7 +1039,7 @@
10391039
(defn find-package-hashes [package-hashes]
10401040
(jdbc/call-with-query-rows [(format "SELECT id, %s as package_hash FROM packages WHERE hash = ANY(?)"
10411041
(sutils/sql-hash-as-str "hash"))
1042-
(sutils/array-to-param "bytea" PGobject
1042+
(sutils/array-to-param "bytea" PDBBytea
10431043
(map sutils/munge-hash-for-storage package-hashes))]
10441044
(fn [rows]
10451045
(reduce (fn [acc {:keys [id package_hash]}]

src/puppetlabs/puppetdb/scf/storage_utils.clj

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,36 @@
44
[clojure.java.jdbc :as sql]
55
[clojure.string :as str]
66
[clojure.tools.logging :as log]
7+
[next.jdbc.prepare :refer [SettableParameter]]
78
[puppetlabs.i18n.core :refer [trs tru]]
89
[puppetlabs.kitchensink.core :as kitchensink]
910
[puppetlabs.puppetdb.cheshire :as json]
1011
[puppetlabs.puppetdb.honeysql]
1112
[puppetlabs.puppetdb.jdbc :as jdbc]
1213
[puppetlabs.puppetdb.query.common :refer [bad-query-ex]]
1314
[puppetlabs.puppetdb.schema :as pls]
15+
[puppetlabs.puppetdb.utils :refer [byte-array-class]]
1416
[schema.core :as s])
1517
(:import
16-
[java.sql Connection]
17-
[java.util UUID]
18-
[org.postgresql.util PGobject]))
18+
(java.sql Connection PreparedStatement)
19+
(java.util HexFormat UUID)
20+
(org.postgresql.util PGobject)
21+
(puppetlabs.puppetdb.jdbc PDBBytea VecPDBBytea)))
22+
23+
(def ^:private warn-on-reflection-orig *warn-on-reflection*)
24+
(set! *warn-on-reflection* true)
1925

2026
;; SCHEMA
2127

2228
(defn array-to-param
2329
[col-type java-type values]
24-
(.createArrayOf ^Connection (:connection (jdbc/db))
25-
col-type
26-
(into-array java-type values)))
30+
;; We create the VecPDBBytea so we can redirect the parameter to
31+
;; setArray in the set-parameter specializations.
32+
(if (= java-type PDBBytea)
33+
;; FIXME: change byte-array-class to byte/1 once we require clojure 1.12+
34+
(VecPDBBytea. (into-array byte-array-class (map #(.data ^PDBBytea %) values)))
35+
(.createArrayOf ^Connection (:connection (jdbc/db))
36+
col-type (into-array java-type values))))
2737

2838
(def pg-extension-map
2939
"Maps to the table definition in postgres, but only includes some of the
@@ -427,9 +437,8 @@
427437
(sql/with-db-connection [_conn db]
428438
(sql/execute! db ["vacuum analyze"] {:transaction? false})))
429439

430-
(defn parse-db-hash
431-
[^PGobject db-hash]
432-
(str/replace (.getValue db-hash) "\\x" ""))
440+
(defn db-hash->hex [^PDBBytea pdbb]
441+
(.formatHex (HexFormat/of) (.data pdbb)))
433442

434443
(defn parse-db-uuid
435444
[^UUID db-uuid]
@@ -454,9 +463,43 @@
454463
(defn bytea-escape [s]
455464
(format "\\x%s" s))
456465

457-
(defn munge-hash-for-storage
458-
[hash]
459-
(str->pgobject "bytea" (bytea-escape hash)))
466+
(extend-protocol sql/ISQLParameter
467+
;; This became necessary with more recent pgjdbc
468+
;; versions (42.7.7 (at least) and newer), because it stopped
469+
;; handling a "bytea" PGobject; it started crashing in
470+
;; PGBytea/toPGLiteral, which doesn't have a clause to handle the
471+
;; escaped String that it ends up with, presumably from the PGobject
472+
;; value. The pgjdbc docs specify .setBytes, so do
473+
;; that:
474+
;; https://jdbc.postgresql.org/documentation/binary-data/
475+
;; https://clojure-doc.org/articles/ecosystem/java_jdbc/using_sql/
476+
;; https://clojure.github.io/java.jdbc/#clojure.java.jdbc/ISQLParameter
477+
PDBBytea
478+
(sql/set-parameter [v ^PreparedStatement s i]
479+
(.setBytes s i (.data v)))
480+
VecPDBBytea
481+
(sql/set-parameter [v ^PreparedStatement s i]
482+
(.setArray s i (.createArrayOf ^Connection (:connection (jdbc/db))
483+
"bytea" (.data v)))))
484+
485+
(extend-protocol SettableParameter
486+
PDBBytea
487+
(sql/set-parameter [v ^PreparedStatement s i] (.setBytes s i (.data v)))
488+
VecPDBBytea
489+
(sql/set-parameter [v ^PreparedStatement s i]
490+
(.setArray s i (.createArrayOf ^Connection (:connection (jdbc/db))
491+
"bytea" (.data v)))))
492+
493+
(defn munge-hash-for-storage [hash]
494+
;; Represent hashes as PDBBytea so we can add them to
495+
;; PreparedStatments via setBytes via the clojure/next jdbc
496+
;; protocols above. It'd be more efficient if eventually pdb just
497+
;; used something like PDBBytea or byte[] everywhere, though the
498+
;; latter would allow mutation and might raise "ownership"
499+
;; questions. The *most* efficient hash that's not just a byte[]
500+
;; might be a class with two long members and a short to store the
501+
;; 20 bytes (i.e. so that the data would be inline in the class).
502+
(PDBBytea. (.parseHex (HexFormat/of) hash)))
460503

461504
(defn munge-json-for-storage
462505
"Prepare a clojure object for storage depending on db type."
@@ -497,3 +540,5 @@
497540
(log/info (trs "Analyzing small tables"))
498541
(apply jdbc/do-commands-outside-txn
499542
(map #(str "analyze " %) small-tables)))
543+
544+
(set! *warn-on-reflection* warn-on-reflection-orig)

test/puppetlabs/puppetdb/scf/storage_test.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@
290290
:stable {:hostname "myhost" :kernel "Linux" :uptime_seconds 3600}
291291
:volatile {:domain "mynewdomain.com" :fqdn "myhost.mynewdomain.com"}}
292292
(-> m
293-
(update :hash sutils/parse-db-hash)
293+
(update :hash sutils/db-hash->hex)
294294
(update :paths_hash bytes->hex)
295295
(update :stable_hash bytes->hex)
296296
(update :stable sutils/parse-db-json)

0 commit comments

Comments
 (0)