{"id":26194272,"url":"https://github.com/active-group/sqlosure","last_synced_at":"2025-06-20T09:37:32.571Z","repository":{"id":62432282,"uuid":"44965133","full_name":"active-group/sqlosure","owner":"active-group","description":"SQL \u0026 DB access with Clojure","archived":false,"fork":false,"pushed_at":"2023-03-10T16:03:58.000Z","size":666,"stargazers_count":7,"open_issues_count":7,"forks_count":0,"subscribers_count":15,"default_branch":"master","last_synced_at":"2025-04-15T03:15:15.050Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"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/active-group.png","metadata":{"files":{"readme":"README.org","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":"2015-10-26T11:49:44.000Z","updated_at":"2022-02-24T13:39:15.000Z","dependencies_parsed_at":"2022-11-01T21:01:01.115Z","dependency_job_id":null,"html_url":"https://github.com/active-group/sqlosure","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/active-group%2Fsqlosure","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/active-group%2Fsqlosure/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/active-group%2Fsqlosure/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/active-group%2Fsqlosure/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/active-group","download_url":"https://codeload.github.com/active-group/sqlosure/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248997080,"owners_count":21195799,"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":[],"created_at":"2025-03-12T01:56:00.435Z","updated_at":"2025-04-15T03:15:21.317Z","avatar_url":"https://github.com/active-group.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"=sqlosure= is a relational algebra library and DSL for interacting with\nrelational data, primarily in a SQL-Database from your Clojure-program.\n\n* Using it\n  Just add the current version of SQLosure to your =deps.edn=.\n  To interact with a specific database system, you must also include the\n  corresponding JDBC driver.\n  \n  Example using [[https://www.postgresql.org][PostgreSQL]]:\n  #+begin_src \n  {:deps {de.active-group/sqlosure \"0.5.0-SNAPSHOT\"\n          org.postgresql/postgresql \"42.2.14\"}\n  #+end_src\n* Overview\n  The core of =sqlosure= is a declarative query language based on [[https://en.wikipedia.org/wiki/Relational_algebra][relational\n  algebra]].\n  There are two types of values in SQLosure: [[*Queries][queries]] and [[*Expressions][expressions]].\n** Example\n   Suppose you want to write queries on a =employee= model.\n   An employee consists of\n   - an id (integer)\n   - a name (string)\n   - a flag that tells us whether they are an admin or not (boolean)\n   Additionally, each user is assigned to an office.\n   An office consists of\n   - an id (integer)\n   - a floor (integer)\n   - a name (string)\n  \n   The corresponding SQL-tables might have these =CREATE=-statements\n\n   #+begin_src sql\n     CREATE TABLE employee (\n       id INT,\n       name TEXT,\n       is_admin BOOLEAN,\n       office_id INT\n     );\n\n     CREATE TABLE office (\n       id INT,\n       floor INT,\n       name TEXT\n     );\n   #+end_src\n \n   To construct queries on those tables, you first need to define relations to\n   operate on\n   \n   #+begin_src clojure\n     (ns your.name.space\n       (:require [sqlosure.core :refer :all]\n                 [sqlosure.db-connection :as db-connection]))\n\n     ;; Defines a relation with three fields.\n     (def employee (table \"employee\" [[\"id\" $integer-t]\n                                      [\"name\" $string-t]\n                                      [\"is_admin\" $boolean-t]]))\n\n     (def employee-with-office (table \"employee\" [[\"id\" $integer-t]\n                                                  [\"name\" $string-t]\n                                                  [\"admin\" $boolean-t]\n                                                  [\"office_id\" $integer-t]]))\n\n     (def office (table \"office\" [[\"id\" $integer-t]\n                                  [\"floor\" $integer-t]\n                                  [\"name\" $string-t]]))\n   #+end_src\n \n   The =table= function expects the database table-name as string and a vector\n   of tuples for each attribute we want to consider.\n   Note that your are not required to \"project\" all fields from a table -- you\n   just need to define what you need in this each particular case.\n   \n   Next, we want to query our database.\n   To accommodate for the differences between different RDBMSs, SQLosure has to\n   concept of a /backend/.\n   Luckily, SQLosure comes equipped with several of those backends.\n   In this case, let's say we want to talk to a PostgreSQL-instance.\n   First, we jot down the [[https://github.com/clojure/java.jdbc][JDBC]] connection map:\n   #+begin_src clojure\n     (def pg-db {:dbtype \"postgresql\"\n                 :dbname \"somedb\"\n                 :host \"localhost\"\n                 :port \"5432\"\n                 :user \"myuser\"\n                 :password \"secret\"})\n   #+end_src\n  \n   To actually run the query, there are two basic modes.\n   The one we will use here is more basic.\n   [[*CRUD][Further down the document]] we will show the more elaborated way.\n   #+begin_src clojure\n     (ns ...\n         [sqlosure.db-connection :as db-connection]\n         [sqlosure.backends.postgresql :as postgresql])\n\n     (def db-conn (db-connect pg-db postgresql/implementation))\n\n     (db-connection/run-query db-conn employee)\n     ;; =\u003e [[0 \"Marco\" true]\n     ;;     [1 \"Markus\" false]\n     ;;     ...]\n   #+end_src\n   As you can see, running a base relation as the query selects the whole table.\n   Now, we only want employees that are admins.\n   #+begin_src clojure\n     (def admins\n       (query [emp (embed employee)]\n              (restrict! ($= (! emp \"is_admin\") ($boolean true)))\n              (project emp)))\n   #+end_src\n   Let's break this down\n   - =query= is a macro that allows us to construct relations from relational\n     algebra terms.\n   - To operate on relations in this context, we need to \"embed\" them via =[emp\n     (embed employee)]=.\n     Think of this as follows: Where on the right hand side of this expression\n     we talk about the relation as a whole,\n     the name on the right hand side refers to the individual values of this\n     relation.\n   - To narrow down the values, we call =restrict!= on them (think of a =WHERE=\n     clause in SQL).\n   - The argument to =restrict!= narrows down the values to those whose\n     attribute =\"is_admin\"= is equal to false.\n     =$== just the regular === but operates on relations instead of values.\n     SQLosure has many such operators, all prefixed with =$=.\n   - =(! emp \"is_admin\")= selects the =\"is_admin\"= attribute from the embedded\n     employee relation.\n   - In the end, we call =project= on all values. This means that, when the\n     query is executed, we want all values with all of their attributes.\n   \n   If we run the query, we get the following result:\n   #+begin_src clojure\n     (db-connection/run-query db-conn admins)\n     ;; =\u003e [[0 \"Marco\" true]\n     ;;     ...\n     ;;     [42 \"Tim\" true]]\n   #+end_src\n   \n   Now, perhaps we only want to see the names of employees, instead of the whole\n   record.\n   We could of course define a new query that just projects the =\"name\"= of each\n   employee:\n   #+begin_src clojure\n     (def admin-names\n       (query [emp (embed employee)]\n              (restrict! ($= (! emp \"is_admin\") ($boolean true)))\n              (project [[\"name\" (! emp \"name\")]])))\n   #+end_src\n   This would do the trick.\n   Still, we did just define almost the same query as above.\n   This is where it comes into play that the result of a =query=-call is itself\n   a query.\n   This means they easily compose to larger/more complex queries from simpler\n   ones.\n   Thus, we can then redefine =admin-names= as follows:\n    #+begin_src clojure\n      (def admin-names\n        (query [as (embed admins)]\n               (project [[\"name\" (! emp \"name\")]])))\n   #+end_src\n   We treat =admins= just as a base relation.\n   The schema (e.g. the fields you can select via the =!=-operator) depends on\n   the projection.\n\n   Lastly, let's write a query that returns all admins and the name's of their\n   offices:\n   #+begin_src clojure\n     (def admins-with-offices\n       (query [as (embed admins)\n               os (embed office)]\n              (restrict! ($= (! as \"office_id\")\n                             (! os \"id\")))\n              (project [[\"name\" (! as \"name\")]\n                        [\"office\" (! os \"name\")]])))\n     (db-connection/run-query db-conn admins-with-offices)\n     ;; =\u003e [[\"Marco\" \"Terassen Office\"]\n     ;;     [\"Tim\" \"Obergeschoss\"]\n     ;;     ...]\n   #+end_src\n* CRUD\n  SQLosure provides a DSL for reading and writing from and to databases.\n  To demonstrate, we will keep our tables from [[*Example][the examples above]].\n  There are four basic functions to construct programs that read and write to a\n  database\n  #+begin_src clojure\n    (ns your.name.space\n      (:require [sqlosure.core :refer :all]\n                [sqlosure.db-connection :as db-connection]\n                [sqlosure.lang :as db]))\n\n    ;; Defines a relation with three fields.\n    (def employee ..)\n    (def office ...)\n\n    (db/read! employee)\n    (db/create! employee [42 \"Mike\" true 0])\n    (db/udpate! employee \n                (fn [id name admin? office-id]\n                  ($= name ($string \"Marco\")))\n                (fn [id name admin? office-id]\n                  {\"is_admin\" ($boolean false)}))\n    (db/delete! employee\n                (fn [id name admin? office-id]\n                  ($= id ($integer 42))))\n  #+end_src\n  What do these functions do?\n  - =read!= operates in much the same way as we saw above. It takes a =query= as\n    it's argument and, upon execution, runs the query.\n  - =create!= takes a base relation (e.g. something that was created via the\n    =table= function) and, upon execution, inserts the record (a vector of\n    values to insert)\n  - =update!= takes a base relation (e.g. something that was created via the\n    =table= function),\n    a predicate function and an update fuction, and, upon execution, applies the\n    update.\n    The predicate function takes as it's arguments all fields of the relation\n    and must return an expression (something constructed from =$=-functions that\n    returns a =$boolean=).\n    The update function takes the same arguments and must return a map with all\n    updates to apply.\n    The keys must correspond to the attributes of the relation, their right hand\n    sides must be =$= const values.\n  - =delete!= takes a base relation (e.g. something that was created via the\n    =table= function), and a predicate function and, upon execution, applies the\n    delete operation.\n    The predicate function takes as it's arguments all fields of the relation\n    and must return an expression (something constructed from =$=-functions that\n    returns a =$boolean=).\n\n    The keen observer might have noticed that a) all those functions just return\n    values and b) there is no mention of a database backend or connection\n    strings.\n    First, let's construct a program out of these functions, using\n    [[https://github.com/active-group/active-clojure][active-clojure]]'s free monad.\n    #+begin_src clojure\n      (ns your.name.space\n        (:require [sqlosure.core :refer :all]\n                  [sqlosure.db-connection :as db-connection]\n                  [sqlosure.lang :as db]\n\n                  [active.clojure.monad :as monad]))\n\n      ;; Defines a relation with three fields.\n      (def employee ..)\n      (def office ...)\n\n      (def make-all-employees-admins!\n        (db/udpate! employee \n                    (fn [_ _ _ _]\n                      ($boolean true))\n                    (fn [_ _ _ _]\n                      {\"is_admin\" ($boolean true)})))\n\n      (def prog (monad/monadic (db/create! employee [0 \"Marco\" false 0])\n                               (db/create! employee [1 \"Erika\" true 0])\n                               (db/create! employee [2 \"Sibylle\" false 0])\n                               [employees (db/read! employee)]\n                               make-all-users-admins!\n                               (db/delete! employee (fn [id _ _ _]\n                                                      ($= id ($integer 0))))\n                               [employees-after (db/read! employee)]\n                               (monad/return {:before employees :after employees-after})))\n    #+end_src\n    This program first inserts three employees and reads them back, storing the\n    result as =employees=.\n    Then, after making all employees admins, deletes the user with =\"id\" = 0=.\n    It then reads the users back once more, storing the result as\n    =employees-after= before returning the two query results.\n    Keep in mind that is still just the description of the operations we want to\n    execute.\n     \n    To actually run the program, we need a \"command-config\" object.\n    For this, we have two optionse.\n** Option 1: Running against an actual database\n   If we want to run the program with an actual database, we need a\n   db-connection as above.\n   Then, we can run the program using the =run-db= function:\n   #+begin_src clojure\n     (ns your.name.space\n       (:require ...\n                 [sqlosure.runner.database :as db-runner]))\n\n     ...\n     (run-db (db-runner/command-config db-conn) prog)\n     ;; =\u003e {:before [[0 \"Marco\" false 0]\n     ;;              [1 \"Erike\" true 0]\n     ;;              [2 \"Sibylle\" false 0]]\n     ;;     :after [[1 \"Erika\" true 0]\n     ;;             [2 \"Sibylle\" true 0]]}\n   #+end_src\n   The second argument decides how the program will be executed. \n   Since we used the =sqlosure.runner.database= runner, the program is executed\n   in the context of our PostgeSQL database, just as before.\n** Option 2: Running with the =embedded= runner\n   Often, we want to construct such programs without really running them against\n   a database.\n   SQLosure provides the =sqlosure.runner.embedded= runner to enable the user to\n   run such programs against a simple, map based \"database\".\n   Such a database is just a map from table-names to a set of maps, representing\n   the individual records.\n   #+begin_src clojure\n     (ns your.name.space\n       (:require ...\n                 [sqlosure.runner.embedded :as embedded-runner]))\n\n     ...\n     (def empty-db {})\n     (def db {\"employees\" #{{\"id\" 42 \"name\" \"Simon\" \"is_admin\" false \"office_id\" 23}}})\n\n     (run-db (embedded-runner/command-config empty-db) prog)\n     ;; =\u003e {:before [[0 \"Marco\" false 0]\n     ;;              [1 \"Erike\" true 0]\n     ;;              [2 \"Sibylle\" false 0]]\n     ;;     :after [[1 \"Erika\" true 0]\n     ;;             [2 \"Sibylle\" true 0]]}\n\n     (run-db (embedded-runner/command-config db) prog)\n     ;; =\u003e {:before [[0 \"Marco\" false 0]\n     ;;              [1 \"Erike\" true 0]\n     ;;              [2 \"Sibylle\" false 0]\n     ;;              [42 \"Simon\" false 23]]\n     ;;     :after [[1 \"Erika\" true 0]\n     ;;             [2 \"Sibylle\" true 0]\n     ;;              [42 \"Simon\" true 23]]}\n   #+end_src\n   This yields the exact same result without ever touching the database and is\n   therefore the perfect way to tests your queries\n   (or even just operate on an in-memory \"database\").\n* Queries\n  TODO\n* Expressions\n  TODO\n* Typing\n  TODO\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Factive-group%2Fsqlosure","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Factive-group%2Fsqlosure","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Factive-group%2Fsqlosure/lists"}