wise_gopher 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7568e5308f8025d5b1f9d9cf2a869a6560b6a0377aed7ca3a79c063c5abe76ec
4
- data.tar.gz: cd076d93f65ff85e50c4279e67604b7ba4caa6811390bdc1bbdba48197d8e50c
3
+ metadata.gz: dc88a6090305daa53b83df1cd298531d6209dbb74cce4aa2c1492a2c3987c168
4
+ data.tar.gz: 874dc19cf652522bdb9ebf89e45543c5ce912dd8a636d6dd01bc7ab7e78e10ec
5
5
  SHA512:
6
- metadata.gz: 7a0d433f7e6c9dbabde1505caaa3b9a0c9318462969458d873280e0bd567c28724172fa1779f1b3163ca7334c8be2b189af9691c27654f1a26f9fc9f77e5ad57
7
- data.tar.gz: 1517fed4b04a5c50640d8824fb9e97827905163c26155e5841dc733ef3349d3f5d27de31f8168f05bb2c9442782075cd71968edb8a42783fed61787dc6154625
6
+ metadata.gz: 4b0f65b90b90414aa03e572e33e0d3ac360f886c61afd19dc3b026b9ff0e76c9fd7040b77cf8c1b1bf89dcddd665d36e1125ff5ec27ff21c73d232b42dfeab09
7
+ data.tar.gz: ed4ceb261617680856ad9896b68b1ac0711ae911edf264d3d14fde7a5b7405b2d6843cd03333be036927fe762e45b7054e023d6f53e353cd19e117f05e451db7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2021-08-04
4
+
5
+ - Add raw_params to `WiseGopher::Base` to insert raw sql in query before handling other params
6
+
3
7
  ## [0.1.0] - 2021-06-22
4
8
 
5
9
  - Initial release
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- wise_gopher (0.1.0)
4
+ wise_gopher (0.2.0)
5
5
  activerecord (>= 5, < 7)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -50,7 +50,7 @@ class PopularArticle < WiseGopher::Base
50
50
  INNER JOIN ratings ON ratings.article_id = articles.id
51
51
  WHERE author_username = {{ username }}
52
52
  GROUP BY articles.id
53
- HAVING average_rating > {{ mininum_rating }}
53
+ HAVING AVG(ratings.stars) > {{ mininum_rating }}
54
54
  ORDER BY averating_rating
55
55
  SQL
56
56
 
@@ -75,14 +75,14 @@ Which you would use this way:
75
75
  result = PopularArticle.execute_with(minimum_rating: 3, username: "PageHey ")
76
76
  # => [#<PopularArticle::Row:0x0000560c37e9de48 @title="My first gem is out!", @average_rating=3.5 ...>, ...]
77
77
  puts result.first
78
- # => Article 'My first gem is out!' by PageHey is rated 3.5/5.
78
+ # => Article 'My first gem is out!' by PageHey is rated 3.50/5.
79
79
  result.first.class
80
80
  # => PopularArticle::Row
81
81
  ```
82
82
 
83
83
  ------
84
84
 
85
- So, basically what you need to do is make your class inherits from `WiseGopher::Base` and provide your SQL with `.query`. You can then declare what columns will be present in result with `.column` in a block given to `row`.
85
+ So, basically what you need to do is make your class inherits from `WiseGopher::Base` and provide your SQL with `query`. You can then declare what columns will be present in result with `column` in a block given to `row`.
86
86
 
87
87
 
88
88
  If your query doesn't need any parameter like this one:
@@ -95,7 +95,7 @@ class PopularArticle < WiseGopher::Base
95
95
  end
96
96
  end
97
97
  ```
98
- You can simply get result with `.execute`:
98
+ You can simply get result with `execute`:
99
99
  ```ruby
100
100
  PopularArticle.execute
101
101
  ```
@@ -116,28 +116,28 @@ class PopularArticle < WiseGopher::Base
116
116
  end
117
117
  end
