appquery 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10872e5424d3f5853f5317384cd44927241c74ac6f1df8ba41f925796384816b
4
- data.tar.gz: eaa746cb1b2c7b2a48058ae1ceb1374245b6a197fab801411d0d602f4f58e7e2
3
+ metadata.gz: c63ba7b9e16d2f5b7071f8dd9203695f3c46b956d0302ff27a5cab0970cc598f
4
+ data.tar.gz: 71bebdc43197fce0e8917a3e6f4be0615bcb28215ef004b2496ce0554241c54d
5
5
  SHA512:
6
- metadata.gz: 8a11faf4af63d87413ccfd28e2224ad230e2e451cdf3518302c4faaf2df0594118f7d2ac99fa57d65a75feb52163780088038e9748ffeb2212b41cc4505ae0a3
7
- data.tar.gz: 7a6e4c0337a2fff23f845b04878994f162f7c49b5e9d981f1713cd5d0c490d8b0ea226871a92d754b3a4a5cc0e0b873217d650ec9d9f52ba79a0cfc257d24acc
6
+ metadata.gz: 4f372fb1b1e524e05a2cdf5338d1114f76e08df3b2ec2028e9ec42b515dcbfe4c4c6d9d318a367351e884cfc3ed8e0c670f215d2ccdb072753271fa248993eed
7
+ data.tar.gz: d91262c52aac1d7cb753eab122c2fbf7d352bd160f5112e3139cf1b5bda848e05456644d3e8e37b2c038ba0795fcfe707470c5bb903cc05e0d0d81a30725de80
data/.envrc CHANGED
@@ -1 +1,6 @@
1
- PATH_add bin
1
+ # direnv config
2
+ PATH_add bin
3
+
4
+ export APP_ROOT=$(pwd)
5
+
6
+ source_env_if_exists .envrc.private
@@ -0,0 +1,2 @@
1
+ # copy this to .envrc.private to use with direnv
2
+ export PG_DATABASE_URL=postgres://localhost:5432/some_db
data/.standard.yml CHANGED
@@ -1,3 +1,5 @@
1
1
  # For available configuration options, see:
2
2
  # https://github.com/standardrb/standard
3
- ruby_version: 3.0
3
+ ruby_version: 3.1
4
+ ignore:
5
+ - 'examples/**/*'
data/README.md CHANGED
@@ -1,32 +1,478 @@
1
- # Appquery
1
+ # AppQuery - raw SQL 🥦, cooked :stew:
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ [![Gem Version](https://badge.fury.io/rb/appquery.svg)](https://badge.fury.io/rb/appquery)
4
4
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/appquery`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- ## Installation
39
+ > [!IMPORTANT]
40
+ > **Status**: alpha. API might change. See the CHANGELOG for breaking changes when upgrading.
41
+ >
8
42
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
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 UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
48
+ bundle add appquery
15
49
  ```
16
50
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
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
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
63
+ rails g query recent_articles
21
64
  ```
22
65
 
23
- ## Usage
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
- TODO: Write usage instructions here
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. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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
@@ -0,0 +1,5 @@
1
+ require_relative "rspec/helpers"
2
+
3
+ RSpec.configure do |config|
4
+ config.include AppQuery::RSpec::Helpers, type: :query
5
+ end