appquery 0.3.0 → 0.4.0.rc1
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 +20 -0
- data/Appraisals +14 -4
- data/README.md +35 -26
- data/lib/app_query/version.rb +1 -1
- data/lib/app_query.rb +192 -12
- data/rakelib/gem.rake +25 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 585ecafd973b1dd9fc8c944b93c64998a8b983438250cbd1ec81317d9e2362d4
|
|
4
|
+
data.tar.gz: 8197dc68853e7299266a0f8c2dd7fa102bbbf7e743680786bce4c7c2eedcd719
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37b5baf0f42dbad9ee1f356e0541b5677cb93a934d38ad851931142f79a041337f77cbbd4f56c1e880b4db9140460237afa8627eb0f6f9c73461fc1f6b3843cb
|
|
7
|
+
data.tar.gz: 3edc3e28e09a648a3d14634beab10cdde1be8d1b31c6c8163fd2f19844fab31a5ecf762ddcef9311f18d1356fe6e2c765f99dd2d08706284c687e94d21c36261
|
data/.irbrc
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
puts "Loading #{__FILE__}"
|
|
2
|
+
# put overrides/additions in '_irbrc'
|
|
3
|
+
|
|
4
|
+
IRB.conf[:HISTORY_FILE] = "#{ENV["PROJECT_ROOT"]}/tmp/.irb_history"
|
|
5
|
+
|
|
6
|
+
# Custom IRB prompt showing database adapter
|
|
7
|
+
db_indicator = begin
|
|
8
|
+
adapter = ActiveRecord::Base.connection.adapter_name.downcase
|
|
9
|
+
"\e[33m[#{adapter}]\e[0m "
|
|
10
|
+
rescue ActiveRecord::ConnectionNotEstablished, NameError
|
|
11
|
+
"\e[34m[no-db]\e[0m "
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
IRB.conf[:PROMPT][:APPQUERY] = {
|
|
15
|
+
PROMPT_I: "#{db_indicator}appquery> ",
|
|
16
|
+
PROMPT_S: "#{db_indicator}appquery%l ",
|
|
17
|
+
PROMPT_C: "#{db_indicator}appquery* ",
|
|
18
|
+
RETURN: "=> %s\n"
|
|
19
|
+
}
|
|
20
|
+
IRB.conf[:PROMPT_MODE] = :APPQUERY
|
data/Appraisals
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
appraise "rails-70" do
|
|
2
|
-
gem "rails", "~> 7.0"
|
|
2
|
+
gem "rails", "~> 7.0.0"
|
|
3
|
+
gem "sqlite3", "~> 1.4"
|
|
3
4
|
end
|
|
4
5
|
|
|
5
6
|
appraise "rails-71" do
|
|
6
|
-
gem "rails", "~> 7.1"
|
|
7
|
+
gem "rails", "~> 7.1.0"
|
|
7
8
|
end
|
|
8
9
|
|
|
9
10
|
appraise "rails-72" do
|
|
10
|
-
gem "rails", "~> 7.2"
|
|
11
|
+
gem "rails", "~> 7.2.0"
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
appraise "rails-80" do
|
|
14
|
-
gem "rails", "~> 8.0"
|
|
15
|
+
gem "rails", "~> 8.0.0"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
appraise "rails-81" do
|
|
19
|
+
gem "rails", "~> 8.1.0"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
appraise "rails-head" do
|
|
23
|
+
gem "activerecord", github: "rails/rails"
|
|
24
|
+
gem "rails", github: "rails/rails"
|
|
15
25
|
end
|
data/README.md
CHANGED
|
@@ -95,60 +95,65 @@ Testdriving can be easily done from the console. Either by cloning this reposito
|
|
|
95
95
|
```
|
|
96
96
|
</details>
|
|
97
97
|
|
|
98
|
-
The
|
|
98
|
+
The prompt indicates what adapter the example uses:
|
|
99
99
|
|
|
100
100
|
```ruby
|
|
101
101
|
# showing select_(all|one|value)
|
|
102
|
-
> AppQuery(%{select date('now') as today}).select_all.to_a
|
|
102
|
+
[postgresql]> AppQuery(%{select date('now') as today}).select_all.to_a
|
|
103
103
|
=> [{"today" => "2025-05-10"}]
|
|
104
|
-
> AppQuery(%{select date('now') as today}).select_one
|
|
104
|
+
[postgresql]> AppQuery(%{select date('now') as today}).select_one
|
|
105
105
|
=> {"today" => "2025-05-10"}
|
|
106
|
-
> AppQuery(%{select date('now') as today}).select_value
|
|
106
|
+
[postgresql]> AppQuery(%{select date('now') as today}).select_value
|
|
107
107
|
=> "2025-05-10"
|
|
108
108
|
|
|
109
109
|
# binds
|
|
110
110
|
# positional binds
|
|
111
|
-
> AppQuery(%{select now() - ($1)::interval as date}).select_value(binds: ['2 days'])
|
|
111
|
+
[postgresql]> AppQuery(%{select now() - ($1)::interval as date}).select_value(binds: ['2 days'])
|
|
112
112
|
# named binds
|
|
113
|
-
> AppQuery(%{select now() - (:interval)::interval as date}).select_value(binds: {interval: '2 days'})
|
|
113
|
+
[postgresql]> AppQuery(%{select now() - (:interval)::interval as date}).select_value(binds: {interval: '2 days'})
|
|
114
114
|
|
|
115
115
|
# casting
|
|
116
|
-
> AppQuery(%{select date('now') as today}).select_all(cast: true).to_a
|
|
116
|
+
[postgresql]> AppQuery(%{select date('now') as today}).select_all(cast: true).to_a
|
|
117
117
|
=> [{"today" => Sat, 10 May 2025}]
|
|
118
118
|
|
|
119
119
|
## SQLite doesn't have a notion of dates or timestamp's so casting won't do anything:
|
|
120
|
-
sqlite> AppQuery(%{select date('now') as today}).select_one(cast: true)
|
|
120
|
+
[sqlite]> AppQuery(%{select date('now') as today}).select_one(cast: true)
|
|
121
121
|
=> {"today" => "2025-05-12"}
|
|
122
122
|
## Providing per-column-casts fixes this:
|
|
123
123
|
casts = {"today" => ActiveRecord::Type::Date.new}
|
|
124
|
-
sqlite> AppQuery(%{select date('now') as today}).select_one(cast: casts)
|
|
124
|
+
[sqlite]> AppQuery(%{select date('now') as today}).select_one(cast: casts)
|
|
125
125
|
=> {"today" => Mon, 12 May 2025}
|
|
126
126
|
|
|
127
|
+
|
|
127
128
|
# rewriting queries (using CTEs)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
[postgresql]> articles = [
|
|
130
|
+
[1, "Using my new static site generator", 2.months.ago.to_date],
|
|
131
|
+
[2, "Let's learn SQL", 1.month.ago.to_date],
|
|
132
|
+
[3, "Another article", 2.weeks.ago.to_date]
|
|
133
|
+
]
|
|
134
|
+
[postgresql]> q = AppQuery(<<~SQL, cast: {"published_on" => ActiveRecord::Type::Date.new}).render(articles:)
|
|
135
|
+
WITH articles(id,title,published_on) AS (<%= values(articles) %>)
|
|
133
136
|
select * from articles order by id DESC
|
|
134
137
|
SQL
|
|
135
138
|
|
|
136
139
|
## query the articles-CTE
|
|
137
|
-
q.select_all(select: %{select * from articles where id < 2}).to_a
|
|
140
|
+
[postgresql]> q.select_all(select: %{select * from articles where id < 2}).to_a
|
|
138
141
|
|
|
139
142
|
## query the end-result (available as the CTE named '_')
|
|
140
|
-
q.select_one(select: %{select * from _ limit 1})
|
|
143
|
+
[postgresql]> q.select_one(select: %{select * from _ limit 1})
|
|
141
144
|
|
|
142
145
|
## ERB templating
|
|
143
146
|
# Extract a query from q that can be sorted dynamically:
|
|
144
|
-
q2 = q.with_select("select id,title,published_on::date from articles <%= order_by(order) %>")
|
|
145
|
-
q2.render(order: {"published_on::date": :desc, 'lower(title)': "asc"}).select_all.entries
|
|
147
|
+
[postgresql]> q2 = q.with_select("select id,title,published_on::date from articles <%= order_by(order) %>")
|
|
148
|
+
[postgresql]> q2.render(order: {"published_on::date": :desc, 'lower(title)': "asc"}).select_all.entries
|
|
149
|
+
|
|
146
150
|
# shows latest articles first, and titles sorted alphabetically
|
|
147
151
|
# for articles published on the same date.
|
|
148
152
|
# order_by raises when it's passed something that would result in just `ORDER BY`:
|
|
149
|
-
q2.render(order: {})
|
|
153
|
+
[postgresql]> q2.render(order: {})
|
|
154
|
+
|
|
150
155
|
# doing a select using a query that should be rendered, a `AppQuery::UnrenderedQueryError` will be raised:
|
|
151
|
-
q2.select_all.entries
|
|
156
|
+
[postgresql]> q2.select_all.entries
|
|
152
157
|
|
|
153
158
|
# NOTE you can use both `order` and `@order`: local variables like `order` are required,
|
|
154
159
|
# while instance variables like `@order` are optional.
|
|
@@ -249,7 +254,8 @@ AppQuery[:recent_articles].select_all.entries
|
|
|
249
254
|
# we can provide a different cut off date via binds^1:
|
|
250
255
|
AppQuery[:recent_articles].select_all(binds: [1.month.ago]).entries
|
|
251
256
|
|
|
252
|
-
1) note that SQLite can deal with unbound parameters, i.e. when no binds are provided it assumes null for
|
|
257
|
+
1) note that SQLite can deal with unbound parameters, i.e. when no binds are provided it assumes null for
|
|
258
|
+
$1 and $2 (which our query can deal with).
|
|
253
259
|
For Postgres you would always need to provide 2 values, e.g. `binds: [nil, nil]`.
|
|
254
260
|
```
|
|
255
261
|
|
|
@@ -567,8 +573,8 @@ query.replace_cte("recent_articles as (select values(1, 'Some article'))")
|
|
|
567
573
|
## Compatibility
|
|
568
574
|
|
|
569
575
|
- 💾 tested with **SQLite** and **PostgreSQL**
|
|
570
|
-
- 🚆 tested with Rails
|
|
571
|
-
- 💎 requires Ruby
|
|
576
|
+
- 🚆 tested with Rails v7.x and v8.x (might still work with v6.1, but is no longer included in the test-matrix)
|
|
577
|
+
- 💎 requires Ruby **>=v3.2**
|
|
572
578
|
Goal is to support [maintained Ruby versions](https://www.ruby-lang.org/en/downloads/branches/).
|
|
573
579
|
|
|
574
580
|
## Development
|
|
@@ -581,11 +587,14 @@ Using [mise](https://mise.jdx.dev/) for env-vars recommended.
|
|
|
581
587
|
|
|
582
588
|
The [console-script](./bin/console) is setup such that it's easy to connect with a database and experiment with the library:
|
|
583
589
|
```bash
|
|
584
|
-
$
|
|
585
|
-
$
|
|
590
|
+
$ bin/console sqlite3::memory:
|
|
591
|
+
$ bin/console postgres://localhost:5432/some_db
|
|
586
592
|
|
|
587
593
|
# more details
|
|
588
|
-
$
|
|
594
|
+
$ bin/console -h
|
|
595
|
+
|
|
596
|
+
# when needing an appraisal, use bin/run (this ensures signals are handled correctly):
|
|
597
|
+
$ bin/run rails_head console
|
|
589
598
|
```
|
|
590
599
|
|
|
591
600
|
### various
|
data/lib/app_query/version.rb
CHANGED
data/lib/app_query.rb
CHANGED
|
@@ -122,7 +122,16 @@ module AppQuery
|
|
|
122
122
|
|
|
123
123
|
def render(vars)
|
|
124
124
|
vars ||= {}
|
|
125
|
-
|
|
125
|
+
helper = render_helper(vars)
|
|
126
|
+
sql = to_erb.result(helper.get_binding)
|
|
127
|
+
collected = helper.collected_binds
|
|
128
|
+
|
|
129
|
+
with_sql(sql).tap do |q|
|
|
130
|
+
# Merge collected binds with existing binds (convert array to hash if needed)
|
|
131
|
+
existing = @binds.is_a?(Hash) ? @binds : {}
|
|
132
|
+
new_binds = existing.merge(collected)
|
|
133
|
+
q.instance_variable_set(:@binds, new_binds) if new_binds.any?
|
|
134
|
+
end
|
|
126
135
|
end
|
|
127
136
|
|
|
128
137
|
def to_erb
|
|
@@ -134,11 +143,84 @@ module AppQuery
|
|
|
134
143
|
Module.new do
|
|
135
144
|
extend self
|
|
136
145
|
|
|
146
|
+
@collected_binds = {}
|
|
147
|
+
@placeholder_counter = 0
|
|
148
|
+
|
|
137
149
|
vars.each do |k, v|
|
|
138
150
|
define_method(k) { v }
|
|
139
151
|
instance_variable_set(:"@#{k}", v)
|
|
140
152
|
end
|
|
141
153
|
|
|
154
|
+
def collect_bind(value)
|
|
155
|
+
@placeholder_counter += 1
|
|
156
|
+
key = :"b#{@placeholder_counter}"
|
|
157
|
+
@collected_binds[key] = value
|
|
158
|
+
":#{key}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
attr_reader :collected_binds
|
|
162
|
+
|
|
163
|
+
# Examples
|
|
164
|
+
# quote("Let's learn Ruby") #=> 'Let''s learn Ruby'
|
|
165
|
+
def quote(...)
|
|
166
|
+
ActiveRecord::Base.connection.quote(...)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Examples
|
|
170
|
+
# <%= bind(title) %> #=> :b1 (with title added to binds)
|
|
171
|
+
def bind(value)
|
|
172
|
+
collect_bind(value)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Examples
|
|
176
|
+
# <%= values([[1, "Some video"], [2, "Another video"]]) %>
|
|
177
|
+
# #=> VALUES (:b1, :b2), (:b3, :b4) with binds {b1: 1, b2: "Some video", ...}
|
|
178
|
+
#
|
|
179
|
+
# <%= values([{id: 1, title: "Some video"}]) %>
|
|
180
|
+
# #=> (id, title) VALUES (:b1, :b2) with binds {b1: 1, b2: "Some video"}
|
|
181
|
+
#
|
|
182
|
+
# <%= values([{title: "A"}, {title: "B", published_on: "2024-01-01"}]) %>
|
|
183
|
+
# #=> (title, published_on) VALUES (:b1, NULL), (:b2, :b3)
|
|
184
|
+
#
|
|
185
|
+
# Skip column names (e.g. for UNION ALL or CTEs):
|
|
186
|
+
# with articles as(
|
|
187
|
+
# <%= values([[1, "title"]], skip_columns: true) %>
|
|
188
|
+
# )
|
|
189
|
+
# #=> with articles as (VALUES (:b1, :b2))
|
|
190
|
+
#
|
|
191
|
+
# With block (mix bind() and quote()):
|
|
192
|
+
# <%= values(videos) { |v| [bind(v[:id]), quote(v[:title]), 'now()'] } %>
|
|
193
|
+
# #=> VALUES (:b1, 'Some title', now()), (:b2, 'Other title', now())
|
|
194
|
+
def values(coll, skip_columns: false, &block)
|
|
195
|
+
first = coll.first
|
|
196
|
+
|
|
197
|
+
# For hash collections, collect all unique keys
|
|
198
|
+
if first.is_a?(Hash) && !block
|
|
199
|
+
all_keys = coll.flat_map(&:keys).uniq
|
|
200
|
+
|
|
201
|
+
rows = coll.map do |row|
|
|
202
|
+
vals = all_keys.map { |k| row.key?(k) ? collect_bind(row[k]) : "NULL" }
|
|
203
|
+
"(#{vals.join(", ")})"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
columns = skip_columns ? "" : "(#{all_keys.join(", ")}) "
|
|
207
|
+
"#{columns}VALUES #{rows.join(",\n")}"
|
|
208
|
+
else
|
|
209
|
+
# Arrays or block - current behavior
|
|
210
|
+
rows = coll.map do |item|
|
|
211
|
+
vals = if block
|
|
212
|
+
block.call(item)
|
|
213
|
+
elsif item.is_a?(Array)
|
|
214
|
+
item.map { |v| collect_bind(v) }
|
|
215
|
+
else
|
|
216
|
+
[collect_bind(item)]
|
|
217
|
+
end
|
|
218
|
+
"(#{vals.join(", ")})"
|
|
219
|
+
end
|
|
220
|
+
"VALUES #{rows.join(",\n")}"
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
142
224
|
# Examples
|
|
143
225
|
# <%= order_by({year: :desc, month: :desc}) %>
|
|
144
226
|
# #=> ORDER BY year DESC, month DESC
|
|
@@ -164,22 +246,86 @@ module AppQuery
|
|
|
164
246
|
end
|
|
165
247
|
private :render_helper
|
|
166
248
|
|
|
167
|
-
|
|
168
|
-
|
|
249
|
+
# TODO: have aliases for common casts: select_all(cast: {"today" => :date})
|
|
250
|
+
def select_all(binds: nil, select: nil, cast: self.cast)
|
|
169
251
|
with_select(select).render({}).then do |aq|
|
|
252
|
+
# Support both positional (array) and named (hash) binds
|
|
253
|
+
if binds.is_a?(Array)
|
|
254
|
+
if @binds.is_a?(Hash) && @binds.any?
|
|
255
|
+
raise ArgumentError, "Cannot use positional binds (Array) when query has collected named binds from values()/bind() helpers. Use named binds (Hash) instead."
|
|
256
|
+
end
|
|
257
|
+
# Positional binds using $1, $2, etc.
|
|
258
|
+
ActiveRecord::Base.connection.select_all(aq.to_s, name, binds).then do |result|
|
|
259
|
+
Result.from_ar_result(result, cast)
|
|
260
|
+
end
|
|
261
|
+
else
|
|
262
|
+
# Named binds - merge collected binds with explicitly passed binds
|
|
263
|
+
merged_binds = (@binds.is_a?(Hash) ? @binds : {}).merge(binds || {})
|
|
264
|
+
if merged_binds.any?
|
|
265
|
+
sql = if ActiveRecord::VERSION::STRING.to_f >= 7.1
|
|
266
|
+
Arel.sql(aq.to_s, **merged_binds)
|
|
267
|
+
else
|
|
268
|
+
ActiveRecord::Base.sanitize_sql_array([aq.to_s, **merged_binds])
|
|
269
|
+
end
|
|
270
|
+
ActiveRecord::Base.connection.select_all(sql, name).then do |result|
|
|
271
|
+
Result.from_ar_result(result, cast)
|
|
272
|
+
end
|
|
273
|
+
else
|
|
274
|
+
ActiveRecord::Base.connection.select_all(aq.to_s, name).then do |result|
|
|
275
|
+
Result.from_ar_result(result, cast)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
rescue NameError => e
|
|
281
|
+
# Prevent any subclasses, e.g. NoMethodError
|
|
282
|
+
raise e unless e.instance_of?(NameError)
|
|
283
|
+
raise UnrenderedQueryError, "Query is ERB. Use #render before select-ing."
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def select_one(binds: nil, select: nil, cast: self.cast)
|
|
287
|
+
select_all(binds:, select:, cast:).first
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def select_value(binds: nil, select: nil, cast: self.cast)
|
|
291
|
+
select_one(binds:, select:, cast:)&.values&.first
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Examples
|
|
295
|
+
# AppQuery(<<~SQL).insert(binds: ["Let's learn SQL!"])
|
|
296
|
+
# INSERT INTO videos(title, created_at, updated_at) values($1, now(), now())
|
|
297
|
+
# SQL
|
|
298
|
+
#
|
|
299
|
+
# articles = [
|
|
300
|
+
# {title: "First article"}
|
|
301
|
+
# ].map { it.merge(created_at: Time.current)}
|
|
302
|
+
# AppQuery(<<~SQL).render(articles:)
|
|
303
|
+
# INSERT INTO articles(title, created_at) <%= values(articles) %>
|
|
304
|
+
# SQL
|
|
305
|
+
def insert(binds: [], returning: nil)
|
|
306
|
+
# ActiveRecord::Base.connection.insert(sql, name, _pk = nil, _id_value = nil, _sequence_name = nil, binds, returning: nil)
|
|
307
|
+
if returning && ActiveRecord::VERSION::STRING.to_f < 7.1
|
|
308
|
+
raise ArgumentError, "The 'returning' option requires Rails 7.1+. Current version: #{ActiveRecord::VERSION::STRING}"
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
binds = binds.presence || @binds
|
|
312
|
+
render({}).then do |aq|
|
|
170
313
|
if binds.is_a?(Hash)
|
|
171
314
|
sql = if ActiveRecord::VERSION::STRING.to_f >= 7.1
|
|
172
315
|
Arel.sql(aq.to_s, **binds)
|
|
173
316
|
else
|
|
174
317
|
ActiveRecord::Base.sanitize_sql_array([aq.to_s, **binds])
|
|
175
318
|
end
|
|
176
|
-
ActiveRecord::
|
|
177
|
-
|
|
319
|
+
if ActiveRecord::VERSION::STRING.to_f >= 7.1
|
|
320
|
+
ActiveRecord::Base.connection.insert(sql, name, returning:)
|
|
321
|
+
else
|
|
322
|
+
ActiveRecord::Base.connection.insert(sql, name)
|
|
178
323
|
end
|
|
324
|
+
elsif ActiveRecord::VERSION::STRING.to_f >= 7.1
|
|
325
|
+
# pk is the less flexible returning
|
|
326
|
+
ActiveRecord::Base.connection.insert(aq.to_s, name, _pk = nil, _id_value = nil, _sequence_name = nil, binds, returning:)
|
|
179
327
|
else
|
|
180
|
-
ActiveRecord::Base.connection.
|
|
181
|
-
Result.from_ar_result(result, cast)
|
|
182
|
-
end
|
|
328
|
+
ActiveRecord::Base.connection.insert(aq.to_s, name, _pk = nil, _id_value = nil, _sequence_name = nil, binds)
|
|
183
329
|
end
|
|
184
330
|
end
|
|
185
331
|
rescue NameError => e
|
|
@@ -188,12 +334,46 @@ module AppQuery
|
|
|
188
334
|
raise UnrenderedQueryError, "Query is ERB. Use #render before select-ing."
|
|
189
335
|
end
|
|
190
336
|
|
|
191
|
-
|
|
192
|
-
|
|
337
|
+
# Examples:
|
|
338
|
+
# AppQuery("UPDATE videos SET title = 'New' WHERE id = :id").update(binds: {id: 1})
|
|
339
|
+
def update(binds: [])
|
|
340
|
+
binds = binds.presence || @binds
|
|
341
|
+
render({}).then do |aq|
|
|
342
|
+
if binds.is_a?(Hash)
|
|
343
|
+
sql = if ActiveRecord::VERSION::STRING.to_f >= 7.1
|
|
344
|
+
Arel.sql(aq.to_s, **binds)
|
|
345
|
+
else
|
|
346
|
+
ActiveRecord::Base.sanitize_sql_array([aq.to_s, **binds])
|
|
347
|
+
end
|
|
348
|
+
ActiveRecord::Base.connection.update(sql, name)
|
|
349
|
+
else
|
|
350
|
+
ActiveRecord::Base.connection.update(aq.to_s, name, binds)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
rescue NameError => e
|
|
354
|
+
raise e unless e.instance_of?(NameError)
|
|
355
|
+
raise UnrenderedQueryError, "Query is ERB. Use #render before updating."
|
|
193
356
|
end
|
|
194
357
|
|
|
195
|
-
|
|
196
|
-
|
|
358
|
+
# Examples:
|
|
359
|
+
# AppQuery("DELETE FROM videos WHERE id = :id").delete(binds: {id: 1})
|
|
360
|
+
def delete(binds: [])
|
|
361
|
+
binds = binds.presence || @binds
|
|
362
|
+
render({}).then do |aq|
|
|
363
|
+
if binds.is_a?(Hash)
|
|
364
|
+
sql = if ActiveRecord::VERSION::STRING.to_f >= 7.1
|
|
365
|
+
Arel.sql(aq.to_s, **binds)
|
|
366
|
+
else
|
|
367
|
+
ActiveRecord::Base.sanitize_sql_array([aq.to_s, **binds])
|
|
368
|
+
end
|
|
369
|
+
ActiveRecord::Base.connection.delete(sql, name)
|
|
370
|
+
else
|
|
371
|
+
ActiveRecord::Base.connection.delete(aq.to_s, name, binds)
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
rescue NameError => e
|
|
375
|
+
raise e unless e.instance_of?(NameError)
|
|
376
|
+
raise UnrenderedQueryError, "Query is ERB. Use #render before deleting."
|
|
197
377
|
end
|
|
198
378
|
|
|
199
379
|
def tokens
|
data/rakelib/gem.rake
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
namespace :gem do
|
|
2
|
+
task "write_version", [:version] do |_task, args|
|
|
3
|
+
if args[:version]
|
|
4
|
+
version = args[:version].split("=").last
|
|
5
|
+
version_file = File.expand_path("../../lib/app_query/version.rb", __FILE__)
|
|
6
|
+
|
|
7
|
+
system(<<~CMD, exception: true)
|
|
8
|
+
ruby -pi -e 'gsub(/VERSION = ".*"/, %{VERSION = "#{version}"})' #{version_file}
|
|
9
|
+
CMD
|
|
10
|
+
Bundler.ui.confirm "Version #{version} written to #{version_file}."
|
|
11
|
+
else
|
|
12
|
+
Bundler.ui.warn "No version provided, keeping version.rb as is."
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc "Build [version]"
|
|
17
|
+
task "build", [:version] => %w[write_version] do
|
|
18
|
+
Rake::Task["build"].invoke
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
desc "Build and push [version] to rubygems"
|
|
22
|
+
task "release", [:version] => %w[build] do
|
|
23
|
+
Rake::Task["release:rubygem_push"].invoke
|
|
24
|
+
end
|
|
25
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: appquery
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0.rc1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gert Goet
|
|
@@ -40,6 +40,7 @@ executables: []
|
|
|
40
40
|
extensions: []
|
|
41
41
|
extra_rdoc_files: []
|
|
42
42
|
files:
|
|
43
|
+
- ".irbrc"
|
|
43
44
|
- ".rspec"
|
|
44
45
|
- ".standard.yml"
|
|
45
46
|
- Appraisals
|
|
@@ -61,6 +62,7 @@ files:
|
|
|
61
62
|
- lib/rails/generators/rspec/templates/query_spec.rb.tt
|
|
62
63
|
- mise.local.toml.example
|
|
63
64
|
- mise.toml
|
|
65
|
+
- rakelib/gem.rake
|
|
64
66
|
- sig/appquery.rbs
|
|
65
67
|
homepage: https://github.com/eval/appquery
|
|
66
68
|
licenses:
|
|
@@ -83,7 +85,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
83
85
|
- !ruby/object:Gem::Version
|
|
84
86
|
version: '0'
|
|
85
87
|
requirements: []
|
|
86
|
-
rubygems_version: 3.6.
|
|
88
|
+
rubygems_version: 3.6.9
|
|
87
89
|
specification_version: 4
|
|
88
90
|
summary: "raw SQL \U0001F966, cooked \U0001F372 or: make working with raw SQL queries
|
|
89
91
|
in Rails convenient by improving their introspection and testability."
|