118
118
  ```
119
- You should declare the params with `.param` so you can pass the parameters as a hash to `.execute_with`:
119
+ You should declare the params with `param` so you can pass the parameters as a hash to `execute_with`:
120
120
  ```ruby
121
121
  PopularArticle.execute_with(author_name: "PageHey", published_after: Date.today - 1.month)
122
122
  ```
123
123
 
124
- If any parameter is missing or if you call `.execute` for a class that needs some, it will raise `WiseGopher::ArgumentError`.
124
+ If any parameter is missing or if you call `execute` for a class that needs some, it will raise `WiseGopher::ArgumentError`.
125
125
 
126
126
  Before query execution, the placeholders will be replaced with the standard `?` placeholder or with the `$1`, `$2` ... numbered placeholders for PostgreSQL database.
127
127
 
128
- To declare the column in result, you should use `.row` and pass it a block. Calling this method will create a `Row` class nested into your query class. The block will be then executed in `Row` class context. In this context you can use `.column` but also define method, include module, basicaly write any code you would find in a class delacration.
128
+ To declare the column in result, you should use `row` and pass it a block. Calling this method will create a `Row` class nested into your query class. The block will be then executed in `Row` class context. In this context you can use `column` but also define method, include module, basicaly write any code you would find in a class delacration.
129
129
 
130
- The goal of this syntax is to gather in the same file the input and output logic of the query while keeping dedicated classes for each logic.
131
- You can provide a custom class to `.row` if you prefer. If you still pass the block to the method, the `WiseGopher::Row` module will be included in the class before evaluating it, so you can have this syntax:
130
+ The goal of this syntax is to gather in the same file the inputs and outputs of the query while keeping dedicated classes for each subject.
131
+ You can provide a custom class to `row` if you prefer. If you still pass the block to the method, the `WiseGopher::Row` module will be included in the class before evaluating it, so you can have this syntax:
132
132
  ```ruby
133
- _/my_custom_row.rb_
133
+ # /my_custom_row.rb
134
134
  class MyCustomRow
135
135
  def some_custom_method
136
136
  # [...]
137
137
  end
138
138
  end
139
139
 
140
- _/my_query_class.rb
140
+ # /my_query_class.rb
141
141
  class MyQueryClass < WiseGopher::Base
142
142
  query "SELECT title FROM articles"
143
143
 
@@ -147,13 +147,88 @@ class MyQueryClass < WiseGopher::Base
147
147
  end
