appquery 0.4.0.rc1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.irbrc +9 -0
- data/.yardopts +11 -0
- data/CHANGELOG.md +53 -2
- data/README.md +80 -307
- data/lib/app_query/render_helpers.rb +242 -0
- data/lib/app_query/rspec/helpers.rb +9 -1
- data/lib/app_query/tokenizer.rb +2 -1
- data/lib/app_query/version.rb +1 -1
- data/lib/app_query.rb +565 -210
- data/mise.toml +1 -1
- data/rakelib/yard.rake +17 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0ca8349f2df1ddf7e2248314238dd72ff5623d2924f1265f385cb1dfde8b274d
|
|
4
|
+
data.tar.gz: 12440ed32a10ca28acd01df89175906e2ed2f77105905ed44f905bc54def4c5d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8154cfbf83c7327cc6bdfbe501430007fb4ed9a5ecfb65f6d751358e3721ca853b9d4655968955147fa2e3aa4ea898db9f71bd3da19647e4e550d2887121daab
|
|
7
|
+
data.tar.gz: a51fc511fa7b5da2bf03c4d63910abb1bb980b95fc300bf188a8749e726e965d781d14d28ffa3240dbca515c7d2bb0f5e83041eae61bb6ea7e815a1f7a13d19f
|
data/.irbrc
CHANGED
|
@@ -17,4 +17,13 @@ IRB.conf[:PROMPT][:APPQUERY] = {
|
|
|
17
17
|
PROMPT_C: "#{db_indicator}appquery* ",
|
|
18
18
|
RETURN: "=> %s\n"
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
# copy from history easier as prompt is not repeated across lines
|
|
22
|
+
IRB.conf[:PROMPT][:CLEAN] = {
|
|
23
|
+
PROMPT_I: ">> ",
|
|
24
|
+
PROMPT_S: " ", # same width as ">> "
|
|
25
|
+
PROMPT_C: " ",
|
|
26
|
+
PROMPT_N: " ",
|
|
27
|
+
RETURN: "=> %s\n"
|
|
28
|
+
}
|
|
20
29
|
IRB.conf[:PROMPT_MODE] = :APPQUERY
|
data/.yardopts
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
-
## [0.
|
|
3
|
+
## [0.5.0] - 2025-12-21
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### 💥 Breaking Changes
|
|
6
|
+
|
|
7
|
+
- 🔄 **`select:` keyword argument removed** — use positional argument instead
|
|
8
|
+
```ruby
|
|
9
|
+
# before
|
|
10
|
+
query.select_all(select: "SELECT * FROM :_")
|
|
11
|
+
# after
|
|
12
|
+
query.select_all("SELECT * FROM :_")
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### ✨ Features
|
|
16
|
+
|
|
17
|
+
- 🍾 **Add paginate ERB-helper**
|
|
18
|
+
```ruby
|
|
19
|
+
SELECT * FROM articles
|
|
20
|
+
<%= paginate(page: 1, per_page: 15) %>
|
|
21
|
+
# SELECT * FROM articles LIMIT 15 OFFSET 0
|
|
22
|
+
```
|
|
23
|
+
- 🧰 **Resolve query without extension**
|
|
24
|
+
`AppQuery[:weekly_sales]` loads `weekly_sales.sql` or `weekly_sales.sql.erb`.
|
|
25
|
+
- 🔗 **Nested result queries** via `with_select` — chain transformations using `:_` placeholder to reference the previous result
|
|
26
|
+
```ruby
|
|
27
|
+
active_users = AppQuery("SELECT * FROM users").with_select("SELECT * FROM :_ WHERE active")
|
|
28
|
+
active_users.count("SELECT * FROM :_ WHERE admin")
|
|
29
|
+
```
|
|
30
|
+
- 🚀 **New methods**: `#column`, `#ids`, `#count`, `#entries` — efficient shortcuts that only fetch what you need
|
|
31
|
+
```ruby
|
|
32
|
+
query.column(:email) # SELECT email only
|
|
33
|
+
query.ids # SELECT id only
|
|
34
|
+
query.count # SELECT COUNT(*) only
|
|
35
|
+
query.entries # shorthand for select_all.entries
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 🐛 Fixes
|
|
39
|
+
|
|
40
|
+
- 🔧 Fix leading whitespace in `prepend_cte` causing parse errors
|
|
41
|
+
- 🔧 Fix binds being reset when no placeholders found
|
|
42
|
+
- ⚡ `select_one` now uses `LIMIT 1` for better performance
|
|
43
|
+
|
|
44
|
+
### 📚 Documentation
|
|
45
|
+
|
|
46
|
+
- 📖 Revised README with cleaner intro and examples
|
|
47
|
+
- 🏠 Added example Rails app in `examples/demo`
|
|
48
|
+
|
|
49
|
+
## [0.4.0] - 2025-12-15
|
|
50
|
+
|
|
51
|
+
### features
|
|
52
|
+
|
|
53
|
+
- add insert, update and delete
|
|
54
|
+
- API docs at [eval.github.io/appquery](https://eval.github.io/appquery)
|
|
55
|
+
- add ERB-helpers [values, bind and quote ](https://eval.github.io/appquery/AppQuery/RenderHelpers.html).
|
|
56
|
+
- enabled trusted publishing to rubygems.org
|
data/README.md
CHANGED
|
@@ -1,68 +1,42 @@
|
|
|
1
1
|
# AppQuery - raw SQL 🥦, cooked :stew:
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/appquery)
|
|
4
|
+
[](https://eval.github.io/appquery/)
|
|
4
5
|
|
|
5
|
-
A
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
```ruby
|
|
37
|
-
AppQuery(%{select array[1,2]}).select_value #=> [1,2]
|
|
38
|
-
cast = {"data" => ActiveRecord::Type::Json.new}
|
|
39
|
-
AppQuery(%{select '{"a": 1}' as data}).select_value(cast:)
|
|
40
|
-
```
|
|
41
|
-
- **...helpers to rewrite a query for introspection during development and testing**
|
|
42
|
-
See what a CTE yields: `query.select_all(select: "SELECT * FROM some_cte")`.
|
|
43
|
-
Query the end result: `query.select_one(select: "SELECT COUNT(*) FROM _ WHERE ...")`.
|
|
44
|
-
Append/prepend CTEs:
|
|
45
|
-
```ruby
|
|
46
|
-
query.prepend_cte(<<~CTE)
|
|
47
|
-
articles(id, title) AS (
|
|
48
|
-
VALUES(1, 'Some title'),
|
|
49
|
-
(2, 'Another article'))
|
|
50
|
-
CTE
|
|
51
|
-
```
|
|
52
|
-
- **...rspec-helpers**
|
|
53
|
-
```ruby
|
|
54
|
-
RSpec.describe "AppQuery reports/weekly", type: :query do
|
|
55
|
-
describe "CTE some_cte" do
|
|
56
|
-
# see what this CTE yields
|
|
57
|
-
expect(described_query.select_all(select: "select * from some_cte")).to \
|
|
58
|
-
include(a_hash_including("id" => 1))
|
|
59
|
-
|
|
60
|
-
# shorter: the query and CTE are derived from the describe-descriptions so this suffices:
|
|
61
|
-
expect(select_all).to include ...
|
|
62
|
-
```
|
|
6
|
+
A 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.
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
# Load and execute
|
|
10
|
+
week = AppQuery[:weekly_sales].with_binds(week: 1, year: 2025)
|
|
11
|
+
week.entries
|
|
12
|
+
#=> [{"week" => 2025-01-13, "category" => "Electronics", "revenue" => 12500, "target_met" => true}, ...]
|
|
13
|
+
|
|
14
|
+
# Filter results (query wraps in CTE, :_ references it)
|
|
15
|
+
week.count("SELECT * FROM :_ WHERE NOT target_met")
|
|
16
|
+
#=> 3
|
|
17
|
+
|
|
18
|
+
# Extract a column efficiently (only fetches that column)
|
|
19
|
+
week.column(:category)
|
|
20
|
+
#=> ["Electronics", "Clothing", "Home & Garden"]
|
|
21
|
+
|
|
22
|
+
# Named binds with defaults
|
|
23
|
+
AppQuery[:weekly_sales].select_all(binds: {min_revenue: 5000})
|
|
24
|
+
|
|
25
|
+
# ERB templating
|
|
26
|
+
AppQuery("SELECT * FROM contracts <%= order_by(ordering) %>")
|
|
27
|
+
.render(ordering: {year: :desc}).select_all
|
|
28
|
+
|
|
29
|
+
# Custom type casting
|
|
30
|
+
AppQuery("SELECT metadata FROM products").select_all(cast: {"metadata" => ActiveRecord::Type::Json.new})
|
|
31
|
+
|
|
32
|
+
# Inspect/mock CTEs for testing
|
|
33
|
+
query.prepend_cte("sales AS (SELECT * FROM mock_data)")
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**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
|
|
63
37
|
|
|
64
38
|
> [!IMPORTANT]
|
|
65
|
-
> **Status**: alpha. API might change. See the CHANGELOG for breaking changes when upgrading.
|
|
39
|
+
> **Status**: alpha. API might change. See [the CHANGELOG](./CHANGELOG.md) for breaking changes when upgrading.
|
|
66
40
|
>
|
|
67
41
|
|
|
68
42
|
## Rationale
|
|
@@ -107,14 +81,23 @@ The prompt indicates what adapter the example uses:
|
|
|
107
81
|
=> "2025-05-10"
|
|
108
82
|
|
|
109
83
|
# binds
|
|
110
|
-
|
|
111
|
-
[postgresql]> AppQuery(%{select now() - ($1)::interval as date}).select_value(binds: ['2 days'])
|
|
112
|
-
# named binds
|
|
84
|
+
## named binds
|
|
113
85
|
[postgresql]> AppQuery(%{select now() - (:interval)::interval as date}).select_value(binds: {interval: '2 days'})
|
|
114
86
|
|
|
87
|
+
## not all binds need to be provided (ie they are nil by default) - so defaults can be added in SQL:
|
|
88
|
+
[postgresql]> AppQuery(<<~SQL).select_all(binds: {ts1: 2.day.ago, ts2: Time.now}).column("series")
|
|
89
|
+
SELECT
|
|
90
|
+
generate_series(:ts1::timestamp, COALESCE(:ts2, :ts2::timestamp, COALESCE(:interval, '5 minutes')::interval)
|
|
91
|
+
AS series
|
|
92
|
+
SQL
|
|
93
|
+
|
|
115
94
|
# casting
|
|
116
|
-
|
|
117
|
-
|
|
95
|
+
## Cast values are used by default:
|
|
96
|
+
[postgresql]> AppQuery(%{select date('now')}).select_first
|
|
97
|
+
=> {"today" => Sat, 10 May 2025}
|
|
98
|
+
## compare ActiveRecord
|
|
99
|
+
[postgresql]> ActiveRecord::Base.connection.select_first(%{select date('now') as today})
|
|
100
|
+
=> {"today" => "2025-12-20"}
|
|
118
101
|
|
|
119
102
|
## SQLite doesn't have a notion of dates or timestamp's so casting won't do anything:
|
|
120
103
|
[sqlite]> AppQuery(%{select date('now') as today}).select_one(cast: true)
|
|
@@ -137,10 +120,12 @@ casts = {"today" => ActiveRecord::Type::Date.new}
|
|
|
137
120
|
SQL
|
|
138
121
|
|
|
139
122
|
## query the articles-CTE
|
|
140
|
-
[postgresql]> q.select_all(
|
|
123
|
+
[postgresql]> q.select_all(%{select * from articles where id < 2}).to_a
|
|
141
124
|
|
|
142
|
-
## query the end-result (available
|
|
143
|
-
[postgresql]> q.select_one(
|
|
125
|
+
## query the end-result (available via the placeholder ':_')
|
|
126
|
+
[postgresql]> q.select_one(%{select * from :_ limit 1})
|
|
127
|
+
### shorthand for that
|
|
128
|
+
[postgresql]> q.first
|
|
144
129
|
|
|
145
130
|
## ERB templating
|
|
146
131
|
# Extract a query from q that can be sorted dynamically:
|
|
@@ -167,7 +152,7 @@ SQL
|
|
|
167
152
|
### ...in a Rails project
|
|
168
153
|
|
|
169
154
|
> [!NOTE]
|
|
170
|
-
> The included [example Rails app](./examples/
|
|
155
|
+
> The included [example Rails app](./examples/demo) contains all data and queries described below.
|
|
171
156
|
|
|
172
157
|
Create a query:
|
|
173
158
|
```bash
|
|
@@ -177,15 +162,15 @@ rails g query recent_articles
|
|
|
177
162
|
Have some SQL (for SQLite, in this example):
|
|
178
163
|
```sql
|
|
179
164
|
-- app/queries/recent_articles.sql
|
|
180
|
-
WITH settings(
|
|
181
|
-
values(datetime('now', '-6 months'))
|
|
165
|
+
WITH settings(min_published_on) as (
|
|
166
|
+
values(COALESCE(:since, datetime('now', '-6 months')))
|
|
182
167
|
),
|
|
183
168
|
|
|
184
169
|
recent_articles(article_id, article_title, article_published_on, article_url) AS (
|
|
185
170
|
SELECT id, title, published_on, url
|
|
186
171
|
FROM articles
|
|
187
172
|
RIGHT JOIN settings
|
|
188
|
-
WHERE published_on >
|
|
173
|
+
WHERE published_on > settings.min_published_on
|
|
189
174
|
),
|
|
190
175
|
|
|
191
176
|
tags_by_article(article_id, tags) AS (
|
|
@@ -204,7 +189,7 @@ JOIN tags_by_article USING(article_id),
|
|
|
204
189
|
WHERE EXISTS (
|
|
205
190
|
SELECT 1
|
|
206
191
|
FROM json_each(tags)
|
|
207
|
-
WHERE json_each.value LIKE
|
|
192
|
+
WHERE json_each.value LIKE :tag OR :tag IS NULL
|
|
208
193
|
)
|
|
209
194
|
GROUP BY recent_articles.article_id
|
|
210
195
|
ORDER BY recent_articles.article_published_on
|
|
@@ -251,28 +236,31 @@ AppQuery[:recent_articles].select_all.entries
|
|
|
251
236
|
...
|
|
252
237
|
]
|
|
253
238
|
|
|
254
|
-
# we can provide a different cut off date via binds
|
|
255
|
-
AppQuery[:recent_articles].select_all(binds:
|
|
239
|
+
# we can provide a different cut off date via binds:
|
|
240
|
+
AppQuery[:recent_articles].select_all(binds: {since: 1.month.ago}).entries
|
|
256
241
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
For Postgres you would always need to provide 2 values, e.g. `binds: [nil, nil]`.
|
|
242
|
+
# NOTE: by default the binds get initialized with nil, e.g. for this example {since: nil, tag: nil}
|
|
243
|
+
# This prevents you from having to provide all binds every time. Default values are put in the SQL (via COALESCE).
|
|
260
244
|
```
|
|
261
245
|
|
|
262
|
-
We can also dig deeper by query-ing the result, i.e. the CTE
|
|
246
|
+
We can also dig deeper by query-ing the result, i.e. the CTE `:_`:
|
|
263
247
|
|
|
264
248
|
```ruby
|
|
265
|
-
AppQuery[:recent_articles].select_one(
|
|
249
|
+
AppQuery[:recent_articles].select_one("select count(*) as cnt from :_")
|
|
266
250
|
# => {"cnt" => 13}
|
|
267
251
|
|
|
268
252
|
# For these kind of aggregate queries, we're only interested in the value:
|
|
269
|
-
AppQuery[:recent_articles].select_value(
|
|
253
|
+
AppQuery[:recent_articles].select_value("select count(*) from :_")
|
|
270
254
|
# => 13
|
|
255
|
+
|
|
256
|
+
# but there's also the shorthand #count (which takes a sub-select):
|
|
257
|
+
AppQuery[:recent_articles].count #=> 13
|
|
258
|
+
AppQuery[:recent_articles].count(binds: {since: 0}) #=> 275
|
|
271
259
|
```
|
|
272
260
|
|
|
273
261
|
Use `AppQuery#with_select` to get a new AppQuery-instance with the rewritten SQL:
|
|
274
262
|
```ruby
|
|
275
|
-
puts AppQuery[:recent_articles].with_select("select
|
|
263
|
+
puts AppQuery[:recent_articles].with_select("select id from :_")
|
|
276
264
|
```
|
|
277
265
|
|
|
278
266
|
|
|
@@ -280,15 +268,15 @@ puts AppQuery[:recent_articles].with_select("select * from _")
|
|
|
280
268
|
|
|
281
269
|
You can select from a CTE similarly:
|
|
282
270
|
```ruby
|
|
283
|
-
AppQuery[:recent_articles].select_all(
|
|
271
|
+
AppQuery[:recent_articles].select_all("SELECT * FROM tags_by_article")
|
|
284
272
|
# => [{"article_id"=>1, "tags"=>"[\"release:pre\",\"release:patch\",\"release:1x\"]"},
|
|
285
273
|
...]
|
|
286
274
|
|
|
287
275
|
# NOTE how the tags are json strings. Casting allows us to turn these into proper arrays^1:
|
|
288
276
|
types = {"tags" => ActiveRecord::Type::Json.new}
|
|
289
|
-
AppQuery[:recent_articles].select_all(
|
|
277
|
+
AppQuery[:recent_articles].select_all("SELECT * FROM tags_by_article", cast: types)
|
|
290
278
|
|
|
291
|
-
1)
|
|
279
|
+
1) unlike SQLite, PostgreSQL has json and array types. Just casting suffices:
|
|
292
280
|
AppQuery("select json_build_object('a', 1, 'b', true)").select_one(cast: true)
|
|
293
281
|
# => {"json_build_object"=>{"a"=>1, "b"=>true}}
|
|
294
282
|
```
|
|
@@ -297,7 +285,7 @@ Using the methods `(prepend|append|replace)_cte`, we can rewrite the query beyon
|
|
|
297
285
|
|
|
298
286
|
```ruby
|
|
299
287
|
AppQuery[:recent_articles].replace_cte(<<~SQL).select_all.entries
|
|
300
|
-
settings(
|
|
288
|
+
settings(min_published_on) as (
|
|
301
289
|
values(datetime('now', '-12 months'))
|
|
302
290
|
)
|
|
303
291
|
SQL
|
|
@@ -309,10 +297,10 @@ You could even mock existing tables (using PostgreSQL):
|
|
|
309
297
|
sample_articles = [{id: 1, title: "Some title", published_on: 3.months.ago},
|
|
310
298
|
{id: 2, title: "Another title", published_on: 1.months.ago}]
|
|
311
299
|
# show the provided cutoff date works
|
|
312
|
-
AppQuery[:recent_articles].prepend_cte(<<-CTE).select_all(binds:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
300
|
+
AppQuery[:recent_articles].prepend_cte(<<-CTE).select_all(binds: {since: 6.weeks.ago, articles: JSON[sample_articles]}).entries
|
|
301
|
+
articles AS (
|
|
302
|
+
SELECT * from json_to_recordset(:articles) AS x(id int, title text, published_on timestamp)
|
|
303
|
+
)
|
|
316
304
|
CTE
|
|
317
305
|
```
|
|
318
306
|
|
|
@@ -332,7 +320,7 @@ require "rails_helper"
|
|
|
332
320
|
RSpec.describe "AppQuery reports/weekly", type: :query, default_binds: [] do
|
|
333
321
|
describe "CTE articles" do
|
|
334
322
|
specify do
|
|
335
|
-
expect(described_query.select_all(
|
|
323
|
+
expect(described_query.select_all("select * from :cte")).to \
|
|
336
324
|
include(a_hash_including("article_id" => 1))
|
|
337
325
|
|
|
338
326
|
# short version: query, cte and select are all implied from descriptions
|
|
@@ -350,225 +338,10 @@ There's some sugar:
|
|
|
350
338
|
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").
|
|
351
339
|
- default_binds
|
|
352
340
|
The `binds`-value used when not explicitly provided.
|
|
353
|
-
E.g. given a query with a where-clause `WHERE published_at > COALESCE($1::timestamp, NOW() - '3 month'::interval)`, when setting `defaults_binds: [nil]` then `select_all` works like `select_all(binds: [nil])`.
|
|
354
|
-
|
|
355
|
-
## 💎 API Doc 💎
|
|
356
|
-
|
|
357
|
-
### generic
|
|
358
|
-
|
|
359
|
-
<details>
|
|
360
|
-
<summary><code>AppQuery(sql) ⇒ AppQuery::Q</code></summary>
|
|
361
|
-
|
|
362
|
-
### Examples
|
|
363
|
-
|
|
364
|
-
```ruby
|
|
365
|
-
AppQuery("some sql")
|
|
366
|
-
```
|
|
367
|
-
</details>
|
|
368
|
-
|
|
369
|
-
### module AppQuery
|
|
370
|
-
|
|
371
|
-
<details>
|
|
372
|
-
<summary><code>AppQuery[query_name] ⇒ AppQuery::Q</code></summary>
|
|
373
|
-
|
|
374
|
-
### Examples
|
|
375
|
-
|
|
376
|
-
```ruby
|
|
377
|
-
AppQuery[:recent_articles]
|
|
378
|
-
AppQuery["export/articles"]
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
</details>
|
|
382
|
-
|
|
383
|
-
<details>
|
|
384
|
-
<summary><code>AppQuery.configure {|Configuration| ... } ⇒ void </code></summary>
|
|
385
|
-
|
|
386
|
-
Configure AppQuery.
|
|
387
|
-
|
|
388
|
-
### Examples
|
|
389
|
-
|
|
390
|
-
```ruby
|
|
391
|
-
AppQuery.configure do |cfg|
|
|
392
|
-
cfg.query_path = "db/queries" # default: "app/queries"
|
|
393
|
-
end
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
</details>
|
|
397
|
-
|
|
398
|
-
<details>
|
|
399
|
-
<summary><code>AppQuery.configuration ⇒ AppQuery::Configuration </code></summary>
|
|
400
|
-
|
|
401
|
-
Get configuration
|
|
402
|
-
|
|
403
|
-
### Examples
|
|
404
|
-
|
|
405
|
-
```ruby
|
|
406
|
-
AppQuery.configure do |cfg|
|
|
407
|
-
cfg.query_path = "db/queries" # default: "app/queries"
|
|
408
|
-
end
|
|
409
|
-
AppQuery.configuration
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
</details>
|
|
413
|
-
|
|
414
|
-
### class AppQuery::Q
|
|
415
|
-
|
|
416
|
-
Instantiate via `AppQuery(sql)` or `AppQuery[:query_file]`.
|
|
417
|
-
|
|
418
|
-
<details>
|
|
419
|
-
<summary><code>AppQuery::Q#cte_names ⇒ [Array< String >] </code></summary>
|
|
420
|
-
|
|
421
|
-
Returns names of CTEs in query.
|
|
422
|
-
|
|
423
|
-
### Examples
|
|
424
|
-
|
|
425
|
-
```ruby
|
|
426
|
-
AppQuery("select * from articles").cte_names # => []
|
|
427
|
-
AppQuery("with foo as(select 1) select * from foo").cte_names # => ["foo"]
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
</details>
|
|
431
|
-
|
|
432
|
-
<details>
|
|
433
|
-
<summary><code>AppQuery::Q#recursive? ⇒ Boolean </code></summary>
|
|
434
|
-
|
|
435
|
-
Returns whether or not the WITH-clause is recursive or not.
|
|
436
|
-
|
|
437
|
-
### Examples
|
|
438
341
|
|
|
439
|
-
|
|
440
|
-
AppQuery("select * from articles").recursive? # => false
|
|
441
|
-
AppQuery("with recursive foo as(select 1) select * from foo") # => true
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
</details>
|
|
445
|
-
|
|
446
|
-
<details>
|
|
447
|
-
<summary><code>AppQuery::Q#select ⇒ String </code></summary>
|
|
448
|
-
|
|
449
|
-
Returns select-part of the query. When using CTEs, this will be `<select>` in a query like `with foo as (select 1) <select>`.
|
|
450
|
-
|
|
451
|
-
### Examples
|
|
452
|
-
|
|
453
|
-
```ruby
|
|
454
|
-
AppQuery("select * from articles") # => "select * from articles"
|
|
455
|
-
AppQuery("with foo as(select 1) select * from foo") # => "select * from foo"
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
</details>
|
|
459
|
-
|
|
460
|
-
#### query execution
|
|
461
|
-
|
|
462
|
-
<details>
|
|
463
|
-
<summary><code>AppQuery::Q#select_all(select: nil, binds: [], cast: false) ⇒ AppQuery::Result</code></summary>
|
|
464
|
-
|
|
465
|
-
`select` replaces the existing select. The existing select is wrapped in a CTE named `_`.
|
|
466
|
-
`binds` array with values for any (positional) placeholder in the query.
|
|
467
|
-
`cast` boolean or `Hash` indicating whether or not (and how) to cast. E.g. `{"some_column" => ActiveRecord::Type::Date.new}`.
|
|
468
|
-
|
|
469
|
-
### Examples
|
|
470
|
-
|
|
471
|
-
```ruby
|
|
472
|
-
# SQLite
|
|
473
|
-
aq = AppQuery(<<~SQL)
|
|
474
|
-
with data(id, title) as (
|
|
475
|
-
values('1', 'Some title'),
|
|
476
|
-
('2', 'Another title')
|
|
477
|
-
)
|
|
478
|
-
select * from data
|
|
479
|
-
where id=?1 or ?1 is null
|
|
480
|
-
SQL
|
|
481
|
-
|
|
482
|
-
# selecting from the select
|
|
483
|
-
aq.select_all(select: "select * from _ where id > 1").entries #=> [{...}]
|
|
484
|
-
|
|
485
|
-
# selecting from a CTE
|
|
486
|
-
aq.select_all(select: "select id from data").entries
|
|
487
|
-
|
|
488
|
-
# casting
|
|
489
|
-
aq.select_all(select: "select id from data", cast: {"id" => ActiveRecord::Type::Integer.new})
|
|
490
|
-
|
|
491
|
-
# binds
|
|
492
|
-
aq.select_all(binds: ['2'])
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
</details>
|
|
496
|
-
|
|
497
|
-
<details>
|
|
498
|
-
<summary><code>AppQuery::Q#select_one(select: nil, binds: [], cast: false) ⇒ AppQuery::Result </code></summary>
|
|
499
|
-
|
|
500
|
-
First result from `AppQuery::Q#select_all`.
|
|
501
|
-
|
|
502
|
-
See examples from `AppQuery::Q#select_all`.
|
|
503
|
-
|
|
504
|
-
</details>
|
|
505
|
-
|
|
506
|
-
<details>
|
|
507
|
-
<summary><code>AppQuery::Q#select_value(select: nil, binds: [], cast: false) ⇒ AppQuery::Result </code></summary>
|
|
508
|
-
|
|
509
|
-
First value from `AppQuery::Q#select_one`. Typically for selects like `select count(*) ...`, `select min(article_published_on) ...`.
|
|
342
|
+
## API Documentation
|
|
510
343
|
|
|
511
|
-
See
|
|
512
|
-
|
|
513
|
-
</details>
|
|
514
|
-
|
|
515
|
-
#### query rewriting
|
|
516
|
-
|
|
517
|
-
<details>
|
|
518
|
-
<summary><code>AppQuery::Q#with_select(sql) ⇒ AppQuery::Q</code></summary>
|
|
519
|
-
|
|
520
|
-
Returns new instance with provided select. The existing select is available via CTE `_`.
|
|
521
|
-
|
|
522
|
-
### Examples
|
|
523
|
-
|
|
524
|
-
```ruby
|
|
525
|
-
puts AppQuery("select 1").with_select("select 2")
|
|
526
|
-
WITH _ as (
|
|
527
|
-
select 1
|
|
528
|
-
)
|
|
529
|
-
select 2
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
</details>
|
|
533
|
-
|
|
534
|
-
<details>
|
|
535
|
-
<summary><code>AppQuery::Q#prepend_cte(sql) ⇒ AppQuery::Q</code></summary>
|
|
536
|
-
|
|
537
|
-
Returns new instance with provided CTE.
|
|
538
|
-
|
|
539
|
-
### Examples
|
|
540
|
-
|
|
541
|
-
```ruby
|
|
542
|
-
query.prepend_cte("foo as (values(1, 'Some article'))").cte_names # => ["foo", "existing_cte"]
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
</details>
|
|
546
|
-
|
|
547
|
-
<details>
|
|
548
|
-
<summary><code>AppQuery::Q#append_cte(sql) ⇒ AppQuery::Q</code></summary>
|
|
549
|
-
|
|
550
|
-
Returns new instance with provided CTE.
|
|
551
|
-
|
|
552
|
-
### Examples
|
|
553
|
-
|
|
554
|
-
```ruby
|
|
555
|
-
query.append_cte("foo as (values(1, 'Some article'))").cte_names # => ["existing_cte", "foo"]
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
</details>
|
|
559
|
-
|
|
560
|
-
<details>
|
|
561
|
-
<summary><code>AppQuery::Q#replace_cte(sql) ⇒ AppQuery::Q</code></summary>
|
|
562
|
-
|
|
563
|
-
Returns new instance with replaced CTE. Raises `ArgumentError` when CTE does not already exist.
|
|
564
|
-
|
|
565
|
-
### Examples
|
|
566
|
-
|
|
567
|
-
```ruby
|
|
568
|
-
query.replace_cte("recent_articles as (select values(1, 'Some article'))")
|
|
569
|
-
```
|
|
570
|
-
|
|
571
|
-
</details>
|
|
344
|
+
See the [YARD documentation](https://eval.github.io/appquery/) for the full API reference.
|
|
572
345
|
|
|
573
346
|
## Compatibility
|
|
574
347
|
|