{"id":16800255,"url":"https://github.com/camsaul/toucan2","last_synced_at":"2025-12-12T01:34:06.882Z","repository":{"id":49515734,"uuid":"255854461","full_name":"camsaul/toucan2","owner":"camsaul","description":"Successor library to Toucan with a modern and more-extensible API, more consistent behavior, and support for different backends including non-JDBC databases and non-HoneySQL queries. Currently in active beta.","archived":false,"fork":false,"pushed_at":"2025-11-10T21:16:41.000Z","size":2256,"stargazers_count":111,"open_issues_count":74,"forks_count":14,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-12-08T04:04:07.684Z","etag":null,"topics":["clojure","sql","toucan"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-1.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/camsaul.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":"camsaul"}},"created_at":"2020-04-15T08:34:11.000Z","updated_at":"2025-12-08T01:52:49.000Z","dependencies_parsed_at":"2022-09-05T22:50:48.039Z","dependency_job_id":"4f584439-49c0-4914-9e5e-274193195e25","html_url":"https://github.com/camsaul/toucan2","commit_stats":{"total_commits":557,"total_committers":10,"mean_commits":55.7,"dds":0.1903052064631957,"last_synced_commit":"ea7fe293929ab910d8ee61f392d316dfead8a220"},"previous_names":["camsaul/bluejdbc"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/camsaul/toucan2","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/camsaul%2Ftoucan2","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/camsaul%2Ftoucan2/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/camsaul%2Ftoucan2/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/camsaul%2Ftoucan2/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/camsaul","download_url":"https://codeload.github.com/camsaul/toucan2/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/camsaul%2Ftoucan2/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":27673696,"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","status":"online","status_checked_at":"2025-12-11T02:00:11.302Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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","toucan"],"created_at":"2024-10-13T09:31:50.581Z","updated_at":"2025-12-12T01:34:06.847Z","avatar_url":"https://github.com/camsaul.png","language":"Clojure","funding_links":["https://github.com/sponsors/camsaul"],"categories":[],"sub_categories":[],"readme":"[![License](https://img.shields.io/badge/license-Eclipse%20Public%20License-blue.svg?style=for-the-badge)](https://raw.githubusercontent.com/camsaul/toucan2/master/LICENSE)\n[![GitHub last commit](https://img.shields.io/github/last-commit/camsaul/toucan2?style=for-the-badge)](https://github.com/camsaul/toucan2/commits/)\n[![Codecov](https://img.shields.io/codecov/c/github/camsaul/toucan2?style=for-the-badge)](https://codecov.io/gh/camsaul/toucan2)\n[![GitHub Sponsors](https://img.shields.io/github/sponsors/camsaul?style=for-the-badge)](https://github.com/sponsors/camsaul)\n[![cljdoc badge](https://img.shields.io/badge/dynamic/json?color=informational\u0026label=cljdoc\u0026query=results%5B%3F%28%40%5B%22artifact-id%22%5D%20%3D%3D%20%22toucan2%22%29%5D.version\u0026url=https%3A%2F%2Fcljdoc.org%2Fapi%2Fsearch%3Fq%3Dio.github.camsaul%2Ftoucan2\u0026style=for-the-badge)](https://cljdoc.org/d/io.github.camsaul/toucan2/CURRENT)\n[![Get help on Slack](http://img.shields.io/badge/slack-clojurians%20%23toucan-4A154B?logo=slack\u0026style=for-the-badge)](https://clojurians.slack.com/channels/toucan)\n\n\u003c!-- [![Downloads](https://versions.deps.co/camsaul/toucan2/downloads.svg)](https://versions.deps.co/camsaul/toucan2) --\u003e\n\u003c!-- [![Dependencies Status](https://versions.deps.co/camsaul/toucan2/status.svg)](https://versions.deps.co/camsaul/toucan2) --\u003e\n\n[![Clojars Project](https://clojars.org/io.github.camsaul/toucan2/latest-version.svg)](https://clojars.org/io.github.camsaul/toucan2)\n\n# T2: Toucan 2\n\n![Toucan 2](https://github.com/camsaul/toucan2/raw/master/assets/toucan2.png)\n\nToucan 2 is a library for delightful database interaction.\n\nAt the end of the day, almost every non-trivial project needs to interact with a\ndatabase. Toucan 2 makes its easy to query the data in your database in a\nconsistent way and define behaviors that should happen every time a row is\nretrieved, created, updated, or deleted.\n\nToucan 2 is a successor library to [Toucan](https://github.com/metabase/toucan)\nwith a modern and more-extensible API, more consistent behavior (less gotchas),\nsupport for different backends including non-JDBC databases and non-HoneySQL\nqueries, support for namespaced keywords, and with more useful utilities.\n\nToucan 2 uses [Honey SQL 2](https://github.com/seancorfield/honeysql),\n[`next.jdbc`](https://github.com/seancorfield/next-jdbc), and\n[Methodical](https://github.com/camsaul/methodical) under the hood. Everything\nis super efficient and reducible under the hood (even inserts, updates, and\ndeletes!) and magical (in a good way).\n\n## You Can Toucan: 12 Cool Things You Can Do with Toucan 2\n\n### REPL-friendly syntax\n\nToucan 2 is optimized to for *REPL-driven development*, It offers a high-level\ninterface that's quick and easy to use from the REPL.\n\nCompare a simple query with `next.jdbc` vs the equivalent way to do it in Toucan:\n\n```clj\n(require '[next.jdbc :as jdbc])\n\n(def db-spec\n  {:dbtype   \"postgresql\"\n   :dbname   \"toucan2\"\n   :host     \"localhost\"\n   :post     5432\n   :user     \"cam\"\n   :password \"cam\"})\n\n;;; next.jdbc\n\n(let [my-datasource (jdbc/get-datasource db-spec)]\n  (with-open [connection (jdbc/get-connection my-datasource)]\n    (jdbc/execute! connection [\"SELECT * FROM people WHERE name = ?\" \"Cam\"])))\n;; =\u003e\n[{:people/id 1\n  :people/name \"Cam\"\n  :people/created-at #inst \"2020-04-21T23:56:00.000000000-00:00\"}]\n\n;;; Toucan 2\n\n(require '[toucan2.core :as t2])\n\n(t2/select :conn db-spec \"people\" :name \"Cam\")\n;; =\u003e\n[{:id 1\n  :name \"Cam\"\n  :created-at #object[java.time.OffsetDateTime 0x2c8ec7ed \"2020-04-21T23:56Z\"]}]\n\n;;; next.jdbc + Honey SQL 2\n\n(require '[honey.sql :as sql])\n\n(let [my-datasource (jdbc/get-datasource db-spec)]\n  (with-open [connection (jdbc/get-connection my-datasource)]\n    (jdbc/execute! connection (sql/format {:select [:*]\n                                           :from   [:people]\n                                           :where  [:like :name \"C%\"]}))))\n;; =\u003e\n[{:people/id 1\n  :people/name \"Cam\"\n  :people/created-at #inst \"2020-04-21T23:56:00.000000000-00:00\"}]\n\n;;; Toucan 2\n\n(t2/select :conn db-spec \"people\" :name [:like \"C%\"])\n;; =\u003e\n[{:id 1\n  :name \"Cam\"\n  :created-at #object[java.time.OffsetDateTime 0x2c8ec7ed \"2020-04-21T23:56Z\"]}]\n```\n\n### Define a default connection\n\nAs you can see, Toucan 2 is quick and easy to use from the REPL. But it can be\neven easier! Typing `dbspec` over and over can get tedious. Toucan 2 lets you\ndefine a default connection:\n\n```clj\n(require '[methodical.core :as m])\n\n(m/defmethod t2/do-with-connection :default\n  [_connectable f]\n  (t2/do-with-connection db-spec f))\n\n(t2/select \"people\" :name [:like \"C%\"])\n;; =\u003e\n[{:id 1\n  :name \"Cam\"\n  :created-at #object[java.time.OffsetDateTime 0x2bf3111d \"2020-04-21T23:56Z\"]}]\n```\n\nYou can also define a default connection for specific tables (*models*). For\nmore information, see [Connections](/docs/connections.md).\n\n### Define models\n\nAs we'll see below, you can define lots of arbitrary behaviors when selecting,\nupdating, inserting, and deleting rows from various tables in your database.\nThese behaviors are encapsulated in multimethods that are triggered for what\nToucan 2 calls *models*, which are usually just plain Clojure keywords.\n\nBy default, Toucan 2 will use the `name` of a model keyword as the table name to\nuse when querying it. Let's try using a model called `:model/people`:\n\n```clj\n(t2/table-name :model/people)\n;; =\u003e :people\n\n;; Select one row from :people with the primary key 1\n(t2/select-one :model/people 1)\n;; =\u003e\n{:id 1\n :name \"Cam\"\n :created-at #object[java.time.OffsetDateTime 0x2c8ec7ed \"2020-04-21T23:56Z\"]}\n```\n\nGoing forward, we'll be deriving a lot of models from `:models/people`. To make\nnew models like `:models/people.cool` use the right table name, let's tell\nToucan 2 to always use `people` as the table name for anything deriving from\n`:models/people`:\n\n```clj\n(m/defmethod t2/table-name :models/people\n  [_model]\n  :people)\n```\n\nTo learn more about models, see [Models](/docs/models.md).\n\n### Define transforms\n\nToucan 2 is much more than just a glorified collection of helper functions.\nSuppose we have a column that we would like to automatically be converted to\nkeywords whenever it comes out of the database, and automatically be converted\nback to strings when it goes back into the database. With Toucan 2, you can\neasily define column transformations with\n[`deftransforms`](https://cljdoc.org/d/io.github.camsaul/toucan2/CURRENT/api/toucan2.tools.transformed#deftransforms).\n\nLet's define a new model, so we don't affect `:models/people` itself.\n\n```clj\n(derive :models/people.keyword-name :models/people)\n\n(t2/deftransforms :models/people.keyword-name\n  {:name {:in  name\n          :out keyword}})\n\n(t2/select :models/people.keyword-name :name :Cam)\n;; =\u003e\n[{:id 1\n  :name :Cam\n  :created-at #object[java.time.OffsetDateTime 0x3af24cf \"2020-04-21T23:56Z\"]}]\n```\n\nWhenever a non-nil value goes in to the database, Toucan 2 calls `name` on it;\nwhen a non-nil value comes out, Toucan 2 calls `keyword` on it. For more info,\nsee [Transforms](/docs/transforms.md).\n\n### Define behavior after selecting something\n\nSometimes we want to do more general things than just transform a single column\nto another value. You can use tools like\n[`define-after-select`](https://cljdoc.org/d/io.github.camsaul/toucan2/CURRENT/api/toucan2.tools.after-select#define-after-select)\nto define more general transformations for results, such as adding additional\ncolumns, or to trigger some other behavior for side effects. Let's say we want\nto give all our people cool names when they come out of the database.\n\n```clj\n(derive :models/people.cool :models/people)\n\n(t2/define-after-select :models/people.cool\n  [person]\n  (assoc person :cool-name (str \"Cool \" (:name person))))\n\n(t2/select-one :models/people.cool 1)\n;; =\u003e\n{:name       \"Cam\"\n :cool-name  \"Cool Cam\"\n :id         1\n :created-at #object[java.time.OffsetDateTime 0x2691c719 \"2020-04-21T23:56Z\"]}\n```\n\nThis method is not applied when you use `:models/people` or other models derived\nfrom it, unless those models derive from `:models/people.cool`. You can define\n`before-` and `after-` methods for `select`, `insert`, `update`, and `delete`.\nFor more information, see [Before \u0026 After Methods](/docs/before-and-after.md).\n\n### Compose behaviors\n\nBecause Toucan 2 is implemented with multimethods, you can compose various\nbehaviors by simply deriving a model from one or more others. Built-in Toucan 2\ntools like\n[`deftransforms`](https://cljdoc.org/d/io.github.camsaul/toucan2/CURRENT/api/toucan2.tools.transformed#deftransforms)\nand\n[`define-after-select`](https://cljdoc.org/d/io.github.camsaul/toucan2/CURRENT/api/toucan2.tools.after-select#define-after-select)\nautomatically compose.\n\nLet's define another after-select method, `::without-created-at`, to remove the\n`:created-at` key from our results, then create a new model that combines\n`:models/people.cool` with `::without-created-at` to get **both** behaviors:\n\n```clj\n(t2/define-after-select ::without-created-at\n  [row]\n  (dissoc row :created-at))\n\n(derive :models/people.cool.without-created-at :models/people.cool)\n(derive :models/people.cool.without-created-at ::without-created-at)\n\n;;; Tell Methodical to call the :models.people.cool method before the\n;;; ::without-created-at method\n(m/prefer-method! #'toucan2.tools.after-select/after-select\n                  :models/people.cool\n                  ::without-created-at)\n\n(t2/select-one :models/people.cool.without-created-at 1)\n;; =\u003e\n{:name \"Cam\", :cool-name \"Cool Cam\", :id 1}\n```\n\n### Define named queries\n\nOften you'll want to write a big complicated query:\n\n```clj\n(t2/select \"venues\" {:select    [:venues.id\n                                 [:venues.name :venue-name]\n                                 [:category.name :category-name]\n                                 [:category.slug :category-slug]]\n                     :from      [:venues]\n                     :left-join [:category [:= :venues.category :category.name]]})\n;; =\u003e\n[{:id 1, :venue-name \"Tempest\", :category-name \"bar\", :category-slug \"bar_01\"}\n {:id 2, :venue-name \"Ho's Tavern\", :category-name \"bar\", :category-slug \"bar_01\"}\n {:id 3, :venue-name \"BevMo\", :category-name \"store\", :category-slug \"store_02\"}\n ...]\n```\n\nThis is not REPL-friendly! With Toucan 2, you can use\n[`define-named-query`](https://cljdoc.org/d/io.github.camsaul/toucan2/CURRENT/api/toucan2.tools.named-query#define-named-query)\nto define named queries to use again later:\n\n```clj\n(t2/define-named-query ::venues-with-categories\n  {:select    [:venues.id\n               [:venues.name :venue-name]\n               [:category.name :category-name]\n               [:category.slug :category-slug]]\n   :from      [:venues]\n   :left-join [:category [:= :venues.category :category.name]]})\n\n(t2/select \"venues\" ::venues-with-categories)\n;; =\u003e\n[{:id 1, :venue-name \"Tempest\", :category-name \"bar\", :category-slug \"bar_01\"}\n {:id 2, :venue-name \"Ho's Tavern\", :category-name \"bar\", :category-slug \"bar_01\"}\n {:id 3, :venue-name \"BevMo\", :category-name \"store\", :category-slug \"store_02\"}\n ...]\n```\n\nYou can even combine those queries with additional constraints:\n\n```clj\n(t2/select \"venues\" :venues.name [:like \"Temp%\"] ::venues-with-categories)\n;; =\u003e\n{:id 1, :venue-name \"Tempest\", :category-name \"bar\", :category-slug \"bar_01\"}\n```\n\nYou can even define different versions of named queries to use for different\nmodels. For example, you may want to have some sort of analytics query for\nseveral different tables in your database. Define a different version of\n`:analytics-query` for each of them!\n\nTo learn more about query resolution and named queries, see [Query\nResolution](/docs/query-resolution.md).\n\n### Define default fields\n\nTODO\n\n### Disallow update, delete, or insert\n\nTODO\n\n### Get the model associated with an instance\n\nTODO\n\n### Record and `save!` changes made to an instance\n\nTODO\n\n### Define custom keyword-arg behavior\n\nTODO\n\nFor more information, see [Query Compilation \u0026 Map Backends](query-compilation.md)\n\n# `toucan2-toucan1`\n\n[![Clojars Project](https://clojars.org/io.github.camsaul/toucan2-toucan1/latest-version.svg)](https://clojars.org/io.github.camsaul/toucan2-toucan1)\n\nCompatibility layer for projects using [Toucan\n1](https://github.com/metabase/toucan) to ease transition to Toucan 2.\nImplements the same namespaces as Toucan 1, but they are implemented on top of\nToucan 2. Projects using Toucan 1 can remove their dependency on `toucan`, and\ninclude a dependency on `io.github.camsaul/toucan2-toucan1` in its place; with a\nfew changes your project should work without having to switch everything to\nToucan 2 all at once. More details on this coming soon.\n\nSee [`toucan2-toucan1` docs](/toucan1/README.md) for more information.\n\n## License\n\nCode, documentation, and artwork copyright © 2017-2023 [Cam\nSaul](https://camsaul.com).\n\nDistributed under the [Eclipse Public\nLicense](https://raw.githubusercontent.com/camsaul/toucan2/master/LICENSE), same\nas Clojure.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcamsaul%2Ftoucan2","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcamsaul%2Ftoucan2","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcamsaul%2Ftoucan2/lists"}