{"id":18211973,"url":"https://github.com/eval/appquery","last_synced_at":"2026-01-01T23:30:47.398Z","repository":{"id":257825917,"uuid":"872344098","full_name":"eval/appquery","owner":"eval","description":"Raw SQL queries in Ruby/Rails made convenient","archived":false,"fork":false,"pushed_at":"2024-10-14T12:32:54.000Z","size":18,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-10-19T15:47:34.700Z","etag":null,"topics":["rails","ror","ruby","rubygem"],"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/eval.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","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":"2024-10-14T09:20:41.000Z","updated_at":"2024-10-14T14:07:33.000Z","dependencies_parsed_at":null,"dependency_job_id":"996275ed-5240-4cf1-a2ed-66e76a373853","html_url":"https://github.com/eval/appquery","commit_stats":null,"previous_names":["eval/appquery"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eval%2Fappquery","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eval%2Fappquery/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eval%2Fappquery/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eval%2Fappquery/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/eval","download_url":"https://codeload.github.com/eval/appquery/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":234715615,"owners_count":18875905,"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":["rails","ror","ruby","rubygem"],"created_at":"2024-11-03T15:04:13.758Z","updated_at":"2026-01-01T23:30:47.390Z","avatar_url":"https://github.com/eval.png","language":"Ruby","readme":"# AppQuery - raw SQL 🥦, cooked :stew:\n\n[![Gem Version](https://badge.fury.io/rb/appquery.svg)](https://badge.fury.io/rb/appquery)\n[![API Docs](https://img.shields.io/badge/API_Docs-YARD-blue.svg)](https://eval.github.io/appquery/)\n\nA Ruby gem for working with raw SQL in Rails. Store queries in `app/queries/`, execute them with proper type casting, and filter/transform results using CTEs.\n\n```ruby\n# Load and execute\nweek = AppQuery[:weekly_sales].with_binds(week: 1, year: 2025)\nweek.entries\n#=\u003e [{\"week\" =\u003e 2025-01-13, \"category\" =\u003e \"Electronics\", \"revenue\" =\u003e 12500, \"target_met\" =\u003e true}, ...]\n\n# Filter results (query wraps in CTE, :_ references it)\nweek.count(\"SELECT * FROM :_ WHERE NOT target_met\")\n#=\u003e 3\n\n# Extract a column efficiently (only fetches that column)\nweek.column(:category)\n#=\u003e [\"Electronics\", \"Clothing\", \"Home \u0026 Garden\"]\n\n# Named binds with defaults\nAppQuery[:weekly_sales].select_all(binds: {min_revenue: 5000})\n\n# ERB templating\nAppQuery(\"SELECT * FROM contracts \u003c%= order_by(ordering) %\u003e\")\n  .render(ordering: {year: :desc}).select_all\n\n# Custom type casting\nAppQuery(\"SELECT metadata FROM products\").select_all(cast: {metadata: :json})\n\n# Inspect/mock CTEs for testing\nquery.prepend_cte(\"sales AS (SELECT * FROM mock_data)\")\n```\n\n**Highlights**: query files with generator · `select_all`/`select_one`/`select_value`/`count`/`column`/`ids` · query transformation via CTEs · immutable (derive new queries from existing) · named binds · ERB helpers (`order_by`, `paginate`, `values`, `bind`) · automatic + custom type casting · RSpec integration\n\n\u003e [!IMPORTANT]  \n\u003e **Status**: alpha. API might change. See [the CHANGELOG](./CHANGELOG.md) for breaking changes when upgrading.\n\u003e\n\n## Rationale\n\nSometimes ActiveRecord doesn't cut it, and you'd rather use raw SQL to get the right data out. That, however, introduces some new problems. First of all, you'll run into the not-so-intuitive use of [select_(all|one|value)](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/DatabaseStatements.html#method-i-select_all) — for example, how they differ with respect to type casting, and how their behavior can vary between ActiveRecord versions. Then there's the testability, introspection, and maintainability of the resulting SQL queries.  \nThis library aims to alleviate all of these issues by providing a consistent interface across select_* methods and ActiveRecord versions. It should make inspecting and testing queries easier—especially when they're built from CTEs.\n\n## Installation\n\nInstall the gem and add to the application's Gemfile by executing:\n\n```bash\nbundle add appquery\n```\n\n## Usage\n\n\u003e [!NOTE]\n\u003e The following (trivial) examples are not meant to convince you to ditch your ORM, but just to show how this gem handles raw SQL queries.\n\n### ...from console\n\nTestdriving can be easily done from the console. Either by cloning this repository (recommended, see `Development`-section) or installing the gem in an existing Rails project.  \n\u003cdetails\u003e\n  \u003csummary\u003eDatabase setup (the `bin/console`-script does this for your)\u003c/summary\u003e\n  \n  ```ruby\n  ActiveRecord::Base.logger = Logger.new(STDOUT)\n  ActiveRecord::Base.establish_connection(url: 'postgres://localhost:5432/some_db')\n  ```\n\u003c/details\u003e\n\nThe prompt indicates what adapter the example uses:\n\n```ruby\n# showing select_(all|one|value)\n[postgresql]\u003e AppQuery(%{select date('now') as today}).select_all.entries\n=\u003e [{\"today\" =\u003e \"2025-05-10\"}]\n[postgresql]\u003e AppQuery(%{select date('now') as today}).select_one\n=\u003e {\"today\" =\u003e \"2025-05-10\"}\n[postgresql]\u003e AppQuery(%{select date('now') as today}).select_value\n=\u003e \"2025-05-10\"\n\n# binds\n## named binds\n[postgresql]\u003e AppQuery(%{select now() - (:interval)::interval as date}).select_value(binds: {interval: '2 days'})\n\n## not all binds need to be provided (ie they are nil by default) - so defaults can be added in SQL:\n[postgresql]\u003e AppQuery(\u003c\u003c~SQL).select_all(binds: {ts1: 2.days.ago, ts2: Time.now, interval: '1 hour'}).column(\"series\")\n    SELECT generate_series(\n      :ts1::timestamp,\n      :ts2::timestamp,\n      COALESCE(:interval, '5 minutes')::interval\n    ) AS series\n  SQL\n\n# casting\n## Cast values are used by default:\n[postgresql]\u003e AppQuery(%{select date('now')}).select_one\n=\u003e {\"today\" =\u003e Sat, 10 May 2025}\n## compare ActiveRecord\n[postgresql]\u003e ActiveRecord::Base.connection.select_one(%{select date('now') as today})\n=\u003e {\"today\" =\u003e \"2025-12-20\"}\n\n## SQLite doesn't have a notion of dates or timestamp's so casting won't do anything:\n[sqlite]\u003e AppQuery(%{select date('now') as today}).select_one(cast: true)\n=\u003e {\"today\" =\u003e \"2025-05-12\"}\n## Providing per-column-casts fixes this:\ncast = {today: :date}\n[sqlite]\u003e AppQuery(%{select date('now') as today}).select_one(cast:)\n=\u003e {\"today\" =\u003e Mon, 12 May 2025}\n\n\n# rewriting queries (using CTEs)\n[postgresql]\u003e articles = [\n  [1, \"Using my new static site generator\", 2.months.ago.to_date],\n  [2, \"Let's learn SQL\", 1.month.ago.to_date],\n  [3, \"Another article\", 2.weeks.ago.to_date]\n]\n[postgresql]\u003e q = AppQuery(\u003c\u003c~SQL, cast: {published_on: :date}).render(articles:)\n  WITH articles(id,title,published_on) AS (\u003c%= values(articles) %\u003e)\n  select * from articles order by id DESC\nSQL\n\n## query the articles-CTE\n[postgresql]\u003e q.select_all(%{select * from articles where id::integer \u003c 2}).entries\n\n## query the end-result (available via the placeholder ':_')\n[postgresql]\u003e q.select_one(%{select * from :_ limit 1})\n### shorthand for that\n[postgresql]\u003e q.first\n\n## ERB templating\n# Extract a query from q that can be sorted dynamically:\n[postgresql]\u003e q2 = q.with_select(\"select id,title,published_on::date from articles \u003c%= order_by(order) %\u003e\")\n[postgresql]\u003e q2.render(order: {\"published_on::date\": :desc, 'lower(title)': \"asc\"}).select_all.entries\n\n# shows latest articles first, and titles sorted alphabetically\n# for articles published on the same date.\n# order_by raises when it's passed something that would result in just `ORDER BY`:\n[postgresql]\u003e q2.render(order: {})\n\n# doing a select using a query that should be rendered, a `AppQuery::UnrenderedQueryError` will be raised:\n[postgresql]\u003e q2.select_all.entries\n\n# NOTE you can use both `order` and `@order`: local variables like `order` are required,\n# while instance variables like `@order` are optional.\n# To skip the order-part when provided:\n\u003c%= @order.presence \u0026\u0026 order_by(order) %\u003e\n# or use a default when order-part is always wanted but not always provided:\n\u003c%= order_by(@order || {id: :desc}) %\u003e\n```\n\n\n### ...in a Rails project\n\n\u003e [!NOTE]\n\u003e The included [example Rails app](./examples/demo) contains all data and queries described below.\n\nCreate a query:  \n```bash\nrails g query recent_articles\n```\n\nHave some SQL (for SQLite, in this example):\n```sql\n-- app/queries/recent_articles.sql\nWITH settings(min_published_on) as (\n  values(COALESCE(:since, datetime('now', '-6 months')))\n),\n\nrecent_articles(article_id, article_title, article_published_on, article_url) AS (\n  SELECT id, title, published_on, url\n  FROM articles\n  RIGHT JOIN settings\n  WHERE published_on \u003e settings.min_published_on\n),\n\ntags_by_article(article_id, tags) AS (\n  SELECT articles_tags.article_id,\n    json_group_array(tags.name) AS tags\n  FROM articles_tags\n  JOIN tags ON articles_tags.tag_id = tags.id\n  GROUP BY articles_tags.article_id\n)\n\nSELECT recent_articles.*,\n       group_concat(json_each.value, ',' ORDER BY value ASC) tags_str\nFROM recent_articles\nJOIN tags_by_article USING(article_id),\n  json_each(tags)\nWHERE EXISTS (\n  SELECT 1\n  FROM json_each(tags)\n  WHERE json_each.value LIKE :tag OR :tag IS NULL\n)\nGROUP BY recent_articles.article_id\nORDER BY recent_articles.article_published_on\n```\n\nThe result would look like this:\n\n```ruby\n[{\"article_id\"=\u003e292,\n \"article_title\"=\u003e\"Rails Versions 7.0.8.2, and 7.1.3.3 have been released!\",\n \"article_published_on\"=\u003e\"2024-05-17\",\n \"article_url\"=\u003e\"https://rubyonrails.org/2024/5/17/Rails-Versions-7-0-8-2-and-7-1-3-3-have-been-released\",\n \"tags_str\"=\u003e\"release:7x,release:revision\"},\n...\n]\n```\n\nEven for this fairly trivial query, there's already quite some things 'encoded' that we might want to verify or capture in tests:\n- only certain columns\n- only published articles\n- only articles _with_ tags\n- only articles published after some date\n  - either provided or using the default\n- articles are sorted in a certain order\n- tags appear in a certain order and are formatted a certain way\n\nUsing the SQL-rewriting capabilities shown below, this library allows you to express these assertions in tests or verify them during development.\n\n### Verify query results\n\n\u003e [!NOTE]\n\u003e There's `AppQuery#select_all`, `AppQuery#select_one` and `AppQuery#select_value` to execute a query. `select_(all|one)` are tiny wrappers around the equivalent methods from `ActiveRecord::Base.connection`.  \n\u003e Instead of [positional arguments](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/DatabaseStatements.html#method-i-select_all), these methods accept keywords `select`, `binds` and `cast`. See below for examples.\n\nGiven the query above, you can get the result like so:\n```ruby\nAppQuery[:recent_articles].select_all.entries\n# =\u003e\n[{\"article_id\"=\u003e292,\n \"article_title\"=\u003e\"Rails Versions 7.0.8.2, and 7.1.3.3 have been released!\",\n \"article_published_on\"=\u003e\"2024-05-17\",\n \"article_url\"=\u003e\"https://rubyonrails.org/2024/5/17/Rails-Versions-7-0-8-2-and-7-1-3-3-have-been-released\",\n \"tags_str\"=\u003e\"release:7x,release:revision\"},\n...\n]\n\n# we can provide a different cut off date via binds:\nAppQuery[:recent_articles].select_all(binds: {since: 1.month.ago}).entries\n\n# NOTE: by default the binds get initialized with nil, e.g. for this example {since: nil, tag: nil}\n# This prevents you from having to provide all binds every time. Default values are put in the SQL (via COALESCE).\n```\n\nWe can also dig deeper by query-ing the result, i.e. the CTE `:_`:\n\n```ruby\nAppQuery[:recent_articles].select_one(\"select count(*) as cnt from :_\")\n# =\u003e {\"cnt\" =\u003e 13}\n\n# For these kind of aggregate queries, we're only interested in the value:\nAppQuery[:recent_articles].select_value(\"select count(*) from :_\")\n# =\u003e 13\n\n# but there's also the shorthand #count (which takes a sub-select):\nAppQuery[:recent_articles].count #=\u003e 13\nAppQuery[:recent_articles].count(binds: {since: 0}) #=\u003e 275\n```\n\nUse `AppQuery#with_select` to get a new AppQuery-instance with the rewritten SQL:\n```ruby\nputs AppQuery[:recent_articles].with_select(\"select id from :_\")\n```\n\n\n### Verify CTE results\n\nYou can select from a CTE similarly:\n```ruby\nAppQuery[:recent_articles].select_all(\"SELECT * FROM tags_by_article\")\n# =\u003e [{\"article_id\"=\u003e1, \"tags\"=\u003e\"[\\\"release:pre\\\",\\\"release:patch\\\",\\\"release:1x\\\"]\"},\n      ...]\n\n# NOTE how the tags are json strings. Casting allows us to turn these into proper arrays^1:\ncast = {tags: :json}\nAppQuery[:recent_articles].select_all(\"SELECT * FROM tags_by_article\", cast:)\n\n1) unlike SQLite, PostgreSQL has json and array types. Just casting suffices:\nAppQuery(\"select json_build_object('a', 1, 'b', true)\").select_one(cast: true)\n# =\u003e {\"json_build_object\"=\u003e{\"a\"=\u003e1, \"b\"=\u003etrue}}\n```\n\nUsing the methods `(prepend|append|replace)_cte`, we can rewrite the query beyond just the select:\n\n```ruby\nAppQuery[:recent_articles].replace_cte(\u003c\u003c~SQL).select_all.entries\nsettings(min_published_on) as (\n  values(datetime('now', '-12 months'))\n)\nSQL\n```\n\nYou could even mock existing tables (using PostgreSQL):\n```ruby\n# using Ruby data:\nsample_articles = [{id: 1, title: \"Some title\", published_on: 3.months.ago},\n                   {id: 2, title: \"Another title\", published_on: 1.months.ago}]\n# show the provided cutoff date works\nAppQuery[:recent_articles].prepend_cte(\u003c\u003c-CTE).select_all(binds: {since: 6.weeks.ago, articles: JSON[sample_articles]}).entries\narticles AS (\n  SELECT * from json_to_recordset(:articles) AS x(id int, title text, published_on timestamp)\n)\nCTE\n```\n\nUse `AppQuery#with_select` to get a new AppQuery-instance with the rewritten sql:\n```ruby\nputs AppQuery[:recent_articles].with_select(\"select * from some_cte\")\n```\n\n### Spec\n\nWhen generating a query `reports/weekly`, a spec-file like below is generated:\n\n```ruby\n# spec/queries/reports/weekly_query_spec.rb\nrequire \"rails_helper\"\n\nRSpec.describe \"AppQuery reports/weekly\", type: :query, default_binds: [] do\n  describe \"CTE articles\" do\n    specify do\n      expect(described_query.select_all(\"select * from :cte\")).to \\\n        include(a_hash_including(\"article_id\" =\u003e 1))\n\n      # short version: query, cte and select are all implied from descriptions\n      expect(select_all).to include(a_hash_including(\"article_id\" =\u003e 1))\n    end\n  end\nend\n```\n\nThere's some sugar:\n- `described_query`  \n  ...just like `described_class` in regular class specs.  \n  It's an instance of `AppQuery` based on the last word of the top-description (i.e. \"reports/weekly\" from \"AppQuery reports/weekly\").\n- `:cte` placeholder  \n  When doing `select_all`, you can rewrite the `SELECT` of the query by passing `select`. There's no need to use the full name of the CTE as the spec-description contains the name (i.e. \"articles\" in \"CTE articles\").\n- default_binds  \n  The `binds`-value used when not explicitly provided.  \n\n## API Documentation\n\nSee the [YARD documentation](https://eval.github.io/appquery/) for the full API reference.\n\n## Compatibility\n\n- 💾 tested with **SQLite** and **PostgreSQL**\n- 🚆 tested with Rails v7.x and v8.x (might still work with v6.1, but is no longer included in the test-matrix)\n- 💎 requires Ruby **\u003e=v3.2**  \n  Goal is to support [maintained Ruby versions](https://www.ruby-lang.org/en/downloads/branches/).\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. **Make sure to check it exits with status code 0.**\n\nUsing [mise](https://mise.jdx.dev/) for env-vars recommended.\n\n### console\n\nThe [console-script](./bin/console) is setup such that it's easy to connect with a database and experiment with the library:\n```bash\n$ bin/console sqlite3::memory:\n$ bin/console postgres://localhost:5432/some_db\n\n# more details\n$ bin/console -h\n\n# when needing an appraisal, use bin/run (this ensures signals are handled correctly):\n$ bin/run rails_head console\n```\n\n### various\n\nRun `rake spec` to run the tests.\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 the created tag, 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/eval/appquery.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feval%2Fappquery","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feval%2Fappquery","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feval%2Fappquery/lists"}