{"id":27249634,"url":"https://github.com/coast-framework/lighthouse","last_synced_at":"2025-04-11T00:18:24.671Z","repository":{"id":65350717,"uuid":"153872446","full_name":"coast-framework/lighthouse","owner":"coast-framework","description":"Easy clojure relational database queries, migrations and connection pooling","archived":false,"fork":false,"pushed_at":"2019-08-18T00:51:17.000Z","size":111,"stargazers_count":22,"open_issues_count":1,"forks_count":2,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-11T00:18:21.396Z","etag":null,"topics":["clojure","connection-pool","database","datomic","datomic-pull-sql","hikari-cp","postgres","postgresql","relational-databases","sqlite","sqlite3"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/coast-framework.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}},"created_at":"2018-10-20T05:20:45.000Z","updated_at":"2024-04-04T19:17:27.000Z","dependencies_parsed_at":"2023-01-19T04:35:10.970Z","dependency_job_id":null,"html_url":"https://github.com/coast-framework/lighthouse","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coast-framework%2Flighthouse","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coast-framework%2Flighthouse/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coast-framework%2Flighthouse/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coast-framework%2Flighthouse/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/coast-framework","download_url":"https://codeload.github.com/coast-framework/lighthouse/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248317817,"owners_count":21083541,"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","connection-pool","database","datomic","datomic-pull-sql","hikari-cp","postgres","postgresql","relational-databases","sqlite","sqlite3"],"created_at":"2025-04-11T00:18:23.566Z","updated_at":"2025-04-11T00:18:24.629Z","avatar_url":"https://github.com/coast-framework.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# lighthouse\n_clojure + sql + hikaricp connection pool + datomic inspired queries_\n\n## Installation\n\nAdd this to your `deps.edn`\n\n```clojure\ncoast-framework/lighthouse {:mvn/version \"1.2.1\"}\n```\n\nYou'll also need a jdbc driver for your db of choice:\n\n```clojure\norg.xerial/sqlite-jdbc {:mvn/version \"3.25.2\"} ; sqlite\n; or\norg.postgresql/postgresql {:mvn/version \"42.2.1\"} ; postgres\n```\n\n## Require\n\nGet sql superpowers by adding it to your project like this\n\n```clojure\n(ns your-project\n  (:require [lighthouse.core :as db]))\n```\n\nConnect to your db like this:\n\n```clojure\n(def conn (db/connect \"jdbc:sqlite:todos.db\"))\n; or\n(def conn (db/connect \"jdbc:postgresql://localhost:5432/todos\"))\n```\n\n## Migrations\n\nChange your db schema like this\n\n```clojure\n(def todos-users [{:db/col :person/name :db/type \"text\" :db/unique? true}\n                  {:db/rel :person/todos :db/type :many :db/ref :todo/person}\n                  {:db/rel :todo/person :db/type :one :db/ref :person/id}\n                  {:db/col :todo/name :db/type \"text\"}\n                  {:db/col :todo/done :db/type \"boolean\" :db/default false :db/nil? false}])\n\n(db/migrate conn todos-users)\n```\n\nYou can run this over and over, lighthouse keeps track in a schema_migrations table in your db\n\nIf you change the name of the def like todos-users -\u003e todos-users-1\nthen this will *not* be treated like a new migration\nif you change the contents of the def, then this will attempt to run a\nnew migration. If you're unsure of what's about to happen, run this instead it will output the sql and do nothing to the database\n\n```clojure\n(db/migrate conn todos-users {:dry-run? true})\n```\n\nMade a mistake with a column name? No problem, rename columns like this\n\n```clojure\n(def rename-done [{:db/id :todo/done :db/col :todo/finished}])\n\n(db/migrate conn rename-done)\n```\n\nIf you're not using sqlite, you can alter columns quite a bit more with the same syntax,\nreference the existing column with `:db/id` and then give it the properties you want to change\n\n```clojure\n(def done-\u003edone-at [{:db/id :todo/done :db/col :todo/done-at :db/type \"timestamptz\" :db/nil? false}])\n\n(db/migrate conn done-\u003edone-at)\n```\n\n## Tables\n\nAll tables are created from a vector of maps, so this\n\n```clojure\n[{:db/col :person/nick :db/type \"text\" :db/unique? true :db/nil? false :db/default \"''\"}\n {:db/col :todo/name :db/type \"text\"}]\n```\n\nturns into this:\n\n```sql\ncreate table if not exists person (\n  id integer primary key,\n  updated_at timestamp,\n  created_at timestamp not null default current_timestamp\n)\n\ncreate table if not exists todo (\n  id integer primary key,\n  updated_at timestamp,\n  created_at timestamp not null default current_timestamp\n)\n```\n\nThe namespaces of the `:db/col` value in this case `:person/nick` -\u003e `person` and `:todo/name` -\u003e `todo` are the table names. This happens for the distinct namespaces of every key in every migration.\n\n## Columns\n\nColumns are the names of the `:db/col` values in the migration maps, so this:\n\n```clojure\n[{:db/col :person/nick :db/type \"text\" :db/unique? true :db/nil? false :db/default \"''\"}\n {:db/col :todo/name :db/type \"text\"}]\n```\n\nBecomes this:\n\n```sql\nalter table person add column nick text not null default '';\ncreate unique index idx_person_nick on table person (nick);\n\nalter table todo add column name text;\n```\n\nSQLite has several restrictions on what alter table can and can't do, namely, altering a table to add a column must have a default value when specifying not null, this will fail if you don't specify something. This can work on postgres under certain conditions.\n\n## Relationships (Foreign Keys)\n\nForeign keys are a little special and require two, yes that's right, two maps to function as keys in pull queries. For example:\n\n```clojure\n[{:db/rel :person/todos :db/type :many :ref :todo/person}\n {:db/rel :todo/person :db/type :one :ref :person/id}]\n```\n\nTurns into one alter table statement with a references clause:\n\n```sql\nalter table todo add column person integer references person(id) on cascade delete;\n```\n\nYou can control the `on cascade` behavior with the optional `:db/delete` key.\n\nYou're essentially building a join clause and a way to list child rows ahead of time. The way it works is this:\n\n```clojure\n[{:db/rel :person/todos :db/type :many :db/ref :todo/person}\n {:db/rel :todo/person :db/type :one :db/ref :person/id}]\n```\n\nThe `:db/rel` key for `:db/type :many` (the first line) is just name, it doesn't have to be `:person/todos`, it could be anything that you want to reference in a pull query\n\n```clojure\n(pull [{:person/todos [:todo/id]}]\n      [:person/id 1])\n```\n\nSo if you changed it to `:people/todo-items` or something, that is perfectly fine, the pull query would change to look like this:\n\n```clojure\n(pull [{:people/todo-items [:todo/id]}]\n      [:person/id 1])\n```\n\nand it would return results like this:\n\n```clojure\n{:people/todo-items [{:todo/id 1} {:todo/id 2}]}\n```\n\nThe last key in the first line `:db/ref` is a \"pointer\" to the next line:\n\n```clojure\n{:db/rel :todo/person :db/type :one :db/ref :person/id}\n```\n\nThis line is more \"convention over configuration\" (i.e. hardcoded), so the `:db/rel` key here is not just a name, it and the `:db/ref` key are used to build the join statement in pull requests, here's how they come together in a join, `:todo/person` is the foreign key column name, it becomes `todo.person` in the join statement, the `:db/ref` value `:person/id` becomes `person.id` in the join statement, for a join that looks like this:\n\n```sql\nfrom person\nleft outer join todo on todo.person = person.id\n```\n\nThere's more info about pull in the queries section below, but that's pretty much everything you need to know to have a great pull experience!\n\n## Insert\n\nInsert data into the database like this\n\n```clojure\n; simple one row insert\n(let [p (db/insert conn {:person/name \"swlkr\"})]\n\n  ; insert multiple rows like this\n  ; p is auto-resolved to (get p :person/id)\n  (db/insert conn [{:todo/name \"write readme\"\n                    :todo/person p\n                    :todo/done true}\n                   {:todo/name \"write tests 😅\"\n                    :todo/person p\n                    :todo/done false}]})\n\n; or just manually set the foreign key integer value\n(db/insert conn [{:todo/name \"write readme\"\n                  :todo/person 1\n                  :todo/done true}\n                 {:todo/name \"write tests 😅\"\n                  :todo/person 1\n                  :todo/done false}]}))\n```\n\n## Update\n\nUpdate data like this\n\n```clojure\n(db/update conn {:todo/id 2 :todo/name \"write tests 😅😅😅\"})\n\n; you can either perform an update after a select\n(let [todos (db/q conn '[:select todo/id\n                         :where [todo/done false]])] ; =\u003e [{:todo/id 2} {:todo/id 3}]\n  (db/update conn (map #(assoc % :todo/done true) todos)))\n\n; or update from transact\n(db/transact conn '[:update todo\n                    :set [todo/done true]\n                    :where [todo/id [2 3]]])\n```\n\n## Delete\n\nDelete data like this\n\n```clojure\n(db/delete conn {:todo/id 1})\n\n; or multiple rows like this\n(db/delete conn [{:todo/id 1} {:todo/id 2} {:todo/id 3}])\n\n; there's always transact too\n(db/transact conn '[:delete\n                    :from todo\n                    :where [todo/id [1 2 3]]]) ; this implicitly does an in query\n```\n\n## Queries\n\nQuery data like this\n\n```clojure\n(db/q conn '[:select todo/name todo/done\n             :where [todo/done ?done]\n                    [todo/name like ?name]\n             :order todo/created-at desc]\n           {:done true\n            :name \"%write%\"})\n; =\u003e [{:todo/name \"write readme\" :todo/done true}]\n\n; or like this\n(db/q conn '[:select todo/name todo/done\n             :from todo\n             :where [todo/done true]])\n; =\u003e [{:todo/name \"write readme\" :todo/done true}]\n\n; if you don't want to specify every column, you don't have to\n(db/q conn '[:select todo/*\n             :from todo\n             :where [todo/done false]])\n; =\u003e [{:todo/id 1 :todo/name ... :todo/done false :todo/created-at ...}]\n\n; joins are supported too\n(db/q conn '[:select todo/* person/*\n             :joins person/todos])\n; =\u003e [{:todo/id 1 :todo/name ... :person/id 1 :person/name \"swlkr\" ...}]\n\n; you can also add a sql string in the where clause\n(db/q conn '[:select todo/name todo/done\n             :where [todo/done ?done]\n                    [\"todo.created_at \u003e now()\"]\n             :order todo/created-at desc]\n           {:done true})\n; =\u003e [{:todo/name \"write readme\" :todo/done true}]\n```\n\n## Pull Queries\n\nPull queries are special because they don't map to sql 1:1, they return your data hierarchically with the help of some left outer join json aggregation and `clojure.walk` after the fact. They work together with the schema migrations to make your life easier.\n\nGiven this migration:\n\n```clojure\n(def todos-users [{:db/col :person/name :db/type \"text\" :db/unique? true :db/nil? false}\n                  {:db/rel :person/todos :db/type :many :db/ref :todo/person}\n                  {:db/rel :todo/person :db/type :one :db/ref :person/id}\n                  {:db/col :todo/name :db/type \"text\"}\n                  {:db/col :todo/done :db/type \"boolean\" :db/default false :db/nil? false}])\n\n(db/migrate conn todos-users)\n```\n\nYou can run these queries\n\n```clojure\n(db/q conn '[:pull [person/name {:person/todos [todo/name todo/done]}]\n             :from person\n             :where [todo/done ?todo/done]]\n           {:todo/done true})\n; =\u003e [{:person/name \"swlkr\" :person/todos [{:todo/name \"write readme\" :todo/done true}]}]\n\n; or like this\n(db/pull conn '[person/name\n                {:person/todos [todo/name todo/done]}]\n              [:person/name \"swlkr\"]])\n; =\u003e {:person/name \"swlkr\" :person/todos [{:todo/name \"write readme\" :todo/done true}]}\n\n; or you can go the other way as well\n(db/pull conn '[:todo/name\n                {:todo/person [person/name person/id]}]\n              [:todo/name 'like \"%blog post\"])\n```\n\n## Plain Old SQL\n\nDon't like all of this fancy schmancy pull and q stuff? That's ok, there's always `defq`.\n\nMake a SQL file in `resources`\n\n```sql\n-- name: posts-with-comments\nselect\n  post.*,\n  c.comment_count\nfrom\n  post\njoin\n  (\n    select\n      comment.post_id,\n      count(comment.id) as comment_count\n    from\n      comment\n    where\n      comment.post_id = :post_id\n    group by\n      comment.post_id\n ) c on c.post_id = post.id\n```\n\nThen in a clojure file, reference that same sql file in `defq`\n\n```clojure\n(ns your-project\n  (:require [lighthouse.core :refer [defq]]))\n\n(defq conn posts-with-comments \"posts.sql\")\n\n(posts-with-count {:post-id 1}) ; =\u003e [{:id 1 ... :comment-count 12}]\n```\n\nor if you have a lot of queries in a single sql file call defq without the name argument\n\n```clojure\n(defq conn \"posts.sql\")\n```\n\nand defq will create functions for each bit of named sql in the current namespace.\n\n## Plain Old SQL Migrations\n\nIf the fancy schmancy migrations aren't working for you either, there's always `:db/up` and `:db/down`:\n\n```clojure\n(def plain-sql-migration [{:db/up \"create table todo(id serial primary key, name text not null)\" :db/down \"drop table todo\"}])\n```\n\nYou can mix and match with the edn migrations.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcoast-framework%2Flighthouse","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcoast-framework%2Flighthouse","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcoast-framework%2Flighthouse/lists"}