148
148
  ```
149
149
 
150
- **If you don't give any block to `.row`, make sure you include `WiseGopher::Row` in your class.**
150
+ **If you don't give any block to `row`, make sure you include `WiseGopher::Row` in your class.**
151
151
 
152
+ ------
153
+ ## Raw params
154
+ If you need to dinamically interpolate raw SQL in your query, you can use `raw_param`. The value passed with `execute_with` will be interpolated in the base query before inserting the other params.
155
+ ```ruby
156
+ class AnnualReport < WiseGopher::Base
157
+ query <<-SQL
158
+ SELECT month, revenue
159
+ FROM heavy_computations
160
+ WHERE employee_id = {{ id }}
161
+ {{ order_by }}
162
+ SQL
163
+
164
+ param :id, :integer
165
+
166
+ raw_param :order_by
167
+ end
168
+
169
+ AnnualReport.execute_with(id: 1, order_by: "ORDER BY id ASC")
170
+ ```
171
+ executed query will look like this:
172
+ ```SQL
173
+ SELECT month, revenue
174
+ FROM heavy_computations
175
+ WHERE employee_id = ?
176
+ ORDER BY id ASC
177
+ ```
178
+ By default, `raw_param` is required but you can pass `optional: true`. You can then omit the param and the placeholder will be remove for this query instance.
179
+ ```ruby
180
+ AnnualReport.execute_with(id: 1, order_by: "ORDER BY id ASC")
181
+ ```
182
+ ```SQL
183
+ SELECT month, revenue
184
+ FROM heavy_computations
185
+ WHERE employee_id = ?
186
+ ```
187
+
188
+ You can also provide _prefix_ and/or _suffix_ to `raw_param` to make the `raw_param` clearer and the argument lighter.
189
+ ```ruby
190
+ class AnnualReport < WiseGopher::Base
191
+ query <<-SQL
192
+ SELECT month, revenue
193
+ FROM heavy_computations
194
+ WHERE employee_id = {{ id }}
195
+ {{ order_by }}
196
+ SQL
197
+
198
+ param :id, :integer
199
+
200
+ raw_param :order_by, prefix: "ORDER BY ", suffix: " ASC" # note the spacings
201
+ end
202
+ AnnualReport.execute_with(id: 1, order_by: "id")
203
+ ```
204
+ Finally, a default option is also supported, thus making the param optional:
205
+ ```ruby
206
+ class AnnualReport < WiseGopher::Base
207
+ query <<-SQL
208
+ SELECT month, revenue
209
+ FROM heavy_computations
210
+ WHERE employee_id = {{ id }}
211
+ {{ order_by }}
212
+ SQL
213
+
214
+ param :id, :integer
215
+
216
+ raw_param :order_by, prefix: " ORDER BY ", suffix: " ASC ", default: "id"
217
+ end
218
+ AnnualReport.execute_with(id: 1)
219
+ ```
220
+ executed query:
221
+ ```SQL
222
+ SELECT month, revenue
223
+ FROM heavy_computations
224
+ WHERE employee_id = ?
225
+ ORDER BY id ASC
226
+ ```
152
227
 
153
228
  ------
154
229
  ## Methods documentation
155
230
  ### WiseGopher::Base (class)
156
- #### .param
231
+ #### ::param
157
232
  ```ruby
158
233
  param(name, type, transform: nil)
159
234
  ```
@@ -162,10 +237,23 @@ Argument | Required | Descrition
162
237
  ------------ | ------------- | -------------
163
238
  name | true | The name of the parameter as written in the `{{ placeholder }}`
164
239
  type | true | The type of the column. It can be any type registred as ActiveRecord::Type. Including yours
165
- transform: | false | `Proc` or `Symbol`. An operation that will be call before creating the bind parameter when you call `.execute_with`.
240
+ transform: | false | `Proc` or `Symbol`. An operation that will be call before creating the bind parameter when you call `execute_with`.
241
+
242
+ #### ::raw_param
243
+ ```ruby
244
+ raw_param(name, prefix: nil, suffix: nil, default: nil, optional: false)
245
+ ```
246
+
247
+ Argument | Required | Descrition
248
+ ------------ | ------------- | -------------
249
+ name | true | The name of the parameter as written in the `{{ placeholder }}`
250
+ prefix: | false | The string to be inserted **before** the value passed as argument. No spaces will be added around to allow maximum customization.
251
+ suffix: | false | The string to be inserted **after** the value passed as argument. No spaces will be added around to allow maximum customization.
252
+ default: | false | The default value used if none is passed when calling the query
253
+ optional: | false | an empty string will be inserted in place of the placeholder if neither argument or default is provided.
166
254
 
167
255
  ### WiseGopher::Row (module)
168
- #### .column
256
+ #### ::column
169
257
  ```ruby
170
258
  column(name, type, transform: nil, as: nil)
171
259
  ```
@@ -180,10 +268,10 @@ as: | false | The name of the getter you want on the row instance for this colum
180
268
  ------
181
269
  ## Tips
182
270
  #### transform: argument as proc
183
- If you provide a proc to the `transform:` argument (either on `.column` or `.param`), you can expect one argument or none. If one argument is expected the value of the param or column will be passed.
271
+ If you provide a proc to the `transform:` argument (either on `column` or `param`), you can expect one argument or none. If one argument is expected the value of the param or column will be passed.
184
272
 
185
273
  #### Prepare query for later execution
186
- You can prepare the query with param without executing it by simply calling `.new` on your class and providing the params an later call `.execute`.
274
+ You can prepare the query with param without executing it by simply calling `new` on your class and providing the params an later call `execute`.
187
275
  ```ruby
