[Unreleased]
💥 Breaking Changes
- ⚠️ **
AppQuery::Mappableextension API changed.**
Row-level middleware now appends a transformer to the underlyingQ'srow_builderpipeline via an overridden#query, instead of overridingselect_all/select_one. The previous pattern of overriding those two methods will silently do nothing on row-returning paths it didn't cover (entries,first,last,take,with_select(non_nil).first, …). Any custom middleware that overrodeselect_all/select_oneshould migrate to:def query @query ||= super.tap { |q| q.row_builder << method(:build_row) } end - ⚠️ **
Q#columnnow raisesArgumentErrorfor unknown columns.**
Previously, on SQLite,q.column(:typo)silently returned a row per record containing the string"typo"(the SQLite "double-quoted strings are identifiers OR string literals" quirk masked the missing column). It now pre-validates againstcolumn_namesand raises with the available column list — consistently across SQLite and PostgreSQL.
✨ Features
- 🧩
AppQuery::RowBuilder— composable pipeline of row transformers exposed asQ#row_builder. Append withq.row_builder << callable; transformers run in registration order. Multiple row-level middlewares stack cleanly inincludeorder. The pipeline is applied everywhereQexposes rows (entries,first,last,take,take_last,with_select(...).first, …) and is independently copied acrossdeep_dupso chained queries don't mutate their parent. - 🎯
Mappableis now one method. Maps everywhere — includingentries,last,take(n),with_select("…").firstpaths that previously slipped through.rawbypass still works. - 🐛
Q#columntypo protection — see breaking-change note above. - 🐛 Comments inside CTE selects no longer break tokenization; the whole
(SELECT … -- foo … )is preserved as a singleCTE_SELECTtoken. - Publishing gem requires MFA
0.8.0
Releasedate: 14-1-2026
Rubygems: https://rubygems.org/gems/appquery/versions/0.8.0
💥 Breaking Changes
- ⚠️ RSpec helpers refactored
Query under test is expected to be a class,select_*are no longer separate helpers:expect(described_query.first).to \ include("id" => be_a(Integer), ...) expect(described_query.entries).to include(a_hash_including("item_code" => "123456"))
✨ Features
-
📤
copy_to— efficient PostgreSQL COPY export to CSV/text/binary# Return as string csv = AppQuery[:users].copy_to # Write to file AppQuery[:users].copy_to(dest: "export.csv") # Stream to IO (e.g., Rails response) query.copy_to(dest: response.stream) -
🎯
cte(:name)— focus a query on a specific CTE for testing or inspectionquery = AppQuery("WITH active AS (...), admins AS (...) SELECT ...") query.cte(:active).entries # select from the active CTE query.cte(:admins).count # count rows in admins CTE -
🗃️
AppQuery.table(:name)— quick query from a tableAppQuery.table(:products).count AppQuery.table(:users).take(5) -
🔢
take(n)/take_last(n)— fetch first or last n rowsquery.take(5) # first 5 rows query.take_last(5) # last 5 rows -
⏮️
last— fetch the last row (counterpart tofirst)query.last # => {"id" => 42, "name" => "Zoe"} -
📋
column_names— get column names without fetching rowsquery.column_names # => ["id", "name", "email"] -
🦄
unique:keyword forQ#column— return distinct valuesquery.column(:status, unique: true) # => ["active", "pending"] -
🏗️ Overhauled generators — moved to
AppQuery::namespacerails g app_query:example # annotated example query rails g app_query:query Products rails g query Products # hidden alias rails g query --help # details
0.7.0
Releasedate: 8-1-2026
Rubygems: https://rubygems.org/gems/appquery/versions/0.7.0
💥 Breaking Changes
- ⛔ drop Ruby 3.2 support
Ruby 3.2 will be EOL in 2 months but is already no longer working for Rails >v8.1.
✨ Features
-
🗒️ Paginatable: unpaginated
Override any setting for pagination:query = ArticlesQuery.build => #<RecentQuery:0x000000016ed7ef78 @page=1, @per_page=10, ...> query.unpaginated.count #=> 699Also: when
@page.nil?then paginate erb-helper renders nothing:# articles_query.erb.sql # before # skip pagination when we need the total count <%= @page && paginate(page:, per_page:) -%> # after <%= paginate(page:, per_page:) -%> -
🌗 darkmode for API docs
🐛 Fixes
- 🔧 Fix literal strings containing parentheses breaking CTE-parsing.
0.6.0
Releasedate: 2-1-2026
Rubygems: https://rubygems.org/gems/appquery/versions/0.6.0
✨ Features
-
🏗️
AppQuery::BaseQuery— structured query objects with explicit parameter declarationclass ArticlesQuery < AppQuery::BaseQuery bind :author_id bind :status, default: nil var :order_by, default: "created_at DESC" cast published_at: :datetime end ArticlesQuery.new(author_id: 1).entries ArticlesQuery.new(author_id: 1, status: "draft").firstBenefits over
AppQuery[:my_query]:- Explicit
bindandvardeclarations with defaults - Unknown parameter validation (catches typos)
- Self-documenting:
ArticlesQuery.binds,ArticlesQuery.vars - Middleware support via concerns
- Explicit
-
📄
AppQuery::Paginatable— pagination middleware (Kaminari-compatible)class ApplicationQuery < AppQuery::BaseQuery include AppQuery::Paginatable per_page 25 end # With count (full pagination) articles = ArticlesQuery.new.paginate(page: 1).entries articles.total_pages # => 5 # Without count (large datasets, uses limit+1 trick) articles = ArticlesQuery.new.paginate(page: 1, without_count: true).entries articles.next_page # => 2 or nil -
🗺️
AppQuery::Mappable— map results to Ruby objectsclass ArticlesQuery < ApplicationQuery include AppQuery::Mappable class Item < Data.define(:title, :url, :published_on) end end articles = ArticlesQuery.new.entries articles.first.title # => "Hello World" articles.first.class # => ArticlesQuery::Item # Skip mapping ArticlesQuery.new.raw.entries.first # => {"title" => "Hello", ...} -
🔄
Result#transform!— transform result records in-placeresult = AppQuery[:users].select_all result.transform! { |row| row.merge("full_name" => "#{row['first']} #{row['last']}") } -
Add
any?,none?- efficient ways to see if there's any results for a query. -
🎯 Cast type shorthands — use symbols instead of explicit type classes
query.select_all(cast: {"published_on" => :date}) # instead of query.select_all(cast: {"published_on" => ActiveRecord::Type::Date.new})Supports all ActiveRecord types including adapter-specific ones (
:uuid,:jsonb, etc.). -
🔑 Indifferent access — for rows and cast keys
row = query.select_one row["name"] # works row[:name] # also works # cast keys can be symbols too query.select_all(cast: {published_on: :date})
[0.5.0] - 2025-12-21
💥 Breaking Changes
- 🔄
select:keyword argument removed — use positional argument instead# before query.select_all(select: "SELECT * FROM :_") # after query.select_all("SELECT * FROM :_")
✨ Features
- 🍾 Add paginate ERB-helper
SELECT * FROM articles <%= paginate(page: 1, per_page: 15) %> # SELECT * FROM articles LIMIT 15 OFFSET 0 - 🧰 Resolve query without extension
AppQuery[:weekly_sales]loadsweekly_sales.sqlorweekly_sales.sql.erb. - 🔗 Nested result queries via
with_select— chain transformations using:_placeholder to reference the previous resultactive_users = AppQuery("SELECT * FROM users").with_select("SELECT * FROM :_ WHERE active") active_users.count("SELECT * FROM :_ WHERE admin") - 🚀 New methods:
#column,#ids,#count,#entries— efficient shortcuts that only fetch what you needquery.column(:email) # SELECT email only query.ids # SELECT id only query.count # SELECT COUNT(*) only query.entries # shorthand for select_all.entries
🐛 Fixes
- 🔧 Fix leading whitespace in
prepend_ctecausing parse errors - 🔧 Fix binds being reset when no placeholders found
- ⚡
select_onenow usesLIMIT 1for better performance
📚 Documentation
- 📖 Revised README with cleaner intro and examples
- 🏠 Added example Rails app in
examples/demo
[0.4.0] - 2025-12-15
features
- add insert, update and delete
- API docs at eval.github.io/appquery
- add ERB-helpers values, bind and quote .
- enabled trusted publishing to rubygems.org