appquery 0.1.0 → 0.3.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/.standard.yml +3 -1
- data/Appraisals +15 -0
- data/LICENSE.txt +1 -1
- data/README.md +575 -11
- data/lib/app_query/base.rb +45 -0
- 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 +343 -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/mise.local.toml.example +5 -0
- data/mise.toml +6 -0
- data/sig/appquery.rbs +1 -1
- metadata +45 -21
- data/.envrc +0 -1
data/lib/app_query.rb
CHANGED
@@ -1,8 +1,350 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "app_query/version"
|
4
|
+
require_relative "app_query/tokenizer"
|
5
|
+
require "active_record"
|
4
6
|
|
5
7
|
module AppQuery
|
6
8
|
class Error < StandardError; end
|
7
|
-
|
9
|
+
|
10
|
+
class UnrenderedQueryError < StandardError; end
|
11
|
+
|
12
|
+
Configuration = Struct.new(:query_path)
|
13
|
+
|
14
|
+
def self.configuration
|
15
|
+
@configuration ||= AppQuery::Configuration.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.configure
|
19
|
+
yield configuration if block_given?
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.reset_configuration!
|
23
|
+
configure do |config|
|
24
|
+
config.query_path = "app/queries"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
reset_configuration!
|
28
|
+
|
29
|
+
# Examples:
|
30
|
+
# AppQuery[:invoices] # looks for invoices.sql
|
31
|
+
# AppQuery["reports/weekly"]
|
32
|
+
# AppQuery["invoices.sql.erb"]
|
33
|
+
def self.[](query_name, **opts)
|
34
|
+
filename = File.extname(query_name.to_s).empty? ? "#{query_name}.sql" : query_name.to_s
|
35
|
+
full_path = (Pathname.new(configuration.query_path) / filename).expand_path
|
36
|
+
Q.new(full_path.read, name: "AppQuery #{query_name}", filename: full_path.to_s, **opts)
|
37
|
+
end
|
38
|
+
|
39
|
+
class Result < ActiveRecord::Result
|
40
|
+
attr_accessor :cast
|
41
|
+
alias_method :cast?, :cast
|
42
|
+
|
43
|
+
def initialize(columns, rows, overrides = nil, cast: false)
|
44
|
+
super(columns, rows, overrides)
|
45
|
+
@cast = cast
|
46
|
+
# Rails v6.1: prevent mutate on frozen object on #first
|
47
|
+
@hash_rows = [] if columns.empty?
|
48
|
+
end
|
49
|
+
|
50
|
+
def column(name = nil)
|
51
|
+
return [] if empty?
|
52
|
+
unless name.nil? || includes_column?(name)
|
53
|
+
raise ArgumentError, "Unknown column #{name.inspect}. Should be one of #{columns.inspect}."
|
54
|
+
end
|
55
|
+
ix = name.nil? ? 0 : columns.index(name)
|
56
|
+
rows.map { _1[ix] }
|
57
|
+
end
|
58
|
+
|
59
|
+
def size
|
60
|
+
count
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.from_ar_result(r, cast = nil)
|
64
|
+
if r.empty?
|
65
|
+
EMPTY
|
66
|
+
else
|
67
|
+
cast &&= case cast
|
68
|
+
when Array
|
69
|
+
r.columns.zip(cast).to_h
|
70
|
+
when Hash
|
71
|
+
cast
|
72
|
+
else
|
73
|
+
{}
|
74
|
+
end
|
75
|
+
if !cast || (cast.empty? && r.column_types.empty?)
|
76
|
+
# nothing to cast
|
77
|
+
new(r.columns, r.rows, r.column_types)
|
78
|
+
else
|
79
|
+
overrides = (r.column_types || {}).merge(cast)
|
80
|
+
rows = r.cast_values(overrides)
|
81
|
+
# One column is special :( ;(
|
82
|
+
# > ActiveRecord::Base.connection.select_all("select array[1,2]").rows
|
83
|
+
# => [["{1,2}"]]
|
84
|
+
# > ActiveRecord::Base.connection.select_all("select array[1,2]").cast_values
|
85
|
+
# => [[1, 2]]
|
86
|
+
rows = rows.zip if r.columns.one?
|
87
|
+
new(r.columns, rows, overrides, cast: true)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
empty_array = [].freeze
|
93
|
+
EMPTY_HASH = {}.freeze
|
94
|
+
private_constant :EMPTY_HASH
|
95
|
+
|
96
|
+
EMPTY = new(empty_array, empty_array, EMPTY_HASH).freeze
|
97
|
+
private_constant :EMPTY
|
98
|
+
end
|
99
|
+
|
100
|
+
class Q
|
101
|
+
attr_reader :name, :sql, :binds, :cast
|
102
|
+
|
103
|
+
def initialize(sql, name: nil, filename: nil, binds: [], cast: true)
|
104
|
+
@sql = sql
|
105
|
+
@name = name
|
106
|
+
@filename = filename
|
107
|
+
@binds = binds
|
108
|
+
@cast = cast
|
109
|
+
end
|
110
|
+
|
111
|
+
def deep_dup
|
112
|
+
super.send(:reset!)
|
113
|
+
end
|
114
|
+
|
115
|
+
def reset!
|
116
|
+
(instance_variables - %i[@sql @filename @name @binds @cast]).each do
|
117
|
+
instance_variable_set(_1, nil)
|
118
|
+
end
|
119
|
+
self
|
120
|
+
end
|
121
|
+
private :reset!
|
122
|
+
|
123
|
+
def render(vars)
|
124
|
+
vars ||= {}
|
125
|
+
with_sql(to_erb.result(render_helper(vars).get_binding))
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_erb
|
129
|
+
ERB.new(sql, trim_mode: "-").tap { _1.location = [@filename, 0] if @filename }
|
130
|
+
end
|
131
|
+
private :to_erb
|
132
|
+
|
133
|
+
def render_helper(vars)
|
134
|
+
Module.new do
|
135
|
+
extend self
|
136
|
+
|
137
|
+
vars.each do |k, v|
|
138
|
+
define_method(k) { v }
|
139
|
+
instance_variable_set(:"@#{k}", v)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Examples
|
143
|
+
# <%= order_by({year: :desc, month: :desc}) %>
|
144
|
+
# #=> ORDER BY year DESC, month DESC
|
145
|
+
#
|
146
|
+
# Using variable:
|
147
|
+
# <%= order_by(ordering) %>
|
148
|
+
# NOTE Raises when ordering not provided or when blank.
|
149
|
+
#
|
150
|
+
# Make it optional:
|
151
|
+
# <%= @ordering.presence && order_by(ordering) %>
|
152
|
+
#
|
153
|
+
def order_by(hash)
|
154
|
+
raise ArgumentError, "Provide columns to sort by, e.g. order_by(id: :asc) (got #{hash.inspect})." unless hash.present?
|
155
|
+
"ORDER BY " + hash.map do |k, v|
|
156
|
+
v.nil? ? k : [k, v.upcase].join(" ")
|
157
|
+
end.join(", ")
|
158
|
+
end
|
159
|
+
|
160
|
+
def get_binding
|
161
|
+
binding
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
private :render_helper
|
166
|
+
|
167
|
+
def select_all(binds: [], select: nil, cast: self.cast)
|
168
|
+
binds = binds.presence || @binds
|
169
|
+
with_select(select).render({}).then do |aq|
|
170
|
+
if binds.is_a?(Hash)
|
171
|
+
sql = if ActiveRecord::VERSION::STRING.to_f >= 7.1
|
172
|
+
Arel.sql(aq.to_s, **binds)
|
173
|
+
else
|
174
|
+
ActiveRecord::Base.sanitize_sql_array([aq.to_s, **binds])
|
175
|
+
end
|
176
|
+
ActiveRecord::Base.connection.select_all(sql, name).then do |result|
|
177
|
+
Result.from_ar_result(result, cast)
|
178
|
+
end
|
179
|
+
else
|
180
|
+
ActiveRecord::Base.connection.select_all(aq.to_s, name, binds).then do |result|
|
181
|
+
Result.from_ar_result(result, cast)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
rescue NameError => e
|
186
|
+
# Prevent any subclasses, e.g. NoMethodError
|
187
|
+
raise e unless e.instance_of?(NameError)
|
188
|
+
raise UnrenderedQueryError, "Query is ERB. Use #render before select-ing."
|
189
|
+
end
|
190
|
+
|
191
|
+
def select_one(binds: [], select: nil, cast: self.cast)
|
192
|
+
select_all(binds:, select:, cast:).first
|
193
|
+
end
|
194
|
+
|
195
|
+
def select_value(binds: [], select: nil, cast: self.cast)
|
196
|
+
select_one(binds:, select:, cast:)&.values&.first
|
197
|
+
end
|
198
|
+
|
199
|
+
def tokens
|
200
|
+
@tokens ||= tokenizer.run
|
201
|
+
end
|
202
|
+
|
203
|
+
def tokenizer
|
204
|
+
@tokenizer ||= Tokenizer.new(to_s)
|
205
|
+
end
|
206
|
+
|
207
|
+
def cte_names
|
208
|
+
tokens.filter { _1[:t] == "CTE_IDENTIFIER" }.map { _1[:v] }
|
209
|
+
end
|
210
|
+
|
211
|
+
def with_binds(binds)
|
212
|
+
deep_dup.tap do
|
213
|
+
_1.instance_variable_set(:@binds, binds)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def with_cast(cast)
|
218
|
+
deep_dup.tap do
|
219
|
+
_1.instance_variable_set(:@cast, cast)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def with_sql(sql)
|
224
|
+
deep_dup.tap do
|
225
|
+
_1.instance_variable_set(:@sql, sql)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def with_select(sql)
|
230
|
+
return self if sql.nil?
|
231
|
+
if cte_names.include?("_")
|
232
|
+
with_sql(tokens.each_with_object([]) do |token, acc|
|
233
|
+
v = (token[:t] == "SELECT") ? sql : token[:v]
|
234
|
+
acc << v
|
235
|
+
end.join)
|
236
|
+
else
|
237
|
+
append_cte("_ as (\n #{select}\n)").with_select(sql)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def select
|
242
|
+
tokens.find { _1[:t] == "SELECT" }&.[](:v)
|
243
|
+
end
|
244
|
+
|
245
|
+
def recursive?
|
246
|
+
!!tokens.find { _1[:t] == "RECURSIVE" }
|
247
|
+
end
|
248
|
+
|
249
|
+
# example:
|
250
|
+
# AppQuery("select 1").prepend_cte("foo as(select 1)")
|
251
|
+
def prepend_cte(cte)
|
252
|
+
# early raise when cte is not valid sql
|
253
|
+
to_append = Tokenizer.tokenize(cte, state: :lex_prepend_cte).then do |tokens|
|
254
|
+
recursive? ? tokens.reject { _1[:t] == "RECURSIVE" } : tokens
|
255
|
+
end
|
256
|
+
|
257
|
+
if cte_names.none?
|
258
|
+
with_sql("WITH #{cte}\n#{self}")
|
259
|
+
else
|
260
|
+
split_at_type = recursive? ? "RECURSIVE" : "WITH"
|
261
|
+
with_sql(tokens.map do |token|
|
262
|
+
if token[:t] == split_at_type
|
263
|
+
token[:v] + to_append.map { _1[:v] }.join
|
264
|
+
else
|
265
|
+
token[:v]
|
266
|
+
end
|
267
|
+
end.join)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# example:
|
272
|
+
# AppQuery("select 1").append_cte("foo as(select 1)")
|
273
|
+
def append_cte(cte)
|
274
|
+
# early raise when cte is not valid sql
|
275
|
+
add_recursive, to_append = Tokenizer.tokenize(cte, state: :lex_append_cte).then do |tokens|
|
276
|
+
[!recursive? && tokens.find { _1[:t] == "RECURSIVE" },
|
277
|
+
tokens.reject { _1[:t] == "RECURSIVE" }]
|
278
|
+
end
|
279
|
+
|
280
|
+
if cte_names.none?
|
281
|
+
with_sql("WITH #{cte}\n#{self}")
|
282
|
+
else
|
283
|
+
nof_ctes = cte_names.size
|
284
|
+
|
285
|
+
with_sql(tokens.map do |token|
|
286
|
+
nof_ctes -= 1 if token[:t] == "CTE_SELECT"
|
287
|
+
|
288
|
+
if nof_ctes.zero?
|
289
|
+
nof_ctes -= 1
|
290
|
+
token[:v] + to_append.map { _1[:v] }.join
|
291
|
+
elsif token[:t] == "WITH" && add_recursive
|
292
|
+
token[:v] + add_recursive[:v]
|
293
|
+
else
|
294
|
+
token[:v]
|
295
|
+
end
|
296
|
+
end.join)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# Replaces an existing cte.
|
301
|
+
# Raises `ArgumentError` when cte does not exist.
|
302
|
+
def replace_cte(cte)
|
303
|
+
add_recursive, to_append = Tokenizer.tokenize(cte, state: :lex_recursive_cte).then do |tokens|
|
304
|
+
[!recursive? && tokens.find { _1[:t] == "RECURSIVE" },
|
305
|
+
tokens.reject { _1[:t] == "RECURSIVE" }]
|
306
|
+
end
|
307
|
+
|
308
|
+
cte_name = to_append.find { _1[:t] == "CTE_IDENTIFIER" }&.[](:v)
|
309
|
+
unless cte_names.include?(cte_name)
|
310
|
+
raise ArgumentError, "Unknown cte #{cte_name.inspect}. Options: #{cte_names}."
|
311
|
+
end
|
312
|
+
cte_ix = cte_names.index(cte_name)
|
313
|
+
|
314
|
+
return self unless cte_ix
|
315
|
+
|
316
|
+
cte_found = false
|
317
|
+
|
318
|
+
with_sql(tokens.map do |token|
|
319
|
+
if cte_found ||= token[:t] == "CTE_IDENTIFIER" && token[:v] == cte_name
|
320
|
+
unless (cte_found = (token[:t] != "CTE_SELECT"))
|
321
|
+
next to_append.map { _1[:v] }.join
|
322
|
+
end
|
323
|
+
|
324
|
+
next
|
325
|
+
elsif token[:t] == "WITH" && add_recursive
|
326
|
+
token[:v] + add_recursive[:v]
|
327
|
+
else
|
328
|
+
token[:v]
|
329
|
+
end
|
330
|
+
end.join)
|
331
|
+
end
|
332
|
+
|
333
|
+
def to_s
|
334
|
+
@sql
|
335
|
+
end
|
336
|
+
end
|
8
337
|
end
|
338
|
+
|
339
|
+
def AppQuery(...)
|
340
|
+
AppQuery::Q.new(...)
|
341
|
+
end
|
342
|
+
|
343
|
+
begin
|
344
|
+
require "rspec"
|
345
|
+
rescue LoadError
|
346
|
+
end
|
347
|
+
|
348
|
+
require_relative "app_query/rspec" if Object.const_defined? :RSpec
|
349
|
+
|
350
|
+
require "app_query/base" if defined?(ActiveRecord::Base)
|
data/lib/appquery.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "app_query"
|
@@ -0,0 +1,10 @@
|
|
1
|
+
Description:
|
2
|
+
Generates a new query-file and invokes your template
|
3
|
+
engine and test framework generators.
|
4
|
+
|
5
|
+
Example:
|
6
|
+
`bin/rails generate query reports/weekly`
|
7
|
+
|
8
|
+
creates an SQL file and test:
|
9
|
+
Query: app/queries/reports/weekly.sql
|
10
|
+
Spec: spec/queries/reports/weekly_query_spec.rb
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Rails
|
2
|
+
module Generators
|
3
|
+
class QueryGenerator < NamedBase
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
5
|
+
|
6
|
+
def create_query_file
|
7
|
+
template "query.sql",
|
8
|
+
File.join(AppQuery.configuration.query_path, class_path, "#{file_name}.sql")
|
9
|
+
|
10
|
+
# in_root do
|
11
|
+
# if behavior == :invoke && !File.exist?(application_mailbox_file_name)
|
12
|
+
# template "application_mailbox.rb", application_mailbox_file_name
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
end
|
16
|
+
|
17
|
+
hook_for :test_framework
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
-- Instantiate this query with AppQuery[<%= (class_path << file_name).join("/").inspect %>]
|
2
|
+
|
3
|
+
WITH
|
4
|
+
articles(article_id, article_title) AS (
|
5
|
+
VALUES (1, 'Some title'),
|
6
|
+
(2, 'Another article')
|
7
|
+
),
|
8
|
+
authors(author_id, author_name) AS (
|
9
|
+
VALUES (1, 'Some Author'),
|
10
|
+
(2, 'Another Author')
|
11
|
+
)
|
12
|
+
|
13
|
+
SELECT *
|
14
|
+
FROM artciles
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Rspec
|
2
|
+
module Generators
|
3
|
+
class QueryGenerator < ::Rails::Generators::NamedBase
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
5
|
+
|
6
|
+
def create_test_file
|
7
|
+
template "query_spec.rb",
|
8
|
+
File.join("spec/queries", class_path, "#{file_name}_query_spec.rb")
|
9
|
+
end
|
10
|
+
|
11
|
+
hide!
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def query_path
|
16
|
+
AppQuery.configuration.query_path
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails_helper"
|
4
|
+
|
5
|
+
RSpec.describe "AppQuery <%= (class_path << file_name).join("/") %>", type: :query, default_binds: nil do
|
6
|
+
describe "CTE articles" do
|
7
|
+
specify do
|
8
|
+
expect(select_all(select: "select * from :cte").cast_entries).to \
|
9
|
+
include(a_hash_including("article_id" => 1))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/mise.toml
ADDED
data/sig/appquery.rbs
CHANGED
metadata
CHANGED
@@ -1,40 +1,66 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: appquery
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gert Goet
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: appraisal
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
type: :development
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0'
|
26
|
+
description: "Improving introspection and testability of raw SQL queries in Rails\nThis
|
27
|
+
gem improves introspection and testability of raw SQL queries in Rails by:\n- ...providing
|
28
|
+
a separate query-folder and easy instantiation \n A query like `AppQuery[:some_query]`
|
29
|
+
is read from app/queries/some_query.sql.\n\n- ...providing options for rewriting
|
30
|
+
a query:\n\n Query a CTE by replacing the select:\n query.select_all(select: \"select
|
31
|
+
* from some_cte\").entries\n\n ...similarly, query the end result (i.e. CTE `_`):\n
|
32
|
+
\ query.select_all(select: \"select count(*) from _\").entries\n\n- ...providing
|
33
|
+
(custom) casting: \n AppQuery(\"select array[1,2]\").select_value(cast: true)\n\n
|
34
|
+
\ custom deserializers:\n AppQuery(\"select '1' id\").select_all(cast: {\"id\"
|
35
|
+
=> ActiveRecord::Type::Integer.new}).entries\n\n- ...providing spec-helpers and
|
36
|
+
generators\n"
|
23
37
|
email:
|
24
38
|
- gert@thinkcreate.dk
|
25
39
|
executables: []
|
26
40
|
extensions: []
|
27
41
|
extra_rdoc_files: []
|
28
42
|
files:
|
29
|
-
- ".envrc"
|
30
43
|
- ".rspec"
|
31
44
|
- ".standard.yml"
|
45
|
+
- Appraisals
|
32
46
|
- CHANGELOG.md
|
33
47
|
- LICENSE.txt
|
34
48
|
- README.md
|
35
49
|
- Rakefile
|
36
50
|
- lib/app_query.rb
|
51
|
+
- lib/app_query/base.rb
|
52
|
+
- lib/app_query/rspec.rb
|
53
|
+
- lib/app_query/rspec/helpers.rb
|
54
|
+
- lib/app_query/tokenizer.rb
|
37
55
|
- lib/app_query/version.rb
|
56
|
+
- lib/appquery.rb
|
57
|
+
- lib/rails/generators/query/USAGE
|
58
|
+
- lib/rails/generators/query/query_generator.rb
|
59
|
+
- lib/rails/generators/query/templates/query.sql.tt
|
60
|
+
- lib/rails/generators/rspec/query_generator.rb
|
61
|
+
- lib/rails/generators/rspec/templates/query_spec.rb.tt
|
62
|
+
- mise.local.toml.example
|
63
|
+
- mise.toml
|
38
64
|
- sig/appquery.rbs
|
39
65
|
homepage: https://github.com/eval/appquery
|
40
66
|
licenses:
|
@@ -43,7 +69,6 @@ metadata:
|
|
43
69
|
homepage_uri: https://github.com/eval/appquery
|
44
70
|
source_code_uri: https://github.com/eval/appquery
|
45
71
|
changelog_uri: https://github.com/eval/gem-try/blob/main/CHANGELOG.md
|
46
|
-
post_install_message:
|
47
72
|
rdoc_options: []
|
48
73
|
require_paths:
|
49
74
|
- lib
|
@@ -51,16 +76,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
76
|
requirements:
|
52
77
|
- - ">="
|
53
78
|
- !ruby/object:Gem::Version
|
54
|
-
version: 3.
|
79
|
+
version: 3.2.0
|
55
80
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
81
|
requirements:
|
57
82
|
- - ">="
|
58
83
|
- !ruby/object:Gem::Version
|
59
84
|
version: '0'
|
60
85
|
requirements: []
|
61
|
-
rubygems_version: 3.
|
62
|
-
signing_key:
|
86
|
+
rubygems_version: 3.6.7
|
63
87
|
specification_version: 4
|
64
|
-
summary:
|
65
|
-
and testability.
|
88
|
+
summary: "raw SQL \U0001F966, cooked \U0001F372 or: make working with raw SQL queries
|
89
|
+
in Rails convenient by improving their introspection and testability."
|
66
90
|
test_files: []
|
data/.envrc
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
PATH_add bin
|