188
276
  class PopularArticle < WiseGopher::Base
189
277
  query <<-SQL
@@ -203,7 +291,7 @@ last_month_articles.execute # => [#<PopularArticle::Row:0x0000560c37e9de48 ...>]
203
291
  ```
204
292
 
205
293
  #### Ignore column in result
206
- If for some reason, you have a column in your result that you don't want to retrieve on the row instances, you can use `.ignore`.
294
+ If for some reason, you have a column in your result that you don't want to retrieve on the row instances, you can use `ignore`.
207
295
  ```ruby
208
296
  class MyQuery < WiseGopher::Base
209
297
  query "SELECT title, rating FROM articles"
@@ -217,6 +305,45 @@ end
217
305
  MyQuery.execute # => no error raised
218
306
  ```
219
307
 
308
+ #### Array of value as parameter
309
+ You can pass an array as parameter value. The will then make a comma separated list of placeholders and pass the arguments as many bind parameters.
310
+ ```ruby
311
+ class MyQuery < WiseGopher::Base
312
+ query "SELECT title FROM articles WHERE rating in ({{ ratings }})"
313
+
314
+ param :ratings, :integer
315
+
316
+ row do
317
+ column :title, :string
318
+ end
319
+ end
320
+
321
+ MyQuery.execute_with(ratings: [1, 2])
322
+ # query will be "SELECT title FROM articles WHERE rating in (?, ?)"
323
+ ```
324
+
325
+ #### Classic param in raw_param
326
+ As the raw_params are interpolated before the classic params, you can have placeholders in them:
327
+ ```ruby
328
+ class MyQuery < WiseGopher::Base
329
+ query <<-SQL
330
+ SELECT title FROM articles
331
+ WHERE rating > ({{ ratings }})"
332
+ SQL
333
+
334
+ param :min_rating, :integer
335
+
336
+ raw_param :or_condition, prefix: " OR "
337
+
338
+ row do
339
+ column :title, :string
340
+ end
341
+ end
342
+
343
+ MyQuery.execute_with(min_rating: 1, or_condition: "rating = {{ min_rating }}")
344
+ # query will be "SELECT title FROM articles WHERE rating > ? OR rating = ?"
345
+ ```
346
+
220
347
  ------
221
348
 
222
349
  ## Contributing
data/lib/wise_gopher.rb CHANGED
@@ -4,6 +4,7 @@ require_relative "wise_gopher/version"
4
4
  require_relative "wise_gopher/base"
5
5
  require_relative "wise_gopher/column"
6
6
  require_relative "wise_gopher/param"
7
+ require_relative "wise_gopher/raw_param"
7
8
  require_relative "wise_gopher/row"
8
9
  require_relative "wise_gopher/errors"
9
10
 
@@ -7,7 +7,8 @@ module WiseGopher
7
7
  class Base
8
8
  def self.inherited(base)
9
9
  base.class_eval do
10
- @params = {}
10
+ @raw_params = {}
11
+ @params = {}
11
12
  end
12
13
  base.include Methods
13
14
  base.extend ClassMethods
@@ -15,16 +16,26 @@ module WiseGopher
15
16
 
16
17
  # class methods for WiseGopher::Base
17
18
  module ClassMethods
18
- attr_reader :row_class, :params
19
+ attr_reader :row_class, :params, :raw_params
19
20
 
20
21
  def query(query)
21
22
  const_set "QUERY", query.freeze
22
23
  end
23
24
 
24
25
  def param(name, type, transform = nil)
25
- param = WiseGopher::Param.new(name, type, transform)
26
+ new_param = WiseGopher::Param.new(name, type, transform)
26
27
 
27
- params[param.name] = param
28
+ ensure_param_name_is_available(new_param.name)
29
+
30
+ params[new_param.name] = new_param
31
+ end
32
+
33
+ def raw_param(name, **kwargs)
34
+ raw_param = WiseGopher::RawParam.new(name, **kwargs)
35
+
36
+ ensure_param_name_is_available(raw_param.name)
37
+
38
+ raw_params[raw_param.name] = raw_param
28
39
  end
29
40
 
30
41
  def row(base = nil, &block)
@@ -47,16 +58,26 @@ module WiseGopher
47
58
  new(inputs).execute
48
59
  end
49
60
 
61
+ def ensure_all_params_are_given(inputs = {})
62
+ missing_params = required_params.keys - inputs.keys.map(&:to_s)
63
+
64
+ raise WiseGopher::ArgumentError, required_params.slice(*missing_params) if missing_params.any?
65
+ end
66
+
50
67
  private
51
68
 
52
69
  def define_generic_row_class
53
70
  @row_class = const_set "Row", Class.new
54
71
  end
55
72
 
56
- def ensure_all_params_are_given(inputs = {})
57
- missing_params = params.keys - inputs.keys.map(&:to_s)
73
+ def ensure_param_name_is_available(name)
74
+ return unless params[name] || raw_params[name]
75
+
76
+ raise WiseGopher::ParamAlreadyDeclared, name
77
+ end
58
78
 
59
- raise WiseGopher::ArgumentError, params.slice(*missing_params) if missing_params.any?
79
+ def required_params
80
+ params.merge(raw_params.reject { |_name, raw_param| raw_param.optional? })
60
81
  end
61
82
  end
62
83
 
@@ -72,13 +93,15 @@ module WiseGopher
72
93
  @bind_symbol = WiseGopher.postgresql? ? +"$1" : "?"
73
94
  @query_prepared = false
74
95
 
96
+ self.class.ensure_all_params_are_given(inputs)
97
+
75
98
  prepare_query
76
99
  end
77
100
 
78
101
  def execute
79
102
  ensure_row_class_is_declared
80
103
 
81
- result = connection.exec_query(@query.squish, query_class.to_s, @binds, prepare: true)
104
+ result = connection.exec_query(query.squish, query_class.to_s, @binds, prepare: true)
82
105
 
83
106
  ensure_all_columns_are_declared(result)
84
107
 
@@ -88,24 +111,32 @@ module WiseGopher
88
111
  def prepare_query
89
112
  return if @query_prepared
90
113
 
91
- @query = query_class::QUERY.dup
114
+ prepare_raw_params
115
+
116
+ prepare_params
92
117
 
118
+ @query_prepared = true
119
+ end
120
+
121
+ private
122
+
123
+ def prepare_params
93
124
  query_class.params.each do |name, param|
94
125
  name = name.to_sym
95
126
  value = @inputs[name]
96
127
 
97
128
  bind_params(value, param)
98
129
  end
99
-
100
- @query_prepared = true
101
130
  end
102
131
 
103
- private
104
-
105
132
  def query_class
106
133
  self.class
107
134
  end
108
135
 
136
+ def query
137
+ @query ||= query_class::QUERY.dup
138
+ end
139
+
109
140
  def bind_params(value, param)
110
141
  if value.is_a? Array
111
142
  bind_collection_param(value, param)
@@ -117,19 +148,19 @@ module WiseGopher
117
148
  def bind_collection_param(values, param)
118
149
  bindings = values.map { use_bind_symbol }
119
150
 
120
- replace_binding_placeholder(param.name, bindings.join(", "))
151
+ replace_placeholder(param.name, bindings.join(", "))
121
152
 
122
153
  values.each { |value| register_binding(value, param) }
123
154
  end
124
155
 
125
156
  def bind_single_param(value, param)
126
- replace_binding_placeholder(param.name, use_bind_symbol)
157
+ replace_placeholder(param.name, use_bind_symbol)
127
158
 
128
159
  register_binding(value, param)
129
160
  end
130
161
 
131
- def replace_binding_placeholder(name, binding_symbol)
132
- @query.gsub!(/{{ ?#{name} ?}}/, binding_symbol)
162
+ def replace_placeholder(name, value_to_insert)
163
+ query.gsub!(/{{ ?#{name} ?}}/, value_to_insert)
133
164
  end
134
165
 
135
166
  def register_binding(value, param)
@@ -161,6 +192,15 @@ module WiseGopher
161
192
  def connection
162
193
  ActiveRecord::Base.connection
163
194
  end
195
+
196
+ def prepare_raw_params
197
+ query_class.raw_params.each do |name, param|
198
+ name = name.to_sym
199
+ value = @inputs[name]
200
+
201
+ replace_placeholder(name, param.to_s(value))
202
+ end
203
+ end
164
204
  end
165
205
  end
166
206
  end
@@ -11,7 +11,8 @@ module WiseGopher
11
11
 
12
12
  def initialize(params)
13
13
  @params = params.map do |name, param|
14
- "- \"#{name}\" (#{param.type.type})"
14
+ param_type = param.respond_to?(:type) ? param.type.type : :raw_param
15
+ "- \"#{name}\" (#{param_type})"
15
16
  end.join("\n")
16
17
  end
17
18
 
@@ -62,13 +63,17 @@ module WiseGopher
62
63
  # raised when custom row class is given but doesn't include WiseGopher::Row
63
64
  class RowClassNeedsRowModule < Error
64
65
  end
65
- end
66
66
 
67
- # connection;
68
- # class Query < WiseGopher::Base
69
- # query "SELECT title, rating FROM articles"
67
+ # raised when param are raw_param are declared with the same name
68
+ class ParamAlreadyDeclared < Error
69
+ attr_reader :param_name
70
+
71
+ def initialize(param_name)
72
+ @param_name = param_name
73
+ end
70
74
 
71
- # row do
72
- # column :title, :string
73
- # end
74
- # end; Query.execute
75
+ def message
76
+ "'#{param_name}' is already declared either as 'raw_param' or standard 'param'."
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WiseGopher
4
+ # Register query's raw_params and interpolate string in query
5
+ class RawParam
6
+ attr_reader :name, :optional, :default, :prefix, :suffix
7
+
8
+ def initialize(name, optional: false, default: nil, prefix: nil, suffix: nil)
9
+ @name = name.to_s.freeze
10
+ @optional = optional
11
+ @default = default
12
+ @prefix = prefix.to_s.freeze
13
+ @suffix = suffix.to_s.freeze
14
+ end
15
+
16
+ def to_s(string = nil)
17
+ raise ::ArgumentError, "value required" unless string || optional?
18
+
19
+ content = string || default
20
+
21
+ return "#{prefix}#{content}#{suffix}" if content
22
+
23
+ ""
24
+ end
25
+
26
+ def optional?
27
+ optional || !!default
28
+ end
29
+ end
30
+ end
@@ -3,7 +3,7 @@
3
3
  module WiseGopher
4
4
  module VERSION
5
5
  MAJOR = 0
6
- MINOR = 1
6
+ MINOR = 2
7
7
  PATCH = 0
8
8
 
9
9
  STRING = [MAJOR, MINOR, PATCH].join(".")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wise_gopher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - PageHey
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-06-29 00:00:00.000000000 Z
11
+ date: 2021-08-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -199,6 +199,7 @@ files:
199
199
  - lib/wise_gopher/column.rb
200
200
  - lib/wise_gopher/errors.rb
201
201
  - lib/wise_gopher/param.rb
202
+ - lib/wise_gopher/raw_param.rb
202
203
  - lib/wise_gopher/row.rb
203
204
  - lib/wise_gopher/version.rb
204
205
  - wise_gopher.gemspec
@@ -224,7 +225,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
224
225
  - !ruby/object:Gem::Version
225
226
  version: '0'
226
227
  requirements: []
227
- rubygems_version: 3.0.3
228
+ rubygems_version: 3.1.6
228
229
  signing_key:
229
230
  specification_version: 4
230
231
  summary: Encapsulate raw SQL queries and return result as plain Ruby objects, using