{"id":32184731,"url":"https://github.com/exoscale/seql","last_synced_at":"2025-10-21T23:56:17.715Z","repository":{"id":46159395,"uuid":"206250461","full_name":"exoscale/seql","owner":"exoscale","description":"Simplfied EDN Query Language for SQL","archived":false,"fork":false,"pushed_at":"2024-01-31T08:55:22.000Z","size":218,"stargazers_count":117,"open_issues_count":0,"forks_count":5,"subscribers_count":27,"default_branch":"master","last_synced_at":"2025-10-21T23:56:03.763Z","etag":null,"topics":["clojure","database","sql"],"latest_commit_sha":null,"homepage":"https://cljdoc.org/d/exoscale/seql/","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/exoscale.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2019-09-04T06:43:43.000Z","updated_at":"2025-07-02T11:17:00.000Z","dependencies_parsed_at":"2023-09-22T15:10:18.125Z","dependency_job_id":null,"html_url":"https://github.com/exoscale/seql","commit_stats":{"total_commits":124,"total_committers":10,"mean_commits":12.4,"dds":0.5725806451612903,"last_synced_commit":"802e25110ae4eb01913ed95228160a154b04db38"},"previous_names":[],"tags_count":33,"template":false,"template_full_name":null,"purl":"pkg:github/exoscale/seql","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exoscale%2Fseql","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exoscale%2Fseql/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exoscale%2Fseql/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exoscale%2Fseql/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/exoscale","download_url":"https://codeload.github.com/exoscale/seql/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exoscale%2Fseql/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":280354181,"owners_count":26316400,"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-10-21T02:00:06.614Z","response_time":58,"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","database","sql"],"created_at":"2025-10-21T23:56:12.260Z","updated_at":"2025-10-21T23:56:17.708Z","avatar_url":"https://github.com/exoscale.png","language":"Clojure","readme":"seql: Simplified EDN Query Language\n===================================\n\n**seql** intends to provide a simplified\n[EQL](https://edn-query-language.org/) inspired query language to\naccess entities stored in traditional SQL databases.\n![](https://github.com/exoscale/seql/workflows/Clojure/badge.svg)\n[![cljdoc badge](https://cljdoc.org/badge/exoscale/seql)](https://cljdoc.org/d/exoscale/seql/CURRENT)\n[![Clojars Project](https://img.shields.io/clojars/v/exoscale/seql.svg)](https://clojars.org/exoscale/seql)\n\n## Introduction\n\nAccessing SQL entities is often done based on a pre-existing\nschema. In most designs, applications strive to limit the number of\nways mutations should happen on SQL. However, queries often need to be\nvery flexible in the type of data they return as well in the number of\njoins performed.\n\nWith this rationale in mind, **seql** was built to provide:\n\n- A data-based schema syntax to describe entities stored in SQL, as\n  well as their relations to each other, making no assumptions on the\n  database layout\n- A mapping between maps described with clojure.spec and records stored\n  in databases with implicit coercion support.\n- A subset of the schema dedicated to expressing mutations and their\n  input to allow for validation at the edge\n- A query builder allowing ad-hoc relations to be expressed\n- A mutation handler\n- A *listener* facility to support CQRS approaches\n\n**seql** relies on three key libraries:\n\n- [next.jdbc](https://github.com/seancorfield/next.jdbc) to provide SQL access\n- [honeysql](https://github.com/seancorfield/honeysql) to build SQL queries from data\n- [coax](https://github.com/exoscale/coax) for coercion of records\n\nWhere applicable, these dependencies are made obvious.\n\n## Changelog\n\n### 0.2.2\n\n- Deeper spec integration\n- Idents and conditions are now infered\n\n### 0.1.29\n\n- Upgrade to recent `next.jdbc` and `honeysql`\n\n### 0.1.2\n\nMinor fixes for errors spotted by eastwood.\n\n### 0.1.1\n\nInital release.\n\n## Thanks\n\nThis project was greatly inspired by @wilkerlucio's work on\n[pathom](https://github.com/wilkerlucio/pathom) and subsequently\n[EQL](https://github.com/edn-query-language/eql).  As a first consumer\nof this library @davidjacot also helped iron out a few kinks and made\nsome significant improvements. The 0.2.x release benefited from the\nsound advice and reviews of @arnaudgeiser and @mpenet.\n\n## Quickstart\n\nLet us assume the following - admittedly flawed - schema, for which we\nwill add gradual support:\n\n![schema](https://i.imgur.com/DkBtyew.png)\n\nAll the following examples can be reproduced in the\n`test/seql/readme_test.clj` integration test. To perform queries, an\n*environment* must be supplied, which consists of a schema, and a JDBC\nconfig. In `test/seql/fixtures.clj`, code is provided to experiment with an H2\ndatabase.\n\nFor all schemas displayed below, we assume an env set up in the following\nmanner:\n\n```clojure\n(def env {:schema ... :jdbc your-database-config})\n\n(require '[seql.query :as q])\n(require '[seql.lister :as l])\n(require '[seql.mutation :as m])\n(require '[clojure.spec.alpha :as s])\n(require '[seql.helpers :refer [make-schema ident idents field mutation\n                                has-many condition entity-from-spec]])\n```\n\n### Specs for the schema\n\nSeql assumes you are familiar with `clojure.spec` if that is not\nthe case, please refer to: https://clojure.org/guides/spec\n\nWe can start by providing specs for the individual fields in each\ntable:\n\n``` clojure\n(create-ns 'my.entities)\n(create-ns 'my.entities.account)\n(create-ns 'my.entities.user)\n(create-ns 'my.entities.invoice)\n(create-ns 'my.entities.invoice-line)\n(create-ns 'my.entities.product)\n\n\n(alias 'account 'my.entities.account)\n(alias 'user 'my.entities.user)\n(alias 'invoice 'my.entities.invoice)\n(alias 'invoice-line 'my.entities.invoice-line)\n(alias 'product 'my.entities.product)\n\n(s/def ::account/name string?)\n(s/def ::account/state #{:active :suspended :terminated})\n(s/def ::account/account (s/keys :req [::account/name ::account/state]))\n\n(s/def ::user/name string?)\n(s/def ::user/email string?)\n(s/def ::user/user (s/keys :req [::user/name ::user/email]))\n\n(s/def ::invoice/state keyword?)\n(s/def ::invoice/total nat-int?)\n(s/def ::invoice/invoice (s/keys :req [::invoice/state ::invoice/total]))\n\n(s/def ::invoice-line/quantity nat-int?)\n(s/def ::invoice-line/invoice-line (s/keys :req [::invoice-line/quantity]))\n\n(s/def ::product/name string?)\n(s/def ::product/product (s/keys :req [::product/name]))\n```\n\n### Queries on accounts\n\nAt first, accounts need to be looked up. We can build a minimal schema:\n\n```clojure\n(make-schema\n  (entity ::account/account\n\t\t  (field :name)\n\t\t  (field :state)))\n```\n\nLet's unpack things here:\n\n- We give a name our entity, by default it will be assumed that the\n  SQL table it resides in is eponymous, when it is not the case, a\n  tuple of `[entity-name table-name]` can be provided\n- We declare a list of fields known to exist in that table.\n\nWith this, simple queries can be performed:\n\n```clojure\n(query env ::account/account [::account/name ::account/state])\n;; or to fetch all default fields:\n(query env ::account/account)\n\n;; =\u003e\n\n[#::account{:name \"a0\" :state :active}\n #::account{:name \"a1\" :state :active}\n #::account{:name \"a2\" :state :suspended}]\n```\n\nIdents can also be looked up:\n\n```clojure\n(query env [::account/id 0] [::account/name ::account/state])\n\n;; =\u003e\n\n#::account{:name \"a0\" :state :active}\n```\n\nNotice how the last query yielded a single value instead of a collection.\nIt is expected that idents will yield at most a single value (as a corollary,\nidents should only be used for database fields which enforce this guarantee).\n\nAlso notice how there was no prior mention of `::account/id`\n\n### Infering schemas from specs\n\nA first concrete improvement we can bring to the schema build step when\nan `s/keys` spec is available for our entity is to infer most of the schema\nfrom it:\n\n``` clojure\n(make-schema\n  (entity-from-spec ::account/account))\n```\n\nWe can now perform the following query:\n\n``` clojure\n(query env ::account/account [::account/name] [[::account/state :active]])\n\n;; =\u003e\n\n[#::account{:name \"a0\"}\n #::account{:name \"a1\"}]\n\n\n(query env ::account/account [::account/name] [[::account/state :suspended]])\n\n;; =\u003e\n\n[#::account{:name \"a2\"}]\n```\n\n### Adding a relation\n\nFor queries, **seql**'s strength lies in its ability to understand the\nway entities are tied together. **Seql** offers support for\none-to-many (*has many*), one-to-one (*has one*), and many-to-many\n(*has many through*) relations.\n\nLet's start with a single relation before building larger nested\ntrees. Since no assumption is made on schemas, the relations must\nspecify foreign keys explictly:\n\n```clojure\n(make-schema\n  (entity-from-spec ::account/account\n    (has-many ::users [:id ::user/account-id]))\n\n  (entity-from-spec ::user/user))\n```\n\nThis will allow doing tree lookups, fetching arbitrary fields from the\nnested entity as well:\n\n```clojure\n(query env\n       ::account/account\n       [::account/name\n        ::account/state\n        {::account/users [::user/name ::user/email]}])\n\n;; =\u003e\n\n[#::account{:name  \"a0\"\n            :state :active\n            :users [#::user{:name \"u0a0\" :email \"u0@a0\"}\n                    #::user{:name \"u1a0\" :email \"u1@a0\"}]}\n #::account{:name  \"a1\"\n            :state :active\n            :users [#::user{:name \"u2a1\" :email \"u2@a1\"}\n                    #::user{:name \"u3a1\" :email \"u3@a1\"}]}\n #::account{:name \"a2\" :state :suspended}]\n```\n\n### Summary of query description\n\nWe've now covered full capabilities of the *query* part of the schema,\nwere we saw that:\n\n- Each entity should have a *table*.\n- To provide more idiomatic output, spec based coercions are\n  performed in and out of the database.\n- *Conditions* allow for building advanced filters on entities.\n- To build arbitrarily nested entities, *relations* need to be used.\n\nWith this in mind, here's a complete schema for the above database\nschema:\n\n```clojure\n(make-schema\n (entity-from-spec ::account/account\n            (has-many :users    [:id ::user/account-id])\n            (has-many :invoices [:id ::invoice/account-id]))\n (entity-from-spec ::user/user)\n (entity-from-spec ::invoice/invoice\n            (has-many :lines    [:id ::invoice-line/invoice-id]))\n (entity-from-spec ::product/product)\n (entity-from-spec [::invoice-line/invoice-line :invoiceline]\n            (has-one :product [:product-id ::product/id])))\n```\n\n### Controlling the mapping betwen row and column names in the database\n\nSpecific table names can be provided by using a vector as the argument\nfor `entity` or `entity-from-spec`:\n\n``` clojure\n(make-schema\n  (entity-from-spec [::invoice-line/invoice-line :invoiceline]\n    ...))\n```\n\nSpecific column names can be provided by using the `column-name` helper:\n\n``` clojure\n(make-schema\n  (entity-from-spec ::network/network\n    (column-name :ip6address :ip6)\n    ...))\n```\n\n### Mutations\n\nWith querying sorted, mutations need to be expressed. Here, **seql**\ntakes the approach of making mutations separate, explictit, and\nvalidated. As with most other **seql** features, mutations are\nimplemented with a key inside the entity description.\n\nAt its core, mutations expect two things:\n\n- A **spec** of their input\n- A function of this input which must yield a proper **honeysql** query map, or collection\n  of **honeysql** query map to be performed in a transaction.\n\nFor the common case of inserting, updating, or deleting records from the database,\na couple of schema helpers are provided.\n\n\n#### Inserting records with `add-create-mutation`\n\nTo allow record insertion, use the `add-create-mutation` helper:\n\n```clojure\n (entity-from-spec ::account/account\n            (has-many :users    [:id ::user/account-id])\n            (has-many :invoices [:id ::invoice/account-id])\n            (add-create-mutation))\n```\n\nThe implicit mutation created by `add-create-mutation` will be\nnamed: `::account/create`, a spec has to exist for it, as for all\nmutations. Since `spec/valid?` runs on input parameters before handing\nout to mutation functions it should always be present (otherwise mutations\nwill throw early).\n\n```clojure\n(s/def ::account/create ::account/account)\n```\n\nAdding new accounts can now be done through `mutate!`:\n\n```clojure\n(mutate! env ::account/create {::account/name  \"a3\"\n                               ::account/state :active})\n\n(query env [::account/name \"a3\"] [::account/state])\n\n;; =\u003e\n\n#::account{:state :active}\n```\n\n#### Updating records with `add-update-by-id-mutation`\n\nTo allow record updates, use the `add-update-by-id-mutation` helper:\n\n``` clojure\n (entity-from-spec ::account/account\n            (has-many :users    [:id ::user/account-id])\n            (has-many :invoices [:id ::invoice/account-id])\n            (add-create-mutation)\n            (add-update-by-id-mutation ::account/id))\n```\n\nThis instructs the helper that the input map to the mutation function\nwill contain a `::account/id` field which should be used to determine\nwhich row to update in the database. The rest of the map contents will\nbe treated as values to update in the database.\n\n#### Deleting records with `add-delete-by-id-mutation`\n\nTo allow record deletes, use the `add-delete-by-id-mutation` helper:\n\n``` clojure\n (entity-from-spec ::account/account\n            (has-many :users    [:id ::user/account-id])\n            (has-many :invoices [:id ::invoice/account-id])\n            (add-create-mutation)\n            (add-update-by-id-mutation ::account/id)\n            (add-delete-by-id-mutation ::account/id))\n```\nThis instructs the helper that the input map to the mutation function\nwill contain a `::account/id` field which should be used to determine\nwhich row to delete from the database.\n\n#### Arbitrary mutations with `mutation-fn`\n\nIt is hard to predict all types of mutations, and often times, any such attempt\nresults in worse ergonomics than what SQL provideds. To this end, `seql` allows\nproviding arbitrary SQL expressions as mutations through the help of `honeysql`\n\n``` clojure\n(entity-from-spec ::account/account\n (has-many :users    [:id ::user/account-id])\n (has-many :invoices [:id ::invoice/account-id])\n (mutation-fn :remove-users (s/keys :req [::account/id])\n     (fn [params] {:delete-from [:users] :where [:= :account-id (::account/id params)]})))\n```\n\n#### Mutation preconditions\n\nMutations can be provided with preconditions: functions to run before affecting the actual\nmutation. These run in the same transaction as the effective mutation.\n\n``` clojure\n(entity-from-spec ::account/account\n (add-create-mutation)\n (add-update-by-id-mutation ::account/id)\n (add-precondition :delete ::has-no-users?\n   (fn [{::account/keys [id]}]\n     ;; Needs to go through HoneySQL\n     {:select [:id] :from [:users] :where [:= :account-id id]})\n   ;; Ensure the result is empty\n   empty?))\n```\n\n#### Transactions over several mutations\n\nMutations can be performed in a larger transaction cycle. To this effect, the\n`seql.mutation/with-transaction` macro is provided:\n\n``` clojure\n(m/with-transaction env\n  (m/mutate! env ::account/create account-a)\n  (m/mutate! env ::user/create user1-in-account-a)\n  (m/mutate! env ::user/create user2-in-account-a)\n  (q/execute env [::account/id (::account/id account-a]]))\n```\n\n### Listeners\n\nTo provide for clean CQRS type workflows, listeners can be added to\nmutations.  Each listener will subsequently be called on sucessful\ntransactions with a map of:\n\n- `:mutation`: the name of the mutation called\n- `:result`: the result of the transaction\n- `:params`: input parameters given to the mutation\n- `:metadata`: metadata supplied to the mutation, if any\n\n```clojure\n(def last-result (atom nil))\n\n(defn store-result\n  [details]\n  (reset! last-result (select-keys details [:mutation :result])))\n\n(let [env (l/add-listener env ::account/create store-result)]\n   (mutate! env ::account/create {::account/name \"a4\"\n                                  ::account/state :active}))\n\n@last-result\n\n;; =\u003e {:result [1] :mutation :account/create}\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexoscale%2Fseql","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fexoscale%2Fseql","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexoscale%2Fseql/lists"}