{"id":13747288,"url":"https://github.com/vlado/activerecord-cte","last_synced_at":"2025-05-09T08:32:18.242Z","repository":{"id":45601293,"uuid":"247992792","full_name":"vlado/activerecord-cte","owner":"vlado","description":"Brings Common Table Expressions support to ActiveRecord and makes it super easy to build and chain complex CTE queries","archived":false,"fork":false,"pushed_at":"2024-09-13T20:57:53.000Z","size":58,"stargazers_count":304,"open_issues_count":1,"forks_count":9,"subscribers_count":6,"default_branch":"master","last_synced_at":"2024-10-14T04:48:00.292Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","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/vlado.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","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":"2020-03-17T14:36:04.000Z","updated_at":"2024-09-13T20:57:54.000Z","dependencies_parsed_at":"2024-04-19T17:02:40.385Z","dependency_job_id":null,"html_url":"https://github.com/vlado/activerecord-cte","commit_stats":{"total_commits":39,"total_committers":5,"mean_commits":7.8,"dds":"0.15384615384615385","last_synced_commit":"eca4050c621fb78c5aaafecc999d2fdd7137ebbf"},"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vlado%2Factiverecord-cte","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vlado%2Factiverecord-cte/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vlado%2Factiverecord-cte/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vlado%2Factiverecord-cte/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vlado","download_url":"https://codeload.github.com/vlado/activerecord-cte/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224842668,"owners_count":17379009,"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":"2024-08-03T06:01:23.995Z","updated_at":"2024-11-15T20:31:29.345Z","avatar_url":"https://github.com/vlado.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# ActiveRecord::Cte\n\n![Rubocop](https://github.com/vlado/activerecord-cte/actions/workflows/rubocop.yml/badge.svg)\n![MySQL](https://github.com/vlado/activerecord-cte/actions/workflows/test-with-mysql.yml/badge.svg)\n![PostgreSQL](https://github.com/vlado/activerecord-cte/actions/workflows/test-with-postgresql.yml/badge.svg)\n![SQLite](https://github.com/vlado/activerecord-cte/actions/workflows/test-with-sqlite.yml/badge.svg)\n\nAdds [Common Table Expression](https://en.wikipedia.org/wiki/Hierarchical_and_recursive_queries_in_SQL#Common_table_expression) support to ActiveRecord (Rails).\n\nIt adds `.with` query method and makes it super easy to build and chain complex CTE queries. Let's explain it using simple example.\n\n```ruby\nPost.with(\n  posts_with_comments: Post.where(\"comments_count \u003e ?\", 0),\n  posts_with_tags: Post.where(\"tags_count \u003e ?\", 0)\n)\n```\n\nWill return `ActiveRecord::Relation` and will generate SQL like this.\n\n```SQL\nWITH posts_with_comments AS (\n  SELECT * FROM posts WHERE (comments_count \u003e 0)\n), posts_with_tags AS (\n  SELECT * FROM posts WHERE (tags_count \u003e 0)\n)\nSELECT * FROM posts\n```\n\n**Please note that this creates the expressions but is not using them yet. See [Taking it further](#taking-it-further) for more info.**\n\nWithout this gem you would need to use `Arel` directly.\n\n```ruby\npost_with_comments_table = Arel::Table.new(:posts_with_comments)\npost_with_comments_expression = Post.arel_table.where(posts_with_comments_table[:comments_count].gt(0))\npost_with_tags_table = Arel::Table.new(:posts_with_tags)\npost_with_tags_expression = Post.arel_table.where(posts_with_tags_table[:tags_count].gt(0))\n\nPost.all.arel.with([\n  Arel::Node::As.new(posts_with_comments_table, posts_with_comments_expression),\n  Arel::Node::As.new(posts_with_tags_table, posts_with_tags_expression)\n])\n```\n\nInstead of Arel you could also pass raw SQL string but either way you will NOT get `ActiveRecord::Relation` and\nyou will not be able to chain them further, cache them easily, call `count` and other aggregates on them, ...\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem \"activerecord-cte\"\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install activerecord-cte\n\n## Usage\n\n### Hash arguments\n\nEasiest way to build the `WITH` query is to pass the `Hash` where keys are used as names of the tables and values are used to\ngenerate the SQL. You can pass `ActiveRecord::Relation`, `String` or `Arel::Nodes::As` node.\n\n```ruby\nPost.with(\n  posts_with_comments: Post.where(\"comments_count \u003e ?\", 0),\n  posts_with_tags: \"SELECT * FROM posts WHERE tags_count \u003e 0\"\n)\n# WITH posts_with_comments AS (\n#  SELECT * FROM posts WHERE (comments_count \u003e 0)\n# ), posts_with_tags AS (\n# SELECT * FROM posts WHERE (tags_count \u003e 0)\n# )\n# SELECT * FROM posts\n```\n\n### SQL string\n\nYou can also pass complete CTE as a single SQL string\n\n```ruby\nPost.with(\"posts_with_tags AS (SELECT * FROM posts WHERE tags_count \u003e 0)\")\n# WITH posts_with_tags AS (\n#   SELECT * FROM posts WHERE (tags_count \u003e 0)\n# )\n# SELECT * FROM posts\n```\n\n### Arel Nodes\n\nIf you already have `Arel::Node::As` node you can just pass it as is\n\n```ruby\nposts_table = Arel::Table.new(:posts)\ncte_table = Arel::Table.new(:posts_with_tags)\ncte_select = posts_table.project(Arel.star).where(posts_table[:tags_count].gt(100))\nas = Arel::Nodes::As.new(cte_table, cte_select)\n\nPost.with(as)\n# WITH posts_with_tags AS (\n#   SELECT * FROM posts WHERE (tags_count \u003e 0)\n# )\n# SELECT * FROM posts\n```\n\nYou can also pass array of Arel Nodes\n\n```ruby\nposts_table = Arel::Table.new(:posts)\n\nwith_tags_table = Arel::Table.new(:posts_with_tags)\nwith_tags_select = posts_table.project(Arel.star).where(posts_table[:tags_count].gt(100))\nas_posts_with_tags = Arel::Nodes::As.new(with_tags_table, with_tags_select)\n\nwith_comments_table = Arel::Table.new(:posts_with_comments)\nwith_comments_select = posts_table.project(Arel.star).where(posts_table[:comments_count].gt(100))\nas_posts_with_comments = Arel::Nodes::As.new(with_comments_table, with_comments_select)\n\nPost.with([as_posts_with_tags, as_posts_with_comments])\n# WITH posts_with_comments AS (\n#  SELECT * FROM posts WHERE (comments_count \u003e 0)\n# ), posts_with_tags AS (\n# SELECT * FROM posts WHERE (tags_count \u003e 0)\n# )\n# SELECT * FROM posts\n```\n\n### Taking it further\n\nAs you probably noticed from the examples above `.with` is only a half of the equation. Once we have CTE results we also need to do the select on them somehow.\n\nYou can write custom `FROM` that will alias your CTE table to the table ActiveRecord expects by default (`Post -\u003e posts`) for example.\n\n```ruby\nPost\n  .with(posts_with_tags: \"SELECT * FROM posts WHERE tags_count \u003e 0\")\n  .from(\"posts_with_tags AS posts\")\n# WITH posts_with_tags AS (\n#   SELECT * FROM posts WHERE (tags_count \u003e 0)\n# )\n# SELECT * FROM posts_with_tags AS posts\n\nPost\n  .with(posts_with_tags: \"SELECT * FROM posts WHERE tags_count \u003e 0\")\n  .from(\"posts_with_tags AS posts\")\n  .count\n\n# WITH posts_with_tags AS (\n#   SELECT * FROM posts WHERE (tags_count \u003e 0)\n# )\n# SELECT COUNT(*) FROM posts_with_tags AS posts\n```\n\nAnother option would be to use join\n\n```ruby\nPost\n  .with(posts_with_tags: \"SELECT * FROM posts WHERE tags_count \u003e 0\")\n  .joins(\"JOIN posts_with_tags ON posts_with_tags.id = posts.id\")\n# WITH posts_with_tags AS (\n#   SELECT * FROM posts WHERE (tags_count \u003e 0)\n# )\n# SELECT * FROM posts JOIN posts_with_tags ON posts_with_tags.id = posts.id\n```\n\nThere are other options also but that heavily depends on your use case and is out of scope of this README :)\n\n### Recursive CTE\n\nRecursive queries are also supported `Post.with(:recursive, popular_posts: \"... union to get popular posts ...\")`.\n\n```ruby\nposts = Arel::Table.new(:posts)\ntop_posts = Arel::Table.new(:top_posts)\n\nanchor_term = posts.project(posts[:id]).where(posts[:comments_count].gt(1))\nrecursive_term = posts.project(posts[:id]).join(top_posts).on(posts[:id].eq(top_posts[:id]))\n\nPost.with(:recursive, top_posts: anchor_term.union(recursive_term)).from(\"top_posts AS posts\")\n# WITH RECURSIVE \"popular_posts\" AS (\n#   SELECT \"posts\".\"id\" FROM \"posts\" WHERE \"posts\".\"comments_count\" \u003e 0 UNION SELECT \"posts\".\"id\" FROM \"posts\" INNER JOIN \"popular_posts\" ON \"posts\".\"id\" = \"popular_posts\".\"id\" ) SELECT \"posts\".* FROM popular_posts AS posts\n```\n\n## Issues\n\nPlease note that `update_all` and `delete_all` methods are not implemented and will not work as expected. I tried to implement them and was succesfull\nbut the \"monkey patching\" level was so high that I decided not to keep the implementation.\n\nIf my [Pull Request](https://github.com/rails/rails/pull/37944) gets merged adding them to Rails direcly will be easy and since I did not need them yet\nI decided to wait a bit :)\n\n## Development\n\n### Setup\n\nAfter checking out the repo, run `bin/setup` to install dependencies.\n\n### Running Rubocop\n\n```\nbundle exec rubocop\n```\n\n### Running tests\n\nTo run the tests using SQLite adapter and latest version on Rails run\n\n```\nbundle exec rake test\n```\n\nGitHub Actions will run the test matrix with multiple ActiveRecord versions and database adapters. You can also run the matrix locally with\n\n```\nbundle exec rake test:matrix\n```\n\nThis will build Docker image with all dependencies and run all tests in it. See `bin/test` for more info.\n\n### Console\n\nYou can run `bin/console` for an interactive prompt that will allow you to experiment.\n\n### Other\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/vlado/activerecord-cte. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n\n## Code of Conduct\n\nEveryone interacting in the Activerecord::Cte project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/vlado/activerecord-cte/blob/master/CODE_OF_CONDUCT.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvlado%2Factiverecord-cte","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvlado%2Factiverecord-cte","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvlado%2Factiverecord-cte/lists"}