{"id":15713542,"url":"https://github.com/ysbaddaden/sql","last_synced_at":"2025-10-10T23:08:01.409Z","repository":{"id":203960830,"uuid":"710785913","full_name":"ysbaddaden/sql","owner":"ysbaddaden","description":"SQL query builder for Crystal","archived":false,"fork":false,"pushed_at":"2024-06-15T11:04:05.000Z","size":78,"stargazers_count":12,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-07T14:32:30.436Z","etag":null,"topics":["crystal","database","mysql","postgresql","sql","sqlite3"],"latest_commit_sha":null,"homepage":"","language":"Crystal","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/ysbaddaden.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-10-27T12:42:23.000Z","updated_at":"2025-02-14T15:51:51.000Z","dependencies_parsed_at":null,"dependency_job_id":"897d635f-a27d-4777-ba7c-67ec39f2e495","html_url":"https://github.com/ysbaddaden/sql","commit_stats":null,"previous_names":["ysbaddaden/sql"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ysbaddaden/sql","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ysbaddaden%2Fsql","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ysbaddaden%2Fsql/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ysbaddaden%2Fsql/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ysbaddaden%2Fsql/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ysbaddaden","download_url":"https://codeload.github.com/ysbaddaden/sql/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ysbaddaden%2Fsql/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279005549,"owners_count":26083918,"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-10T02:00:06.843Z","response_time":62,"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":["crystal","database","mysql","postgresql","sql","sqlite3"],"created_at":"2024-10-03T21:31:59.841Z","updated_at":"2025-10-10T23:08:01.381Z","avatar_url":"https://github.com/ysbaddaden.png","language":"Crystal","readme":"# SQL\n\nDifferent modules revolving handle SQL queries.\n\n- `SQL::Query` to generate SQL queries;\n- `SQL::InformationSchema` to introspect on your database;\n- `SQL::Migrate` to migrate your database.\n\nThere might be more modules in the future, for example a tokenizer to lex a SQL\nquery srting into tokens, and maybe a full blown parser.\n\n## Status\n\nThe SQL shard is in preliminary alpha. The basis shouldn't change much anymore,\nbut a lot still has to be evaluated in real life situations.\n\nSupported database server flavors:\n\n- MySQL / MariaDB\n- PostgreSQL\n- SQLite3\n\n## SQL::Query\n\nDSL to generate SQL queries for different database servers.\n\nGoals:\n\n- Simplify writing SQL queries, for example using a `NamedTuple` or `Hash`\n  instead of ordering your args manually and counting your `?` in raw SQL\n  strings;\n\n- Avoid each database server quirks such as Postgres using `$i` for statement\n  placeholders, or MySQL using backticks instead of double quotes for quoting\n  column and table names;\n\n- Feel like writing SQL in plain Crystal.\n\n- Be the foundation for an ORM, Repo or plain SQL queries.\n\nNon Goals:\n\n- Execute queries.\n- Become an ORM.\n\n## Queries\n\nYou can write any query:\n\n```crystal\nrequire \"sql\"\nrequire \"sql/query/builder/posgresql\"\n\nsql = SQL.query(\"postgres://\")\nsql.format { |q| q.select(:*).from(:users).where(q.column(:group_id) == 1) }\n# =\u003e {%(SELECT * FROM \"users\" WHERE \"group_id\" = $1), [1]}\n```\n\nYou can include `SQL::Helpers` into the current scope to simplify the access to\nthe `#column` method, along with other helpers (`#raw`, `#operator`).\n\n```crystal\nclass UserRepo\n  include SQL::Helpers\n\n  def get(id : Int32)\n    query, args = sql.format \u0026.select(:id, :name).from(:users).where(column(:id) == id)\n    db.query_one(query, args: args, as: {Int32, String})\n  end\nend\n```\n\nYou can usually use a Symbol to refer to a table or column name, but there are\ncases where we need an object, for example to build a WHERE or HAVING condition.\nIn these cases, we can define a SQL schema to target tables and table columns in\na much more expressive way.\n\n### Schemas\n\nYou can define the schema of your database tables, so you can avoid the use of\nthe column helpers, as well as using aliases more easily. Work is underway to\nhave these schemas automatically generated from your database.\n\n```crystal\nmodule MySchemas\n  struct Users \u003c SQL::Query::Table\n    table_name :users\n    column :id\n    column :group_id\n    column :name\n  end\n\n  struct Groups \u003c SQL::Query::Table\n    table_name :groups\n    column :id\n    column :name\n  end\nend\n```\n\nThen you can:\n\n```crystal\nrequire \"pg\"\nrequire \"sql\"\nrequire \"sql/query/builder/postgresql\"\nrequire \"./schemas\"\n\ndb = DB.open(\"postgres://\")\nsql = SQL.query(\"postgres://\")\n\n# bring schemas and helpers into the current scope:\ninclude SQL::Query::Functions\ninclude SQL::Query::Helpers\ninclude MySchemas\n\nquery, args = sql.format do |q|\n  q.select(Users.id, Users.name)\n    .from(Users)\n    .where(Users.group_id == 1)\nend\n# =\u003e {%(SELECT \"users\".\"id\", \"users.name\" FROM \"users\" WHERE \"users\".\"group_id\" = $1), [1]}\n\ndb.query_all(query, args: args, as: {Int32, String})\n```\n\nAs you can see the WHERE condition is a regular Crystal comparison. Most\noperators are supported. See `SQL::Operators` for the whole list of available\noperators, and see the `#operator` helper to use any operator from your database\n(albeit in a less expressive way).\n\nSub-queries:\n\n```crystal\nsql.format do |q|\n  q.select(:*).from(Users).where(Users.group_id.in {\n    q.select(:id).from(:groups).where(Groups.created_at \u003c 1.month.ago)\n  })\nend\n# =\u003e {%(SELECT \"users\".\"id\", FROM \"users\" WHERE \"users\".\"group_id\" IN (SELECT \"groups\".\"id\" FROM \"groups\" WHERE \"groups\".created_at \u003c $1), [1.month.ago]}\n```\n\nThe SQL is generated as the methods are called, so you must define the sub-query\nright into the block (as you would in SQL); you can't assign it to a variable\nand return that variable from the block. For example the following will generate\ninvalid SQL:\n\n```crystal\nsql.format do |q|\n  group_ids = q.select(:id).from(:groups).where(Groups.created_at \u003c 1.month.ago)\n  q.select(:*).from(Users).where(Users.group_id.in { groups_ids })\nend\n# =\u003e {%(SELECT \"groups\".\"id\" FROM \"groups\" WHERE \"groups\".created_at \u003c $1SELECT \"users\".\"id\", FROM \"users\" WHERE \"users\".\"group_id\" IN (), [1.month.ago]}\n```\n\nFunctions:\n\n```crystal\nsql.format do |q|\n  q.select(:id, count(:*)).from(Users).group_by(Users.group_id).having(count(:*) \u003e 2)\nend\n# =\u003e {%(SELECT \"users\".\"id\", count(*) FROM \"users\" GROUP BY \"users\".\"group_id\" HAVING count(*) \u003e $1), [2]}\n```\n\nAliases:\n\n```crystal\nsql.format do |q|\n  u = Users[:u]\n  q.select({u.id =\u003e :uid, u.name =\u003e nil, length(u.name) =\u003e :len}).from(u).where(u.group_id == 5)\nend\n# =\u003e {%(SELECT \"u\".\"id\" AS \"uid\", \"u\".\"name\", length(\"u\".\"name\") AS \"len\" FROM \"users\" AS \"u\" WHERE \"u\".\"group_id\" == $1), [5]}\n```\n\nWith:\n\n```crystal\nregister_function :very_expensive_function\n\nsql.format do |q|\n  q.with(:w) { q.select({:key =\u003e nil, very_expensive_function(:val) =\u003e :f}).from(:some_table) }\n    .select(:*)\n    .from(column(:w, as: w1))\n    .join(column(:w, as: :w2))\n    .on(raw(\"w1.f\") == raw(\"w2.f\"))\nend\n# =\u003e {%(WITH \"w\" AS (SELECT \"key\", very_expensive_function(\"val\") as \"f\" FROM \"some_table\") SELECT * FROM \"w\" AS \"w1\" JOIN \"w\" AS \"w2\" ON w1.f = w2.f), []}\n```\n\nLots more is possible! You can see lots of examples in the `test` folder or by\nreading the documentation for `SQL::Query::Builder::Generic`.\n\n## SQL::InformationSchema\n\nTODO: missing docs (see `see/information_schema.cr`).\n\n## SQL::Migrate\n\nTODO: missing docs (see `src/migrate/cli.cr` and `bin/migrate.cr`).\n\n## License\n\nDistributed under the Apache-2.0 license.\n\n## Authors\n\n- Julien Portalier\n\n## Influences\n\n- [diesel.rs](https://diesel.rs)\n- [honeysql](https://github.com/seancorfield/honeysql)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fysbaddaden%2Fsql","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fysbaddaden%2Fsql","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fysbaddaden%2Fsql/lists"}