wise_gopher 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: 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