{"id":19345929,"url":"https://github.com/tolitius/inquery","last_synced_at":"2025-04-23T04:36:37.654Z","repository":{"id":65351630,"uuid":"92897788","full_name":"tolitius/inquery","owner":"tolitius","description":"vanilla SQL with params for Clojure/Script","archived":false,"fork":false,"pushed_at":"2025-03-31T17:42:19.000Z","size":43,"stargazers_count":17,"open_issues_count":3,"forks_count":6,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-04-02T08:22:43.774Z","etag":null,"topics":["clojure","sql"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tolitius.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-05-31T02:53:35.000Z","updated_at":"2025-03-31T17:42:22.000Z","dependencies_parsed_at":"2023-01-19T07:15:30.316Z","dependency_job_id":null,"html_url":"https://github.com/tolitius/inquery","commit_stats":null,"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tolitius%2Finquery","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tolitius%2Finquery/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tolitius%2Finquery/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tolitius%2Finquery/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tolitius","download_url":"https://codeload.github.com/tolitius/inquery/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250372501,"owners_count":21419719,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","sql"],"created_at":"2024-11-10T04:08:22.478Z","updated_at":"2025-04-23T04:36:37.640Z","avatar_url":"https://github.com/tolitius.png","language":"Clojure","readme":"# inquery\n\nvanilla SQL with params for Clojure/Script\n\n* no DSL\n* no comments parsing\n* no namespace creations\n* no defs / defqueries\n* no dependencies\n* no edn SQL\n\njust \"read SQL with `:params`\"\n\n[![Clojars Project](https://clojars.org/tolitius/inquery/latest-version.svg)](http://clojars.org/tolitius/inquery)\n\n- [why](#why)\n- [using inquery](#using-inquery)\n  - [escaping](#escaping)\n  - [dynamic queries](#dynamic-queries)\n- [type safety](#type-safety)\n- [batch upserts](#batch-upserts)\n- [ClojureScript](#clojurescript)\n- [scratchpad](#scratchpad)\n- [license](#license)\n\n## why\n\nSQL is a great language, it is very expressive and exremely well optimized and supported by \"SQL\" databases.\nit needs no wrappers. it should live in its pure SQL form.\n\n`inquery` does two things:\n\n* reads SQL files\n* substitutes params at runtime\n\nClojure APIs cover all the rest\n\n## using inquery\n\n`inquery` is about SQL: it _does not_ require or force a particular JDBC library or a database.\n\nBut to demo an actual database conversation, this example will use \"[funcool/clojure.jdbc](http://funcool.github.io/clojure.jdbc/latest/)\" to speak to\na sample [H2](http://www.h2database.com/html/main.html) database since both of them are great.\n\nThere is nothing really to do other than to bring the queries into a map with a `make-query-map` function:\n\n```clojure\n$ make repl\n\n=\u003e (require '[inquery.core :as q]\n            '[jdbc.core :as jdbc])\n```\n\n`dbspec` along with a `set of queries` would usually come from `config.edn` / consul / etc :\n\n```clojure\n=\u003e (def dbspec {:subprotocol \"h2\"\n                :subname \"file:/tmp/solar\"})\n\n=\u003e (def queries (q/make-query-map #{:create-planets\n                                    :find-planets\n                                    :find-planets-by-mass\n                                    :find-planets-by-name}))\n```\n\n`inquiry` by default will look under `sql/*` path for queries. In this case \"[dev-resources](dev-resources)\" is in a classpath:\n\n```\n▾ dev-resources/sql/\n      create-planets.sql\n      find-planets-by-mass.sql\n      find-planets-by-name.sql\n      find-planets.sql\n```\n\nReady to roll, let's create some planets:\n\n```clojure\n=\u003e (with-open [conn (jdbc/connection dbspec)]\n     (jdbc/execute conn (:create-planets queries)))\n```\n\ncheck out the solar system:\n\n```clojure\n=\u003e (with-open [conn (jdbc/connection dbspec)]\n     (jdbc/fetch conn (:find-planets queries)))\n\n[{:id 1, :name \"Mercury\", :mass 330.2M}\n {:id 2, :name \"Venus\", :mass 4868.5M}\n {:id 3, :name \"Earth\", :mass 5973.6M}\n {:id 4, :name \"Mars\", :mass 641.85M}\n {:id 5, :name \"Jupiter\", :mass 1898600M}\n {:id 6, :name \"Saturn\", :mass 568460M}\n {:id 7, :name \"Uranus\", :mass 86832M}\n {:id 8, :name \"Neptune\", :mass 102430M}\n {:id 9, :name \"Pluto\", :mass 13.105M}]\n```\n\nfind all the planets with mass less or equal to the mass of Earth:\n\n```clojure\n=\u003e (with-open [conn (jdbc/connection dbspec)]\n     (jdbc/fetch conn (-\u003e (:find-planets-by-mass queries)\n                          (q/with-params {:max-mass 5973.6}))))\n\n[{:id 1, :name \"Mercury\", :mass 330.2M}\n {:id 2, :name \"Venus\", :mass 4868.5M}\n {:id 3, :name \"Earth\", :mass 5973.6M}\n {:id 4, :name \"Mars\", :mass 641.85M}\n {:id 9, :name \"Pluto\", :mass 13.105M}]\n```\n\nwhich planet is the most `art`sy:\n\n```clojure\n=\u003e (with-open [conn (jdbc/connection dbspec)]\n     (jdbc/fetch conn (-\u003e (:find-planets-by-name queries)\n                      (q/with-params {:name \"%art%\"}))))\n\n[{:id 3, :name \"Earth\", :mass 5973.6M}]\n```\n\n### escaping\n\nby default inquery will \"SQL escape\" all the parameters that need to be substituted in a query.\n\nin case you need to _not_ escape the params inquery has options to not escape the whole query with `{:esc :don't}`:\n\n```clojure\n=\u003e (with-open [conn (jdbc/connection dbspec)]\n     (jdbc/fetch conn (-\u003e (:find-planets-by-name queries)\n                      (q/with-params {:name \"%art%\"}\n                                     {:esc :don't}))))\n\n```\n\nor per individual parameter with `{:as val}`:\n\n```clojure\n=\u003e (with-open [conn (jdbc/connection dbspec)]\n     (jdbc/fetch conn (-\u003e (:find-planets-by-name queries)\n                      (q/with-params {:name {:as \"\"}\n                                      :mass 42}))))\n```\n\n#### things to note about escaping\n\n`nil`s are converted to \"null\":\n\n```clojure\n=\u003e (-\u003e \"name = :name\" (q/with-params {:name nil}))\n\"name = null\"\n```\n\n`{:as nil}` or `{:as \"\"}` are \"as is\", so it will be replaced with an empty string:\n\n```clojure\n=\u003e (-\u003e \"name = :name\" (q/with-params {:name {:as nil}}))\n\"name = \"\n\n=\u003e (-\u003e \"name = :name\" (q/with-params {:name {:as \"\"}}))\n\"name = \"\n```\n\n`\"\"` will become a \"SQL empty string\":\n\n```clojure\n=\u003e (-\u003e \"name = :name\" (q/with-params {:name \"\"}))\n\"name = ''\"\n```\n\nsee [tests](test/inquery/test/core.clj) for more examples.\n\n### dynamic queries\n\ninquery can help out with some runtime decision making to build SQL predicates.\n\n`with-preds` function takes a map of `{pred-fn sql-predicate}`.\u003cbr/\u003e\nfor each \"true\" predicate function its `sql-predicate` will be added to the query:\n\n```clojure\n=\u003e (q/with-preds \"select planet from solar_system where this = that\"\n                 {#(= 42 42) \"and type = :type\"})\n\n\"select planet from solar_system where this = that and type = :type\"\n```\n\n```clojure\n=\u003e (q/with-preds \"select planet from solar_system where this = that\"\n                 {#(= 42 42) \"and type = :type\"\n                  #(= 28 34) \"and size \u003c :max-size\"})\n\n\"select planet from solar_system where this = that and type = :type\"\n```\n\nif both predicates are true, both will be added:\n\n```clojure\n=\u003e (q/with-preds \"select planet from solar_system where this = that\"\n                  {#(= 42 42) \"and type = :type\"\n                   #(= 28 28) \"and size \u003c :max-size\"})\n\n\"select planet from solar_system where this = that and type = :type and size \u003c :max-size\"\n```\n\nsome queries don't come with `where` clause, for these cases `with-preds` takes a prefix:\n\n```clojure\n=\u003e (q/with-preds \"select planet from solar_system\"\n                  {#(= 42 42) \"and type = :type\"\n                   #(= 28 34) \"and size \u003c :max-size\"}\n                  {:prefix \"where\"})\n\n\"select planet from solar_system where type = :type\"\n```\n\ndeveloper will know the (first part of the) query, so this decision is not \"hardcoded\".\n\n```clojure\n=\u003e (q/with-preds \"select planet from solar_system\"\n                  {#(= 42 42) \"and type = :type\"\n                   #(= 34 34) \"and size \u003c :max-size\"}\n                  {:prefix \"where\"})\n\n\"select planet from solar_system where type = :type and size \u003c :max-size\"\n```\n\nin case none of the predicates are true, `\"where\"` prefix won't be used:\n\n```clojure\n=\u003e (q/with-preds \"select planet from solar_system\"\n                  {#(= 42 -42) \"and type = :type\"\n                   #(= 34 28) \"and size \u003c :max-size\"}\n                   {:prefix \"where\"})\n\n\"select planet from solar_system\"\n```\n\n## type safety\n\n### sql parameters\n\ninquery uses a type protocol `SqlParam` to safely convert clojure/script values to sql strings:\n\n```clojure\n(defprotocol SqlParam\n  \"safety first\"\n  (to-sql-string [this] \"trusted type will be SQL'ized\"))\n```\n\nit:\n\n* prevents sql injection\n* properly handles various data types\n* is extensible for custom types\n\ncommon types are handled out of the box:\n\n```clojure\n(q/to-sql-string nil)                                        ;; =\u003e \"null\"\n(q/to-sql-string \"earth\")                                    ;; =\u003e \"'earth'\"\n(q/to-sql-string \"pluto's moon\")                             ;; =\u003e \"'pluto''s moon'\"  ;; note proper escaping\n(q/to-sql-string 42)                                         ;; =\u003e \"42\"\n(q/to-sql-string true)                                       ;; =\u003e \"true\"\n(q/to-sql-string :jupiter)                                   ;; =\u003e \"'jupiter'\"\n(q/to-sql-string [1 2 nil \"mars\"])                           ;; =\u003e \"(1,2,null,'mars')\"\n(q/to-sql-string #uuid \"f81d4fae-7dec-11d0-a765-00a0c91e6bf6\") ;; =\u003e \"'f81d4fae-7dec-11d0-a765-00a0c91e6bf6'\"\n(q/to-sql-string #inst \"2023-01-15T12:34:56Z\")               ;; =\u003e \"'2023-01-15T12:34:56Z'\"\n(q/to-sql-string (java.util.Date.))                          ;; =\u003e \"'Wed Mar 26 09:42:17 EDT 2025'\"\n```\n\n### custom types\n\nyou can extend `SqlParam` protocol to handle custom types:\n\n```clojure\n(defrecord Planet [name mass])\n\n(extend-protocol inquery.core/SqlParam\n  Planet\n  (to-sql-string [planet]\n    (str \"'\" (:name planet) \" (\" (:mass planet) \" x 10^24 kg)'\")))\n\n(q/to-sql-string (-\u003ePlanet \"neptune\" 102)) ;; =\u003e \"'neptune (102 x 10^24 kg)'\"\n```\n\n### its built in\n\nno need to call \"`to-sql-string`\" of course, inquery does it internally:\n\n```clojure\n;; find planets discovered during specific time range with certain composition types\n(let [query \"SELECT * FROM planets\n             WHERE discovery_date BETWEEN :start_date AND :end_date\n             AND name NOT IN :excluded_planets\n             AND composition_type IN :allowed_types\n             AND is_habitable = :habitable\n             AND discoverer_id = :discoverer\"\n      params {:start_date (Instant/parse \"2020-01-01T00:00:00Z\")\n              :end_date (java.util.Date.)\n              :excluded_planets [\"mercury\" \"venus\" \"earth\"]\n              :allowed_types [:rocky :gas-giant :ice-giant]\n              :habitable true\n              :discoverer (UUID/fromString \"f81d4fae-7dec-11d0-a765-00a0c91e6bf6\")}]\n\n  (q/with-params query params))\n```\n```clojure\n;; =\u003e \"SELECT * FROM planets\n;;     WHERE discovery_date BETWEEN '2020-01-01T00:00:00Z' AND 'Wed Mar 26 09:48:32 EDT 2025'\n;;     AND name NOT IN ('mercury','venus','earth')\n;;     AND composition_type IN ('rocky','gas-giant','ice-giant')\n;;     AND is_habitable = true\n;;     AND discoverer_id = 'f81d4fae-7dec-11d0-a765-00a0c91e6bf6'\"\n```\n\n## batch upserts\n\ninquery provides functions to safely convert collections for batch operations:\n\n* `seq-\u003ebatch-params` - converts a sequence of sequences to a string suitable for batch inserts/updates\n* `seq-\u003eupdate-vals` - legacy version that quotes all values (even numbers)\n\n```clojure\n;; using seq-\u003ebatch-params for modern batch operations\n;; (perfect for cataloging newly discovered exoplanets)\n(q/seq-\u003ebatch-params [[42 \"earth\" 5973.6]\n                      [\"34\" nil \"saturn\"]])\n;; =\u003e \"(42,'earth',5973.6),('34',null,'saturn')\"\n\n;; safe handling of UUIDs, timestamps, and other complex types\n;; (for when you need to record celestial events)\n(let [uuid1 (UUID/fromString \"f81d4fae-7dec-11d0-a765-00a0c91e6bf6\")\n      timestamp (Instant/parse \"2023-01-15T12:34:56Z\")]\n  (q/seq-\u003ebatch-params [[uuid1 \"planet\" \"earth\" timestamp]]))\n;; =\u003e \"('f81d4fae-7dec-11d0-a765-00a0c91e6bf6','planet','earth','2023-01-15T12:34:56Z')\"\n```\n\nand the real SQL example:\n\n```clojure\n;; batch insert new celestial bodies with mixed data types\n(let [query \"INSERT INTO celestial_bodies\n              (id, name, type, mass, discovery_date, is_confirmed)\n             VALUES :bodies\"\n\n      ;; collection of [id, name, type, mass, date, confirmed?]\n      bodies [[#uuid \"c7a344f2-0243-4f92-8a96-bfc7ee482a9c\"\n               \"kepler-186f\"\n               :exoplanet\n               4.7\n               #inst \"2014-04-17T00:00:00Z\"\n               true]\n\n              [#uuid \"3236ebed-8248-4b07-a37e-c64c0a062247\"\n               \"toi-700d\"\n               :exoplanet\n               1.72\n               #inst \"2020-01-07T00:00:00Z\"\n               true]\n\n              [#uuid \"b29bc806-7db1-4e0c-93f7-fe5ee38ad1fa\"\n               \"proxima centauri b\"\n               :exoplanet\n               1.27\n               #inst \"2016-08-24T00:00:00Z\"\n               nil]]]\n\n  (q/with-params query {:bodies {:as (q/seq-\u003ebatch-params bodies)}}))\n```\n```clojure\n;; =\u003e \"INSERT INTO celestial_bodies\n;;       (id, name, type, mass, discovery_date, is_confirmed)\n;;     VALUES\n;;       ('c7a344f2-0243-4f92-8a96-bfc7ee482a9c','kepler-186f','exoplanet',4.7,'2014-04-17T00:00:00Z',true),\n;;       ('3236ebed-8248-4b07-a37e-c64c0a062247','toi-700d','exoplanet',1.72,'2020-01-07T00:00:00Z',true),\n;;       ('b29bc806-7db1-4e0c-93f7-fe5ee38ad1fa','proxima centauri b','exoplanet',1.27,'2016-08-24T00:00:00Z',null)\"\n```\n\n## ClojureScript\n\n```clojure\n$ lumo -i src/inquery/core.cljc --repl\nLumo 1.2.0\nClojureScript 1.9.482\n Docs: (doc function-name-here)\n Exit: Control+D or :cljs/quit or exit\n\ncljs.user=\u003e (ns inquery.core)\n```\n\ndepending on how a resource path is setup, an optional parameter `{:path \"...\"}`\ncould help to specify the path to queries:\n\n```clojure\ninquery.core=\u003e (def queries\n                 (make-query-map #{:create-planets\n                                   :find-planets\n                                   :find-planets-by-mass}\n                                 {:path \"dev-resources/sql\"}))\n#'inquery.core/queries\n```\n\n```clojure\ninquery.core=\u003e (print queries)\n\n{:create-planets -- create planets\ndrop table if exists planets;\ncreate table planets (id bigint auto_increment, name varchar, mass decimal);\n\ninsert into planets (name, mass) values ('Mercury', 330.2),\n                                        ('Venus', 4868.5),\n                                        ('Earth', 5973.6),\n                                        ('Mars', 641.85),\n                                        ('Jupiter', 1898600),\n                                        ('Saturn', 568460),\n                                        ('Uranus', 86832),\n                                        ('Neptune', 102430),\n                                        ('Pluto', 13.105);\n, :find-planets -- find all planets\nselect * from planets;\n, :find-planets-by-mass -- find planets under a certain mass\nselect * from planets where mass \u003c= :max-mass\n}\n```\n\n```clojure\ninquery.core=\u003e (-\u003e queries\n                   :find-planets-by-mass\n                   (with-params {:max-mass 5973.6}))\n\n-- find planets under a certain mass\nselect * from planets where mass \u003c= 5973.6\n```\n\n## scratchpad\n\ndevelopment [scratchpad](dev/scratchpad.clj) with sample shortcuts:\n\n```clojure\n$ make repl\n\n=\u003e (require '[scratchpad :as sp :refer [dbspec queries]])\n\n=\u003e (sp/execute dbspec (:create-planets queries))\n\n=\u003e (sp/fetch dbspec (:find-planets queries))\n\n[{:id 1, :name \"Mercury\", :mass 330.2M}\n {:id 2, :name \"Venus\", :mass 4868.5M}\n {:id 3, :name \"Earth\", :mass 5973.6M}\n {:id 4, :name \"Mars\", :mass 641.85M}\n {:id 5, :name \"Jupiter\", :mass 1898600M}\n {:id 6, :name \"Saturn\", :mass 568460M}\n {:id 7, :name \"Uranus\", :mass 86832M}\n {:id 8, :name \"Neptune\", :mass 102430M}\n {:id 9, :name \"Pluto\", :mass 13.105M}]\n\n=\u003e (sp/fetch dbspec (:find-planets-by-mass queries) {:max-mass 5973.6})\n\n[{:id 1, :name \"Mercury\", :mass 330.2M}\n {:id 2, :name \"Venus\", :mass 4868.5M}\n {:id 3, :name \"Earth\", :mass 5973.6M}\n {:id 4, :name \"Mars\", :mass 641.85M}\n {:id 9, :name \"Pluto\", :mass 13.105M}]\n```\n\n## license\n\nCopyright © 2025 tolitius\n\nDistributed under the Eclipse Public License either version 1.0 or (at\nyour option) any later version.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftolitius%2Finquery","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftolitius%2Finquery","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftolitius%2Finquery/lists"}