Skip to content

Commit

Permalink
fix #274 by adding aggregate-by-keys
Browse files Browse the repository at this point in the history
Signed-off-by: Sean Corfield <sean@corfield.org>
  • Loading branch information
seancorfield committed Mar 16, 2024
1 parent 10fd00a commit 3042079
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Only accretive/fixative changes will be made from now on.

* 1.3.next in progress
* Address [#275](https://github.com/seancorfield/next-jdbc/issues/275) by noting that PostgreSQL may perform additional SQL queries to produce table names used in qualified result set builders.
* Address [#274](https://github.com/seancorfield/next-jdbc/issues/274) by adding `next.jdbc.sql/aggregate-by-keys` as a convenient wrapper around `find-by-keys` when you want just a single aggregate value back (such as `count`, `max`, etc).
* Address [#273](https://github.com/seancorfield/next-jdbc/issues/273) by linking to [PG2](https://github.com/igrishaev/pg2) in the PostgreSQL **Tips & Tricks** section.
* Address [#268](https://github.com/seancorfield/next-jdbc/issues/268) by expanding the documentation around `insert-multi!` and `insert!`.
* Update dependency versions (including Clojure).
Expand Down
4 changes: 4 additions & 0 deletions doc/all-the-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ In the simple case, the `:columns` option expects a vector of keywords and each

> Note: `get-by-id` accepts the same options as `find-by-keys` but it will only ever produce one row, as a hash map, so sort order and pagination are less applicable, although `:columns` may be useful.
As of 1.3.next, `aggregate-by-keys` exists as a wrapper around `find-by-keys`
that accepts the same options as `find-by-keys` except that `:columns` may not
be specified (since it is used to add the aggregate to the query).

## Generating Rows and Result Sets

Any function that might realize a row or a result set will accept:
Expand Down
26 changes: 26 additions & 0 deletions doc/friendly-sql-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ These functions are described in more detail below. They are deliberately simple

If you prefer to write your SQL separately from your code, take a look at [HugSQL](https://github.com/layerware/hugsql) -- [HugSQL documentation](https://www.hugsql.org/) -- which has a `next.jdbc` adapter, as of version 0.5.1. See below for a "[quick start](#hugsql-quick-start)" for using HugSQL with `next.jdbc`.

As of 1.3.next, `aggregate-by-keys` exists as a wrapper around `find-by-keys`
that accepts the same options as `find-by-keys` and an aggregate SQL expression
and it returns a single value (the aggregate). `aggregate-by-keys` accepts the
same options as `find-by-keys` except that `:columns` may not be specified
(since it is used to add the aggregate to the query).

## `insert!`

Given a table name (as a keyword) and a hash map of column names and values, this performs a single row insertion into the database:
Expand Down Expand Up @@ -247,6 +253,26 @@ If you want to match all rows in a table -- perhaps with the pagination options

If no rows match, `find-by-keys` returns `[]`, just like `execute!`.

## `aggregate-by-keys`

Added in 1.3.next, this is a wrapper around `find-by-keys` that makes it easier
to perform aggregate queries::

```clojure
(sql/aggregate-by-keys ds :address "count(*)" {:name "Stella"
:email "stella@artois.beer"})
;; is roughly equivalent to
(-> (sql/find-by-keys ds :address {:name "Stella" :email "stella@artois.beer"}
{:columns [["count(*)" :next_jdbc_aggregate_123]]})
(first)
(get :next_jdbc_aggregate_123))
```

(where `:next_jdbc_aggregate_123` is a unique alias generated by `next.jdbc`,
derived from the aggregate expression string).

> Note: the SQL string provided for the aggregate is copied exactly as-is into the generated SQL -- you are responsible for ensuring it is legal SQL!
## `get-by-id`

Given a table name (as a keyword) and a primary key value, with an optional primary key column name, execute a query on the database:
Expand Down
9 changes: 9 additions & 0 deletions src/next/jdbc/specs.clj
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,15 @@
:all #{:all})
:opts (s/? ::opts-map)))

(s/fdef sql/aggregate-by-keys
:args (s/cat :connectable ::connectable
:table keyword?
:aggregate string?
:key-map (s/or :example ::example-map
:where ::sql-params
:all #{:all})
:opts (s/? ::opts-map)))

(s/fdef sql/get-by-id
:args (s/alt :with-id (s/cat :connectable ::connectable
:table keyword?
Expand Down
45 changes: 41 additions & 4 deletions src/next/jdbc/sql.clj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
;; copyright (c) 2019-2023 Sean Corfield, all rights reserved
;; copyright (c) 2019-2024 Sean Corfield, all rights reserved

(ns next.jdbc.sql
"Some utility functions that make common operations easier by
Expand All @@ -21,10 +21,11 @@
In addition, `find-by-keys` supports `:order-by` to add an `ORDER BY`
clause to the generated SQL."
(:require [next.jdbc :refer [execute! execute-one! execute-batch!]]
(:require [clojure.string :as str]
[next.jdbc :refer [execute! execute-batch! execute-one!]]
[next.jdbc.sql.builder
:refer [for-delete for-insert for-insert-multi
for-query for-update]]))
:refer [for-delete for-insert for-insert-multi for-query
for-update]]))

(set! *warn-on-reflection* true)

Expand Down Expand Up @@ -138,6 +139,42 @@
(let [opts (merge (:options connectable) opts)]
(execute! connectable (for-query table key-map opts) opts))))

(defn aggregate-by-keys
"A wrapper over `find-by-keys` that additionally takes an aggregate SQL
expression (a string), and returns just a single result: the value of that
of that aggregate for the matching rows.
Accepts all the same options as `find-by-keys` except `:columns` since that
is used internally by this wrapper to pass the aggregate expression in."
([connectable table aggregate key-map]
(aggregate-by-keys connectable table aggregate key-map {}))
([connectable table aggregate key-map opts]
(let [opts (merge (:options connectable) opts)
_
(when-not (string? aggregate)
(throw (IllegalArgumentException.
"aggregate-by-keys requires a string aggregate expression")))
_
(when (:columns opts)
(throw (IllegalArgumentException.
"aggregate-by-keys does not support the :columns option")))

;; this should be unique enough as an alias to never clash with
;; a real column name in anyone's tables -- in addition it is
;; stable for a given aggregate expression so it should allow
;; for query caching in the JDBC driver:
;; (we use abs to avoid negative hash codes which would produce
;; a hyphen in the alias name which is not valid in SQL identifiers)
total-name (str "next_jdbc_aggregate_"
(Math/abs (.hashCode ^String aggregate)))
total-column (keyword total-name)
;; because some databases return uppercase column names:
total-col-u (keyword (str/upper-case total-name))]
(-> (find-by-keys connectable table key-map
(assoc opts :columns [[aggregate total-column]]))
(first)
(as-> row (or (get row total-column) (get row total-col-u)))))))

(defn get-by-id
"Syntactic sugar over `execute-one!` to make certain common queries easier.
Expand Down
18 changes: 17 additions & 1 deletion test/next/jdbc/sql_test.clj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
;; copyright (c) 2019-2023 Sean Corfield, all rights reserved
;; copyright (c) 2019-2024 Sean Corfield, all rights reserved

(ns next.jdbc.sql-test
"Tests for the syntactic sugar SQL functions."
Expand Down Expand Up @@ -58,6 +58,22 @@
(is (every? meta rs))
(is (= 2 ((column :FRUIT/ID) (first rs)))))))

(deftest test-aggregate-by-keys
(let [ds-opts (jdbc/with-options (ds) (default-options))]
(let [count-v (sql/aggregate-by-keys ds-opts :fruit "count(*)" {:appearance "neon-green"})]
(is (number? count-v))
(is (= 0 count-v)))
(let [count-v (sql/aggregate-by-keys ds-opts :fruit "count(*)" {:appearance "yellow"})]
(is (= 1 count-v)))
(let [count-v (sql/aggregate-by-keys ds-opts :fruit "count(*)" :all)]
(is (= 4 count-v)))
(let [max-id (sql/aggregate-by-keys ds-opts :fruit "max(id)" :all)]
(is (= 4 max-id)))
(let [min-name (sql/aggregate-by-keys ds-opts :fruit "min(name)" :all)]
(is (= "Apple" min-name)))
(is (thrown? IllegalArgumentException
(sql/aggregate-by-keys ds-opts :fruit "count(*)" :all {:columns []})))))

(deftest test-get-by-id
(let [ds-opts (jdbc/with-options (ds) (default-options))]
(is (nil? (sql/get-by-id ds-opts :fruit -1)))
Expand Down

0 comments on commit 3042079

Please sign in to comment.