appquery 0.1.0 → 0.2.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/.envrc +6 -1
- data/.envrc.private.example +2 -0
- data/.standard.yml +3 -1
- data/README.md +458 -11
- data/lib/app_query/rspec/helpers.rb +90 -0
- data/lib/app_query/rspec.rb +5 -0
- data/lib/app_query/tokenizer.rb +356 -0
- data/lib/app_query/version.rb +1 -1
- data/lib/app_query.rb +238 -1
- data/lib/appquery.rb +1 -0
- data/lib/rails/generators/query/USAGE +10 -0
- data/lib/rails/generators/query/query_generator.rb +20 -0
- data/lib/rails/generators/query/templates/query.sql.tt +14 -0
- data/lib/rails/generators/rspec/query_generator.rb +20 -0
- data/lib/rails/generators/rspec/templates/query_spec.rb.tt +12 -0
- data/sig/appquery.rbs +1 -1
- data/tmp/.gitkeep +0 -0
- metadata +28 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c63ba7b9e16d2f5b7071f8dd9203695f3c46b956d0302ff27a5cab0970cc598f
|
4
|
+
data.tar.gz: 71bebdc43197fce0e8917a3e6f4be0615bcb28215ef004b2496ce0554241c54d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4f372fb1b1e524e05a2cdf5338d1114f76e08df3b2ec2028e9ec42b515dcbfe4c4c6d9d318a367351e884cfc3ed8e0c670f215d2ccdb072753271fa248993eed
|
7
|
+
data.tar.gz: d91262c52aac1d7cb753eab122c2fbf7d352bd160f5112e3139cf1b5bda848e05456644d3e8e37b2c038ba0795fcfe707470c5bb903cc05e0d0d81a30725de80
|
data/.envrc
CHANGED
data/.standard.yml
CHANGED
data/README.md
CHANGED
@@ -1,32 +1,478 @@
|
|
1
|
-
#
|
1
|
+
# AppQuery - raw SQL 🥦, cooked :stew:
|
2
2
|
|
3
|
-
|
3
|
+
[](https://badge.fury.io/rb/appquery)
|
4
4
|
|
5
|
-
|
5
|
+
A Rubygem :gem: that makes working with raw SQL queries in Rails projects more convenient.
|
6
|
+
Specifically it provides:
|
7
|
+
- **...a dedicated folder for queries**
|
8
|
+
e.g. `app/queries/reports/weekly.sql` is instantiated via `AppQuery["reports/weekly"]`.
|
9
|
+
- **...Rails/rspec generators**
|
10
|
+
```
|
11
|
+
$ rails generate query reports/weekly
|
12
|
+
create app/queries/reports/weekly.sql
|
13
|
+
invoke rspec
|
14
|
+
create spec/queries/reports/weekly_query_spec.rb
|
15
|
+
```
|
16
|
+
- **...helpers to rewrite a query for introspection during development and testing**
|
17
|
+
See what a CTE yields: `query.select_all(select: "SELECT * FROM some_cte")`.
|
18
|
+
Query the end result: `query.select_one(select: "SELECT COUNT(*) FROM _ WHERE ...")`.
|
19
|
+
Append/prepend CTEs:
|
20
|
+
```ruby
|
21
|
+
query.prepend_cte(<<~CTE)
|
22
|
+
articles(id, title) AS (
|
23
|
+
VALUES(1, 'Some title'),
|
24
|
+
(2, 'Another article'))
|
25
|
+
CTE
|
26
|
+
```
|
27
|
+
- **...rspec-helpers**
|
28
|
+
```ruby
|
29
|
+
RSpec.describe "AppQuery reports/weekly", type: :query do
|
30
|
+
describe "CTE some_cte" do
|
31
|
+
# see what this CTE yields
|
32
|
+
expect(described_query.select_all(select: "select * from some_cte")).to \
|
33
|
+
include(a_hash_including("id" => 1))
|
34
|
+
|
35
|
+
# shorter: the query and CTE are derived from the describe-descriptions so this suffices:
|
36
|
+
expect(select_all).to include ...
|
37
|
+
```
|
6
38
|
|
7
|
-
|
39
|
+
> [!IMPORTANT]
|
40
|
+
> **Status**: alpha. API might change. See the CHANGELOG for breaking changes when upgrading.
|
41
|
+
>
|
8
42
|
|
9
|
-
|
43
|
+
## Installation
|
10
44
|
|
11
45
|
Install the gem and add to the application's Gemfile by executing:
|
12
46
|
|
13
47
|
```bash
|
14
|
-
bundle add
|
48
|
+
bundle add appquery
|
15
49
|
```
|
16
50
|
|
17
|
-
|
51
|
+
## Usage
|
52
|
+
|
53
|
+
> [!NOTE]
|
54
|
+
> 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.
|
55
|
+
|
56
|
+
### Create
|
18
57
|
|
58
|
+
> [!NOTE]
|
59
|
+
> The included [example Rails app](./examples/ror) contains all data and queries described below.
|
60
|
+
|
61
|
+
Create a query:
|
19
62
|
```bash
|
20
|
-
|
63
|
+
rails g query recent_articles
|
21
64
|
```
|
22
65
|
|
23
|
-
|
66
|
+
Have some SQL (for SQLite, in this example):
|
67
|
+
```sql
|
68
|
+
-- app/queries/recent_articles.sql
|
69
|
+
WITH settings(default_min_published_on) as (
|
70
|
+
values(datetime('now', '-6 months'))
|
71
|
+
),
|
72
|
+
|
73
|
+
recent_articles(article_id, article_title, article_published_on, article_url) AS (
|
74
|
+
SELECT id, title, published_on, url
|
75
|
+
FROM articles
|
76
|
+
RIGHT JOIN settings
|
77
|
+
WHERE published_on > COALESCE(?1, settings.default_min_published_on)
|
78
|
+
),
|
79
|
+
|
80
|
+
tags_by_article(article_id, tags) AS (
|
81
|
+
SELECT articles_tags.article_id,
|
82
|
+
json_group_array(tags.name) AS tags
|
83
|
+
FROM articles_tags
|
84
|
+
JOIN tags ON articles_tags.tag_id = tags.id
|
85
|
+
GROUP BY articles_tags.article_id
|
86
|
+
)
|
87
|
+
|
88
|
+
SELECT recent_articles.*,
|
89
|
+
group_concat(json_each.value, ',' ORDER BY value ASC) tags_str
|
90
|
+
FROM recent_articles
|
91
|
+
JOIN tags_by_article USING(article_id),
|
92
|
+
json_each(tags)
|
93
|
+
WHERE EXISTS (
|
94
|
+
SELECT 1
|
95
|
+
FROM json_each(tags)
|
96
|
+
WHERE json_each.value LIKE ?2 OR ?2 IS NULL
|
97
|
+
)
|
98
|
+
GROUP BY recent_articles.article_id
|
99
|
+
ORDER BY recent_articles.article_published_on
|
100
|
+
```
|
101
|
+
|
102
|
+
The result would look like this:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
[{"article_id"=>292,
|
106
|
+
"article_title"=>"Rails Versions 7.0.8.2, and 7.1.3.3 have been released!",
|
107
|
+
"article_published_on"=>"2024-05-17",
|
108
|
+
"article_url"=>"https://rubyonrails.org/2024/5/17/Rails-Versions-7-0-8-2-and-7-1-3-3-have-been-released",
|
109
|
+
"tags_str"=>"release:7x,release:revision"},
|
110
|
+
...
|
111
|
+
]
|
112
|
+
```
|
113
|
+
|
114
|
+
Even for this fairly trivial query, there's already quite some things 'encoded' that we might want to verify or capture in tests:
|
115
|
+
- only certain columns
|
116
|
+
- only published articles
|
117
|
+
- only articles _with_ tags
|
118
|
+
- only articles published after some date
|
119
|
+
- either provided or using the default
|
120
|
+
- articles are sorted in a certain order
|
121
|
+
- tags appear in a certain order and are formatted a certain way
|
122
|
+
|
123
|
+
Using the SQL-rewriting capabilities shown below, this library allows you to express these assertions in tests or verify them during development.
|
124
|
+
|
125
|
+
### Verify query results
|
126
|
+
|
127
|
+
> [!NOTE]
|
128
|
+
> 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`.
|
129
|
+
> 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.
|
130
|
+
|
131
|
+
Given the query above, you can get the result like so:
|
132
|
+
```ruby
|
133
|
+
AppQuery[:recent_articles].select_all.entries
|
134
|
+
# =>
|
135
|
+
[{"article_id"=>292,
|
136
|
+
"article_title"=>"Rails Versions 7.0.8.2, and 7.1.3.3 have been released!",
|
137
|
+
"article_published_on"=>"2024-05-17",
|
138
|
+
"article_url"=>"https://rubyonrails.org/2024/5/17/Rails-Versions-7-0-8-2-and-7-1-3-3-have-been-released",
|
139
|
+
"tags_str"=>"release:7x,release:revision"},
|
140
|
+
...
|
141
|
+
]
|
142
|
+
|
143
|
+
# we can provide a different cut off date via binds^1:
|
144
|
+
AppQuery[:recent_articles].select_all(binds: [1.month.ago]).entries
|
145
|
+
|
146
|
+
1) note that SQLite can deal with unbound parameters, i.e. when no binds are provided it assumes null for $1 and $2 (which our query can deal with).
|
147
|
+
For Postgres you would always need to provide 2 values, e.g. `binds: [nil, nil]`.
|
148
|
+
```
|
149
|
+
|
150
|
+
We can also dig deeper by query-ing the result, i.e. the CTE `_`:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
AppQuery[:recent_articles].select_one(select: "select count(*) as cnt from _")
|
154
|
+
# => {"cnt" => 13}
|
155
|
+
|
156
|
+
# For these kind of aggregate queries, we're only interested in the value:
|
157
|
+
AppQuery[:recent_articles].select_value(select: "select count(*) from _")
|
158
|
+
# => 13
|
159
|
+
```
|
160
|
+
|
161
|
+
Use `AppQuery#with_select` to get a new AppQuery-instance with the rewritten SQL:
|
162
|
+
```ruby
|
163
|
+
puts AppQuery[:recent_articles].with_select("select * from _")
|
164
|
+
```
|
165
|
+
|
166
|
+
|
167
|
+
### Verify CTE results
|
168
|
+
|
169
|
+
You can select from a CTE similarly:
|
170
|
+
```ruby
|
171
|
+
AppQuery[:recent_articles].select_all(select: "SELECT * FROM tags_by_article")
|
172
|
+
# => [{"article_id"=>1, "tags"=>"[\"release:pre\",\"release:patch\",\"release:1x\"]"},
|
173
|
+
...]
|
174
|
+
|
175
|
+
# NOTE how the tags are json strings. Casting allows us to turn these into proper arrays^1:
|
176
|
+
types = {"tags" => ActiveRecord::Type::Json.new}
|
177
|
+
AppQuery[:recent_articles].select_all(select: "SELECT * FROM tags_by_article", cast: types)
|
178
|
+
|
179
|
+
1) PostgreSQL, unlike SQLite, has json and array types. Just casting suffices:
|
180
|
+
AppQuery("select json_build_object('a', 1, 'b', true)").select_one(cast: true)
|
181
|
+
# => {"json_build_object"=>{"a"=>1, "b"=>true}}
|
182
|
+
```
|
183
|
+
|
184
|
+
Using the methods `(prepend|append|replace)_cte`, we can rewrite the query beyond just the select:
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
AppQuery[:recent_articles].replace_cte(<<~SQL).select_all.entries
|
188
|
+
settings(default_min_published_on) as (
|
189
|
+
values(datetime('now', '-12 months'))
|
190
|
+
)
|
191
|
+
SQL
|
192
|
+
```
|
193
|
+
|
194
|
+
You could even mock existing tables (using PostgreSQL):
|
195
|
+
```ruby
|
196
|
+
# using Ruby data:
|
197
|
+
sample_articles = [{id: 1, title: "Some title", published_on: 3.months.ago},
|
198
|
+
{id: 2, title: "Another title", published_on: 1.months.ago}]
|
199
|
+
# show the provided cutoff date works
|
200
|
+
AppQuery[:recent_articles].prepend_cte(<<-CTE).select_all(binds: [6.weeks.ago, nil, JSON[sample_articles]).entries
|
201
|
+
articles AS (
|
202
|
+
SELECT * from json_to_recordset($3) AS x(id int, title text, published_on timestamp)
|
203
|
+
)
|
204
|
+
CTE
|
205
|
+
```
|
206
|
+
|
207
|
+
Use `AppQuery#with_select` to get a new AppQuery-instance with the rewritten sql:
|
208
|
+
```ruby
|
209
|
+
puts AppQuery[:recent_articles].with_select("select * from some_cte")
|
210
|
+
```
|
211
|
+
|
212
|
+
### Spec
|
213
|
+
|
214
|
+
When generating a query `reports/weekly`, a spec-file like below is generated:
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
# spec/queries/reports/weekly_query_spec.rb
|
218
|
+
require "rails_helper"
|
219
|
+
|
220
|
+
RSpec.describe "AppQuery reports/weekly", type: :query, default_binds: [] do
|
221
|
+
describe "CTE articles" do
|
222
|
+
specify do
|
223
|
+
expect(described_query.select_all(select: "select * from :cte")).to \
|
224
|
+
include(a_hash_including("article_id" => 1))
|
225
|
+
|
226
|
+
# short version: query, cte and select are all implied from descriptions
|
227
|
+
expect(select_all).to include(a_hash_including("article_id" => 1))
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
```
|
232
|
+
|
233
|
+
There's some sugar:
|
234
|
+
- `described_query`
|
235
|
+
...just like `described_class` in regular class specs.
|
236
|
+
It's an instance of `AppQuery` based on the last word of the top-description (i.e. "reports/weekly" from "AppQuery reports/weekly").
|
237
|
+
- `:cte` placeholder
|
238
|
+
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").
|
239
|
+
- default_binds
|
240
|
+
The `binds`-value used when not explicitly provided.
|
241
|
+
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])`.
|
242
|
+
|
243
|
+
## 💎 API Doc 💎
|
244
|
+
|
245
|
+
### generic
|
246
|
+
|
247
|
+
<details>
|
248
|
+
<summary><code>AppQuery(sql) ⇒ AppQuery::Q</code></summary>
|
249
|
+
|
250
|
+
### Examples
|
251
|
+
|
252
|
+
```ruby
|
253
|
+
AppQuery("some sql")
|
254
|
+
```
|
255
|
+
</details>
|
256
|
+
|
257
|
+
### module AppQuery
|
258
|
+
|
259
|
+
<details>
|
260
|
+
<summary><code>AppQuery[query_name] ⇒ AppQuery::Q</code></summary>
|
261
|
+
|
262
|
+
### Examples
|
263
|
+
|
264
|
+
```ruby
|
265
|
+
AppQuery[:recent_articles]
|
266
|
+
AppQuery["export/articles"]
|
267
|
+
```
|
268
|
+
|
269
|
+
</details>
|
270
|
+
|
271
|
+
<details>
|
272
|
+
<summary><code>AppQuery.configure {|Configuration| ... } ⇒ void </code></summary>
|
273
|
+
|
274
|
+
Configure AppQuery.
|
275
|
+
|
276
|
+
### Examples
|
277
|
+
|
278
|
+
```ruby
|
279
|
+
AppQuery.configure do |cfg|
|
280
|
+
cfg.query_path = "db/queries" # default: "app/queries"
|
281
|
+
end
|
282
|
+
```
|
283
|
+
|
284
|
+
</details>
|
285
|
+
|
286
|
+
<details>
|
287
|
+
<summary><code>AppQuery.configuration ⇒ AppQuery::Configuration </code></summary>
|
288
|
+
|
289
|
+
Get configuration
|
290
|
+
|
291
|
+
### Examples
|
292
|
+
|
293
|
+
```ruby
|
294
|
+
AppQuery.configure do |cfg|
|
295
|
+
cfg.query_path = "db/queries" # default: "app/queries"
|
296
|
+
end
|
297
|
+
AppQuery.configuration
|
298
|
+
```
|
299
|
+
|
300
|
+
</details>
|
24
301
|
|
25
|
-
|
302
|
+
### class AppQuery::Q
|
303
|
+
|
304
|
+
Instantiate via `AppQuery(sql)` or `AppQuery[:query_file]`.
|
305
|
+
|
306
|
+
<details>
|
307
|
+
<summary><code>AppQuery::Q#cte_names ⇒ [Array< String >] </code></summary>
|
308
|
+
|
309
|
+
Returns names of CTEs in query.
|
310
|
+
|
311
|
+
### Examples
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
AppQuery("select * from articles").cte_names # => []
|
315
|
+
AppQuery("with foo as(select 1) select * from foo").cte_names # => ["foo"]
|
316
|
+
```
|
317
|
+
|
318
|
+
</details>
|
319
|
+
|
320
|
+
<details>
|
321
|
+
<summary><code>AppQuery::Q#recursive? ⇒ Boolean </code></summary>
|
322
|
+
|
323
|
+
Returns whether or not the WITH-clause is recursive or not.
|
324
|
+
|
325
|
+
### Examples
|
326
|
+
|
327
|
+
```ruby
|
328
|
+
AppQuery("select * from articles").recursive? # => false
|
329
|
+
AppQuery("with recursive foo as(select 1) select * from foo") # => true
|
330
|
+
```
|
331
|
+
|
332
|
+
</details>
|
333
|
+
|
334
|
+
<details>
|
335
|
+
<summary><code>AppQuery::Q#select ⇒ String </code></summary>
|
336
|
+
|
337
|
+
Returns select-part of the query. When using CTEs, this will be `<select>` in a query like `with foo as (select 1) <select>`.
|
338
|
+
|
339
|
+
### Examples
|
340
|
+
|
341
|
+
```ruby
|
342
|
+
AppQuery("select * from articles") # => "select * from articles"
|
343
|
+
AppQuery("with foo as(select 1) select * from foo") # => "select * from foo"
|
344
|
+
```
|
345
|
+
|
346
|
+
</details>
|
347
|
+
|
348
|
+
#### query execution
|
349
|
+
|
350
|
+
<details>
|
351
|
+
<summary><code>AppQuery::Q#select_all(select: nil, binds: [], cast: false) ⇒ AppQuery::Result</code></summary>
|
352
|
+
|
353
|
+
`select` replaces the existing select. The existing select is wrapped in a CTE named `_`.
|
354
|
+
`binds` array with values for any (positional) placeholder in the query.
|
355
|
+
`cast` boolean or `Hash` indicating whether or not (and how) to cast. E.g. `{"some_column" => ActiveRecord::Type::Date.new}`.
|
356
|
+
|
357
|
+
### Examples
|
358
|
+
|
359
|
+
```ruby
|
360
|
+
# SQLite
|
361
|
+
aq = AppQuery(<<~SQL)
|
362
|
+
with data(id, title) as (
|
363
|
+
values('1', 'Some title'),
|
364
|
+
('2', 'Another title')
|
365
|
+
)
|
366
|
+
select * from data
|
367
|
+
where id=?1 or ?1 is null
|
368
|
+
SQL
|
369
|
+
|
370
|
+
# selecting from the select
|
371
|
+
aq.select_all(select: "select * from _ where id > 1").entries #=> [{...}]
|
372
|
+
|
373
|
+
# selecting from a CTE
|
374
|
+
aq.select_all(select: "select id from data").entries
|
375
|
+
|
376
|
+
# casting
|
377
|
+
aq.select_all(select: "select id from data", cast: {"id" => ActiveRecord::Type::Integer.new})
|
378
|
+
|
379
|
+
# binds
|
380
|
+
aq.select_all(binds: ['2'])
|
381
|
+
```
|
382
|
+
|
383
|
+
</details>
|
384
|
+
|
385
|
+
<details>
|
386
|
+
<summary><code>AppQuery::Q#select_one(select: nil, binds: [], cast: false) ⇒ AppQuery::Result </code></summary>
|
387
|
+
|
388
|
+
First result from `AppQuery::Q#select_all`.
|
389
|
+
|
390
|
+
See examples from `AppQuery::Q#select_all`.
|
391
|
+
|
392
|
+
</details>
|
393
|
+
|
394
|
+
<details>
|
395
|
+
<summary><code>AppQuery::Q#select_value(select: nil, binds: [], cast: false) ⇒ AppQuery::Result </code></summary>
|
396
|
+
|
397
|
+
First value from `AppQuery::Q#select_one`. Typically for selects like `select count(*) ...`, `select min(article_published_on) ...`.
|
398
|
+
|
399
|
+
See examples from `AppQuery::Q#select_all`.
|
400
|
+
|
401
|
+
</details>
|
402
|
+
|
403
|
+
#### query rewriting
|
404
|
+
|
405
|
+
<details>
|
406
|
+
<summary><code>AppQuery::Q#with_select(sql) ⇒ AppQuery::Q</code></summary>
|
407
|
+
|
408
|
+
Returns new instance with provided select. The existing select is available via CTE `_`.
|
409
|
+
|
410
|
+
### Examples
|
411
|
+
|
412
|
+
```ruby
|
413
|
+
puts AppQuery("select 1").with_select("select 2")
|
414
|
+
WITH _ as (
|
415
|
+
select 1
|
416
|
+
)
|
417
|
+
select 2
|
418
|
+
```
|
419
|
+
|
420
|
+
</details>
|
421
|
+
|
422
|
+
<details>
|
423
|
+
<summary><code>AppQuery::Q#prepend_cte(sql) ⇒ AppQuery::Q</code></summary>
|
424
|
+
|
425
|
+
Returns new instance with provided CTE.
|
426
|
+
|
427
|
+
### Examples
|
428
|
+
|
429
|
+
```ruby
|
430
|
+
query.prepend_cte("foo as (values(1, 'Some article'))").cte_names # => ["foo", "existing_cte"]
|
431
|
+
```
|
432
|
+
|
433
|
+
</details>
|
434
|
+
|
435
|
+
<details>
|
436
|
+
<summary><code>AppQuery::Q#append_cte(sql) ⇒ AppQuery::Q</code></summary>
|
437
|
+
|
438
|
+
Returns new instance with provided CTE.
|
439
|
+
|
440
|
+
### Examples
|
441
|
+
|
442
|
+
```ruby
|
443
|
+
query.append_cte("foo as (values(1, 'Some article'))").cte_names # => ["existing_cte", "foo"]
|
444
|
+
```
|
445
|
+
|
446
|
+
</details>
|
447
|
+
|
448
|
+
<details>
|
449
|
+
<summary><code>AppQuery::Q#replace_cte(sql) ⇒ AppQuery::Q</code></summary>
|
450
|
+
|
451
|
+
Returns new instance with replaced CTE. Raises `ArgumentError` when CTE does not already exist.
|
452
|
+
|
453
|
+
### Examples
|
454
|
+
|
455
|
+
```ruby
|
456
|
+
query.replace_cte("recent_articles as (select values(1, 'Some article'))")
|
457
|
+
```
|
458
|
+
|
459
|
+
</details>
|
460
|
+
|
461
|
+
## Compatibility
|
462
|
+
|
463
|
+
- 💾 tested with **SQLite** and **PostgreSQL**
|
464
|
+
- 🚆 tested with Rails **v6.1**, **v7** and **v8.0**
|
465
|
+
- 💎 requires Ruby **>v3.1**
|
466
|
+
Goal is to support [maintained Ruby versions](https://www.ruby-lang.org/en/downloads/branches/).
|
26
467
|
|
27
468
|
## Development
|
28
469
|
|
29
|
-
After checking out the repo, run `bin/setup` to install dependencies.
|
470
|
+
After checking out the repo, run `bin/setup` to install dependencies. **Make sure to check it exits with status code 0.**
|
471
|
+
|
472
|
+
Using [direnv](https://direnv.net/) for env-vars recommended.
|
473
|
+
|
474
|
+
|
475
|
+
Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
476
|
|
31
477
|
To 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).
|
32
478
|
|
@@ -37,3 +483,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/eval/a
|
|
37
483
|
## License
|
38
484
|
|
39
485
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
486
|
+
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module AppQuery
|
2
|
+
module RSpec
|
3
|
+
module Helpers
|
4
|
+
def default_binds
|
5
|
+
self.class.default_binds
|
6
|
+
end
|
7
|
+
|
8
|
+
def expand_select(s)
|
9
|
+
s.gsub(":cte", cte_name)
|
10
|
+
end
|
11
|
+
|
12
|
+
def select_all(select: nil, binds: default_binds, **kws)
|
13
|
+
@query_result = described_query(select:).select_all(binds:, **kws)
|
14
|
+
end
|
15
|
+
|
16
|
+
def select_one(select: nil, binds: default_binds, **kws)
|
17
|
+
@query_result = described_query(select:).select_one(binds:, **kws)
|
18
|
+
end
|
19
|
+
|
20
|
+
def select_value(select: nil, binds: default_binds, **kws)
|
21
|
+
@query_result = described_query(select:).select_value(binds:, **kws)
|
22
|
+
end
|
23
|
+
|
24
|
+
def described_query(select: nil)
|
25
|
+
select ||= "SELECT * FROM :cte" if cte_name
|
26
|
+
select &&= expand_select(select) if cte_name
|
27
|
+
self.class.described_query.with_select(select)
|
28
|
+
end
|
29
|
+
|
30
|
+
def cte_name
|
31
|
+
self.class.cte_name
|
32
|
+
end
|
33
|
+
|
34
|
+
def query_name
|
35
|
+
self.class.query_name
|
36
|
+
end
|
37
|
+
|
38
|
+
def query_result
|
39
|
+
@query_result
|
40
|
+
end
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
def described_query
|
44
|
+
AppQuery[query_name]
|
45
|
+
end
|
46
|
+
|
47
|
+
def metadatas
|
48
|
+
scope = is_a?(Class) ? self : self.class
|
49
|
+
metahash = scope.metadata
|
50
|
+
result = []
|
51
|
+
loop do
|
52
|
+
result << metahash
|
53
|
+
metahash = metahash[:parent_example_group]
|
54
|
+
break unless metahash
|
55
|
+
end
|
56
|
+
result
|
57
|
+
end
|
58
|
+
|
59
|
+
def descriptions
|
60
|
+
metadatas.map { _1[:description] }
|
61
|
+
end
|
62
|
+
|
63
|
+
def query_name
|
64
|
+
descriptions.find { _1[/(app)?query\s/i] }&.then { _1.split.last }
|
65
|
+
end
|
66
|
+
|
67
|
+
def cte_name
|
68
|
+
descriptions.find { _1[/cte\s/i] }&.then { _1.split.last }
|
69
|
+
end
|
70
|
+
|
71
|
+
def default_binds
|
72
|
+
metadatas.find { _1[:default_binds] }&.[](:default_binds) || []
|
73
|
+
end
|
74
|
+
|
75
|
+
def included(klass)
|
76
|
+
super
|
77
|
+
# Inject classmethods into the group.
|
78
|
+
klass.extend(ClassMethods)
|
79
|
+
# If the describe block is aimed at string or resource/provider class
|
80
|
+
# then set the default subject to be the Chef run.
|
81
|
+
# if klass.described_class.nil? || klass.described_class.is_a?(Class) && (klass.described_class < Chef::Resource || klass.described_class < Chef::Provider)
|
82
|
+
# klass.subject { chef_run }
|
83
|
+
# end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
extend ClassMethods
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|