dynamic-records-meritfront 1.1.10 → 2.0.2

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: 549b1d707e2419d0444845f8c15cd1564572c0ca68a72bf208a39be116feb5f3
4
- data.tar.gz: 5641ddc311be64741db802cc638b73b586be91d30edcdc171fc04949b3f57542
3
+ metadata.gz: c1482963a5871ccc6bb8aab98747903fff5f170ae7582fc78a1ea0b2a0897191
4
+ data.tar.gz: 204c714455e17ced54c0796596ec9d27bc4864174216f300915e013cc9742e92
5
5
  SHA512:
6
- metadata.gz: dc0a7c85bbb2ca8cdfd931c5ae9002c319809658d1437f68d3c4356758d71a5db7e277e2bcb87caa37b44608f13bc7a1c0fd38ad0e2aeb7bcc4de3a262519e28
7
- data.tar.gz: ce81fa5af8b092b10be95b3134b37051a321df53434220bca3ebbb9815b260f3fcba698891282eba179f04c22090466251c0d5a3f3e1e48d6e903b75e5197ce7
6
+ metadata.gz: 19cfdd5db92fcf93433b8d55f35f59a05d6f9f3c3d25f717a4869bf90b25628d4e8c9c583570e8c43fe3bda9336171b2e9c98238d7f6cddc412986684d99be4b
7
+ data.tar.gz: 60cd1f6b76073266fa918085787133d6024d3ad10851ca63147969fba3762775d6ca52daaa60bc5278329f95cbb30e0af12ae7f3502121c74c3023c89828399b
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dynamic-records-meritfront (1.1.10)
4
+ dynamic-records-meritfront (2.0.2)
5
5
  hashid-rails
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -33,31 +33,6 @@ class ApplicationRecord < ActiveRecord::Base
33
33
  end
34
34
  ```
35
35
 
36
- ### Hashed Global IDS
37
-
38
- hashed global ids look like this: "gid://meritfront/User/K9YI4K". They also have an optional tag so it can also look like "gid://meritfront/User/K9YI4K@user_image". They are based on global ids.
39
-
40
- I have been using hgids (Hashed Global IDs) for a while now and they have some unique benefits in front-end back-end communication. This is as they:
41
- 1. hash the id which is good practice
42
- 2. provide a way to have tags, this is good when updating different UI elements dynamically from the backend. For instance updating the @user_image without affecting the @user_name
43
- 3. Carry the class with them, this can allow for more abstract and efficient code, and prevents id collisions between diffrent classes.
44
-
45
- #### methods from the hashid-rails gem
46
-
47
- See the hashid-rails gem for more (https://github.com/jcypret/hashid-rails). Also note that I aliased .hashid to .hid and .find_by_hashid to .hfind
48
-
49
- #### methods from this gem
50
-
51
- 1. hgid(tag: nil) - get the hgid with optional tag. Aliased to ghid
52
- 2. hgid_as_selector(str, attribute: 'id') - get a css selector for the hgid, good for updating the front-end (especially over cable-ready and morphdom operations)
53
- 3. self.locate_hgid(hgid_string, with_associations: nil, returns_nil: false) - locates the database record from a hgid. Here are some examples of usage:
54
- - ApplicationRecord.locate_hgid(hgid) - <b>DANGEROUS</b> will return any object referenced by the hgid.
55
- - User.locate_hgid(hgid) - locates the User record but only if the hgid references a user class. Fires an error if not.
56
- - ApplicationRecord.locate_hgid(hgid, with_associations: [:votes]) - locates the record but only if the record's class has a :votes active record association. So for instance, you can accept only votable objects for upvote functionality. Fires an error if the hgid does not match.
57
- - User.locate_hgid(hgid, returns_nil: true) - locates the hgid but only if it is the user class. Returns nil if not.
58
- 4. get_hgid_tag(hgid) - returns the tag attached to the hgid
59
- 5. self.blind_hgid(id, tag) - creates a hgid without bringing the object down from the database. Useful with hashid-rails encode_id and decode_id methods
60
-
61
36
  ### SQL methods
62
37
 
63
38
  These are methods written for easier sql usage.
@@ -101,6 +76,7 @@ obj.has_association?(:votes) #false
101
76
  </details>
102
77
 
103
78
  #### self.headache_sql(name, sql, opts = { })
79
+ A better and safer way to write sql.
104
80
  with options:
105
81
  - instantiate_class: returns User, Post, etc objects instead of straight sql output.
106
82
  I prefer doing the alterantive
@@ -109,7 +85,7 @@ with options:
109
85
  - prepare: sets whether the db will preprocess the strategy for lookup (defaults true) (have not verified the prepared-ness)
110
86
  - name_modifiers: allows one to change the preprocess associated name, useful in cases of dynamic sql.
111
87
  - multi_query: allows more than one query (you can seperate an insert and an update with ';' I dont know how else to say it.)
112
- this disables other options (except name_modifiers). Not sure how it effects prepared statements.
88
+ this disables other options including arguments (except name_modifiers). Not sure how it effects prepared statements.
113
89
  - async: Gets passed to ActiveRecord::Base.connection.exec_query as a parameter. See that methods documentation for more. I was looking through the source code, and I think it only effects how it logs to the logfile?
114
90
  - other options: considered sql arguments
115
91
 
@@ -205,6 +181,31 @@ Preload :votes on some comments. :votes is an active record has_many relation.
205
181
  puts comments[0].votes #this line should be preloaded and hence not call the database
206
182
  ```
207
183
  </details>
184
+
185
+ ### Hashed Global IDS
186
+
187
+ hashed global ids look like this: "gid://meritfront/User/K9YI4K". They also have an optional tag so it can also look like "gid://meritfront/User/K9YI4K@user_image". They are based on global ids.
188
+
189
+ I have been using hgids (Hashed Global IDs) for a while now and they have some unique benefits in front-end back-end communication. This is as they:
190
+ 1. hash the id which is good practice
191
+ 2. provide a way to have tags, this is good when updating different UI elements dynamically from the backend. For instance updating the @user_image without affecting the @user_name
192
+ 3. Carry the class with them, this can allow for more abstract and efficient code, and prevents id collisions between diffrent classes.
193
+
194
+ #### methods from the hashid-rails gem
195
+
196
+ See the hashid-rails gem for more (https://github.com/jcypret/hashid-rails). Also note that I aliased .hashid to .hid and .find_by_hashid to .hfind
197
+
198
+ #### methods from this gem
199
+
200
+ 1. hgid(tag: nil) - get the hgid with optional tag. Aliased to ghid
201
+ 2. hgid_as_selector(str, attribute: 'id') - get a css selector for the hgid, good for updating the front-end (especially over cable-ready and morphdom operations)
202
+ 3. self.locate_hgid(hgid_string, with_associations: nil, returns_nil: false) - locates the database record from a hgid. Here are some examples of usage:
203
+ - ApplicationRecord.locate_hgid(hgid) - <b>DANGEROUS</b> will return any object referenced by the hgid.
204
+ - User.locate_hgid(hgid) - locates the User record but only if the hgid references a user class. Fires an error if not.
205
+ - ApplicationRecord.locate_hgid(hgid, with_associations: [:votes]) - locates the record but only if the record's class has a :votes active record association. So for instance, you can accept only votable objects for upvote functionality. Fires an error if the hgid does not match.
206
+ - User.locate_hgid(hgid, returns_nil: true) - locates the hgid but only if it is the user class. Returns nil if not.
207
+ 4. get_hgid_tag(hgid) - returns the tag attached to the hgid
208
+ 5. self.blind_hgid(id, tag: nil, encode: true) - creates a hgid without bringing the object down from the database. Useful with hashid-rails encode_id and decode_id methods.
208
209
 
209
210
  ## Potential Issues
210
211
 
@@ -212,12 +213,19 @@ This gem was made with a postgresql database. Although most of the headache_sql
212
213
 
213
214
  Let me know if this actually becomes an issue for someone and I will throw in a workaround.
214
215
 
216
+ ## Next Up
217
+ - I have the beginnings of something called swiss_instaload in mind, which will load multiple tables at the same time. For instance instead of Doing a ```usrs = User.all``` combined with a ```usrs.preload(:votes)```, which takes two sql requests, it could be done in one. Its kind of a crazy and dubious idea (efficiency wise), but I have a working prototype. It works by casting everything to json before returning it from the database. There might be a better way to do that long term though.
218
+ - will be changing names from headache_* which is a bit negative to swiss_* as in swiss_army_knife which is known for its wide versitility. headache names will become aliases.
219
+
215
220
  ## Changelog
216
221
 
217
222
  1.1.10
218
223
  - Added functionality in headache_sql where for sql arguments that are equal, we only use one sql argument instead of repeating arguments
219
224
  - Added functionality in headache_sql for 'multi row expressions' which are inputtable as an Array of Arrays. See the upsert example in the headache_sql documentation above for more.
220
225
  - Added a warning in the README for non-postgresql databases. Contact me if you hit issues and we can work it out.
226
+
227
+ 1.1.11
228
+ - Added encode option for blind_hgid to allow creation of just a general gid
221
229
 
222
230
  ## Contributing
223
231
 
@@ -1,5 +1,5 @@
1
1
 
2
2
  module DynamicRecordsMeritfront
3
- VERSION = '1.1.10'
3
+ VERSION = '2.0.2'
4
4
  end
5
5
  #this file gets overwritten automatically on minor updates, major ones need to be manually changed
@@ -18,53 +18,56 @@ module DynamicRecordsMeritfront
18
18
  #should work, probably able to override by redefining in ApplicationRecord class.
19
19
  #Note we defined here as it breaks early on as Rails.application returns nil
20
20
  PROJECT_NAME = Rails.application.class.to_s.split("::").first.to_s.downcase
21
+ DYNAMIC_SQL_RAW = true
22
+ attr_accessor :dynamic
21
23
  end
22
24
 
23
- class MultiRowExpression
24
- #this class is meant to be used in congunction with headache_sql method
25
- #Could be used like so in headache_sql:
26
-
27
- #ApplicationRecord.headache_sql( "teeeest", %Q{
28
- # INSERT INTO tests(id, username, is_awesome)
29
- # VALUES :rows
30
- # ON CONFLICT SET is_awesome = true
31
- #}, rows: [[1, luke, true], [2, josh, false]])
32
-
33
- #which would output this sql
34
-
35
- # INSERT INTO tests(id, username, is_awesome)
36
- # VALUES ($0,$1,$2),($3,$4,$5)
37
- # ON CONFLICT SET is_awesome = true
38
-
39
- attr_accessor :val
40
- def initialize(val)
41
- #assuming we are putting in an array of arrays.
42
- self.val = val
43
- end
44
- def for_query(x = 0, unique_value_hash:)
45
- #accepts x = current number of variables previously processed
46
- #returns ["sql string with $# location information", variables themselves in order, new x]
47
- db_val = val.map{|attribute_array| "(#{
48
- attribute_index = 0
49
- attribute_array.map{|attribute|
50
- prexist_num = unique_value_hash[attribute]
51
- if prexist_num
52
- attribute_array[attribute_index] = nil
53
- ret = "$#{prexist_num}"
54
- else
55
- unique_value_hash[attribute] = x
56
- ret = "$#{x}"
57
- x += 1
58
- end
59
- attribute_index += 1
60
- next ret
61
- }.join(",")
62
- })"}.join(",")
63
- return db_val, val.flatten.select{|a| not a.nil?}, x
64
- end
65
- end
25
+ class MultiRowExpression
26
+ #this class is meant to be used in congunction with headache_sql method
27
+ #Could be used like so in headache_sql:
28
+
29
+ #ApplicationRecord.headache_sql( "teeeest", %Q{
30
+ # INSERT INTO tests(id, username, is_awesome)
31
+ # VALUES :rows
32
+ # ON CONFLICT SET is_awesome = true
33
+ #}, rows: [[1, luke, true], [2, josh, false]])
34
+
35
+ #which would output this sql
36
+
37
+ # INSERT INTO tests(id, username, is_awesome)
38
+ # VALUES ($0,$1,$2),($3,$4,$5)
39
+ # ON CONFLICT SET is_awesome = true
40
+
41
+ attr_accessor :val
42
+ def initialize(val)
43
+ #assuming we are putting in an array of arrays.
44
+ self.val = val
45
+ end
46
+ def for_query(x = 0, unique_value_hash:)
47
+ #accepts x = current number of variables previously processed
48
+ #returns ["sql string with $# location information", variables themselves in order, new x]
49
+ db_val = val.map{|attribute_array| "(#{
50
+ attribute_index = 0
51
+ attribute_array.map{|attribute|
52
+ prexist_num = unique_value_hash[attribute]
53
+ if prexist_num
54
+ attribute_array[attribute_index] = nil
55
+ ret = "$#{prexist_num}"
56
+ else
57
+ unique_value_hash[attribute] = x
58
+ ret = "$#{x}"
59
+ x += 1
60
+ end
61
+ attribute_index += 1
62
+ next ret
63
+ }.join(",")
64
+ })"}.join(",")
65
+ return db_val, val.flatten.select{|a| not a.nil?}, x
66
+ end
67
+ end
66
68
 
67
69
  module ClassMethods
70
+
68
71
  def has_run_migration?(nm)
69
72
  #put in a string name of the class and it will say if it has allready run the migration.
70
73
  #good during enum migrations as the code to migrate wont run if enumerate is there
@@ -115,12 +118,10 @@ module DynamicRecordsMeritfront
115
118
 
116
119
  return migration_ran
117
120
  end
118
-
119
121
  def list_associations
120
122
  #lists associations (see has_association? below)
121
123
  reflect_on_all_associations.map(&:name)
122
124
  end
123
-
124
125
  def has_association?(*args)
125
126
  #checks whether current class has needed association (for example, checks it has comments)
126
127
  #associations can be seen in has_many belongs_to and other similar methods
@@ -131,12 +132,11 @@ module DynamicRecordsMeritfront
131
132
  args = args.flatten.map { |a| a.to_sym }
132
133
  associations = list_associations
133
134
  (args.length == (associations & args).length)
134
- end
135
-
136
- def blind_hgid(id, tag: nil)
135
+ end
136
+ def blind_hgid(id, tag: nil, encode: true)
137
137
  # this method is to get an hgid for a class without actually calling it down from the database.
138
138
  # For example Notification.blind_hgid 1 will give gid://PROJECT_NAME/Notification/69DAB69 etc.
139
- unless id.class == String
139
+ if id.class == Integer and encode
140
140
  id = self.encode_id id
141
141
  end
142
142
  gid = "gid://#{PROJECT_NAME}/#{self.to_s}/#{id}"
@@ -146,13 +146,11 @@ module DynamicRecordsMeritfront
146
146
  "#{gid}@#{tag}"
147
147
  end
148
148
  end
149
-
150
149
  def string_as_selector(str, attribute: 'id')
151
150
  #this is needed to allow us to quey various strange characters in the id etc. (see hgids)
152
151
  #also useful for querying various attributes
153
152
  return "*[#{attribute}=\"#{str}\"]"
154
153
  end
155
-
156
154
  def locate_hgid(hgid_string, with_associations: nil, returns_nil: false)
157
155
  if hgid_string == nil or hgid_string.class != String
158
156
  if returns_nil
@@ -199,7 +197,6 @@ module DynamicRecordsMeritfront
199
197
  raise StandardError.new 'Not the expected class, or a subclass of ApplicationRecord if called on that.'
200
198
  end
201
199
  end
202
-
203
200
  def get_hgid_tag(hgid_string)
204
201
  if hgid_string.include?('@')
205
202
  return hgid_string.split('@')[-1]
@@ -242,138 +239,318 @@ module DynamicRecordsMeritfront
242
239
  Array => Proc.new{ |first_el_class| ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(DB_TYPE_MAPS[first_el_class].new) }
243
240
  }
244
241
 
245
- def convert_to_query_attribute(name, v)
246
- #yes its dumb I know dont look at me look at rails
247
-
248
- # https://stackoverflow.com/questions/40407700/rails-exec-query-bindings-ignored
249
- # binds = [ ActiveRecord::Relation::QueryAttribute.new(
250
- # "id", 6, ActiveRecord::Type::Integer.new
251
- # )]
252
- # ApplicationRecord.connection.exec_query(
253
- # 'SELECT * FROM users WHERE id = $1', 'sql', binds
254
- # )
255
-
256
- return v if v.kind_of? ActiveRecord::Relation::QueryAttribute #so users can have fine-grained control if they are trying to do something
257
- #that we didn't handle properly.
258
-
259
- type = DB_TYPE_MAPS[v.class]
260
- if type.nil?
261
- raise StandardError.new("#{v}'s class #{v.class} unsupported type right now for ApplicationRecord#headache_sql")
262
- elsif type.class == Proc
263
- a = v[0]
264
- # if a.nil?
265
- # a = Integer
266
- # elsif a.class == Array
267
- a = a.nil? ? Integer : a.class
268
- type = type.call(a)
269
- else
270
- type = type.new
271
- end
272
-
273
- ActiveRecord::Relation::QueryAttribute.new( name, v, type )
274
- end
275
-
242
+ def convert_to_query_attribute(name, v)
243
+ #yes its dumb I know dont look at me look at rails
244
+
245
+ # https://stackoverflow.com/questions/40407700/rails-exec-query-bindings-ignored
246
+ # binds = [ ActiveRecord::Relation::QueryAttribute.new(
247
+ # "id", 6, ActiveRecord::Type::Integer.new
248
+ # )]
249
+ # ApplicationRecord.connection.exec_query(
250
+ # 'SELECT * FROM users WHERE id = $1', 'sql', binds
251
+ # )
252
+
253
+ return v if v.kind_of? ActiveRecord::Relation::QueryAttribute #so users can have fine-grained control if they are trying to do something
254
+ #that we didn't handle properly.
255
+
256
+ type = DB_TYPE_MAPS[v.class]
257
+ if type.nil?
258
+ raise StandardError.new("#{v}'s class #{v.class} unsupported type right now for ApplicationRecord#headache_sql")
259
+ elsif type.class == Proc
260
+ a = v[0]
261
+ # if a.nil?
262
+ # a = Integer
263
+ # elsif a.class == Array
264
+ a = a.nil? ? Integer : a.class
265
+ type = type.call(a)
266
+ else
267
+ type = type.new
268
+ end
269
+
270
+ ActiveRecord::Relation::QueryAttribute.new( name, v, type )
271
+ end
276
272
  #allows us to preload on a list and not a active record relation. So basically from the output of headache_sql
277
- def headache_preload(records, associations)
273
+ def dynamic_preload(records, associations)
278
274
  ActiveRecord::Associations::Preloader.new(records: records, associations: associations).call
279
275
  end
280
276
 
281
- def headache_sql(name, sql, opts = { }) #see below for opts
282
- # - instantiate_class - returns User, Post, etc objects instead of straight sql output.
283
- # I prefer doing the alterantive
284
- # User.headache_class(...)
285
- # which is also supported
286
- # - prepare sets whether the db will preprocess the strategy for lookup (defaults true) (I dont think turning this off works...)
287
- # - name_modifiers allows one to change the preprocess associated name, useful in cases of dynamic sql.
288
- # - multi_query allows more than one query (you can seperate an insert and an update with ';' I dont know how else to say it.)
289
- # this disables other options (except name_modifiers). Not sure how it effects prepared statements. Its a fairly useless
290
- # command as you can do multiple queries anyway with 'WITH' statements and also gain the other options.
291
- # - async does what it says but I haven't used it yet so. Probabably doesn't work
292
- #
293
- # Any other option is assumed to be a sql argument (see other examples in code base)
294
-
295
- #grab options from the opts hash
296
- instantiate_class = opts.delete(:instantiate_class)
297
- name_modifiers = opts.delete(:name_modifiers)
298
- name_modifiers ||= []
299
- prepare = opts.delete(:prepare) != false
300
- multi_query = opts.delete(:multi_query) == true
301
- async = opts.delete(:async) == true
302
- params = opts
303
-
304
- #unique value hash cuts down on the number of repeated arguments like in an update or insert statement
305
- #by checking if there is an equal existing argument and then using that argument number instead.
306
- #If this functionality is used at a lower level we should probably remove this.
307
- unique_value_hash = {}
308
-
309
- #allows dynamic sql prepared statements.
310
- for mod in name_modifiers
311
- name << "_#{mod.to_s}" unless mod.nil?
312
- end
313
-
314
- unless multi_query
315
- #https://stackoverflow.com/questions/49947990/can-i-execute-a-raw-sql-query-leverage-prepared-statements-and-not-use-activer/67442353#67442353
316
- #change the keys to $1, $2 etc. this step is needed for ex. {id: 1, id_user: 2}.
317
- #doing the longer ones first prevents id replacing :id_user -> 1_user
318
- keys = params.keys.sort{|a,b| b.to_s.length <=> a.to_s.length}
319
- sql_vals = []
320
- x = 1
321
- for key in keys
322
- #replace the key with $1, $2 etc
323
- v = params[key]
324
-
325
- #this is where we guess what it is
326
- looks_like_multi_attribute_array = ((v.class == Array) and (not v.first.nil?) and (v.first.class == Array))
327
-
328
- if v.class == MultiRowExpression or looks_like_multi_attribute_array
329
- #it looks like or is a multi-row expression (like those in an insert statement)
330
- v = MultiRowExpression.new(v) if looks_like_multi_attribute_array
331
- #process into usable information
332
- sql_for_replace, mat_vars, new_x = v.for_query(x, unique_value_hash: unique_value_hash)
333
- #replace the key with the sql
334
- if sql.gsub!(":#{key}", sql_for_replace) != nil
335
- #if successful set the new x number and append variables to our sql variables
336
- x = new_x
337
- name_num = 0
338
- mat_vars.each{|mat_var|
339
- name_num += 1
340
- sql_vals << convert_to_query_attribute("#{key}_#{name_num}", mat_var)
341
- }
342
- end
343
- else
344
- prexist_arg_num = unique_value_hash[v]
345
- if prexist_arg_num
346
- sql.gsub!(":#{key}", "$#{prexist_arg_num}")
347
- else
348
- if sql.gsub!(":#{key}", "$#{x}") == nil
349
- #nothing changed, param not used, delete it
350
- params.delete key
351
- else
352
- unique_value_hash[v] = x
353
- sql_vals << convert_to_query_attribute(key, v)
354
- x += 1
355
- end
356
- end
357
- end
358
- end
359
- ret = ActiveRecord::Base.connection.exec_query sql, name, sql_vals, prepare: prepare, async: async
360
- else
361
- ret = ActiveRecord::Base.connection.execute sql, name
362
- end
363
-
364
- #this returns a PG::Result object, which is pretty basic. To make this into User/Post/etc objects we do
365
- #the following
366
- if instantiate_class or self != ApplicationRecord
367
- instantiate_class = self if not instantiate_class
368
- #no I am not actually this cool see https://stackoverflow.com/questions/30826015/convert-pgresult-to-an-active-record-model
369
- fields = ret.columns
370
- vals = ret.rows
371
- ret = vals.map { |v|
372
- instantiate_class.instantiate(Hash[fields.zip(v)])
373
- }
374
- end
375
- ret
376
- end
277
+ alias headache_preload dynamic_preload
278
+
279
+ def dynamic_sql(name, sql, opts = { }) #see below for opts
280
+ # - instantiate_class - returns User, Post, etc objects instead of straight sql output.
281
+ # I prefer doing the alterantive
282
+ # User.headache_class(...)
283
+ # which is also supported
284
+ # - prepare sets whether the db will preprocess the strategy for lookup (defaults true) (I dont think turning this off works...)
285
+ # - name_modifiers allows one to change the preprocess associated name, useful in cases of dynamic sql.
286
+ # - multi_query allows more than one query (you can seperate an insert and an update with ';' I dont know how else to say it.)
287
+ # this disables other options (except name_modifiers). Not sure how it effects prepared statements. Its a fairly useless
288
+ # command as you can do multiple queries anyway with 'WITH' statements and also gain the other options.
289
+ # - async does what it says but I haven't used it yet so. Probabably doesn't work
290
+ # - instaload is actually insane just look at the example in the readme yolo
291
+ #
292
+ # Any other option is assumed to be a sql argument (see other examples in code base)
293
+
294
+ #grab options from the opts hash
295
+ instantiate_class = opts.delete(:instantiate_class)
296
+ name_modifiers = opts.delete(:name_modifiers)
297
+ raw = opts.delete(:raw)
298
+ raw = DYNAMIC_SQL_RAW if raw.nil?
299
+ name_modifiers ||= []
300
+ prepare = opts.delete(:prepare) != false
301
+ multi_query = opts.delete(:multi_query) == true
302
+ async = opts.delete(:async) == true
303
+ params = opts
304
+
305
+ #unique value hash cuts down on the number of repeated arguments like in an update or insert statement
306
+ #by checking if there is an equal existing argument and then using that argument number instead.
307
+ #If this functionality is used at a lower level we should probably remove this.
308
+ unique_value_hash = {}
309
+
310
+ #allows dynamic sql prepared statements.
311
+ for mod in name_modifiers
312
+ name << "_#{mod.to_s}" unless mod.nil?
313
+ end
314
+
315
+ unless multi_query
316
+ #https://stackoverflow.com/questions/49947990/can-i-execute-a-raw-sql-query-leverage-prepared-statements-and-not-use-activer/67442353#67442353
317
+ #change the keys to $1, $2 etc. this step is needed for ex. {id: 1, id_user: 2}.
318
+ #doing the longer ones first prevents id replacing :id_user -> 1_user
319
+ keys = params.keys.sort{|a,b| b.to_s.length <=> a.to_s.length}
320
+ sql_vals = []
321
+ x = 1
322
+ for key in keys
323
+ #replace the key with $1, $2 etc
324
+ v = params[key]
325
+
326
+ #this is where we guess what it is
327
+ looks_like_multi_attribute_array = ((v.class == Array) and (not v.first.nil?) and (v.first.class == Array))
328
+
329
+ if v.class == MultiRowExpression or looks_like_multi_attribute_array
330
+ #it looks like or is a multi-row expression (like those in an insert statement)
331
+ v = MultiRowExpression.new(v) if looks_like_multi_attribute_array
332
+ #process into usable information
333
+ sql_for_replace, mat_vars, new_x = v.for_query(x, unique_value_hash: unique_value_hash)
334
+ #replace the key with the sql
335
+ if sql.gsub!(":#{key}", sql_for_replace) != nil
336
+ #if successful set the new x number and append variables to our sql variables
337
+ x = new_x
338
+ name_num = 0
339
+ mat_vars.each{|mat_var|
340
+ name_num += 1
341
+ sql_vals << convert_to_query_attribute("#{key}_#{name_num}", mat_var)
342
+ }
343
+ end
344
+ else
345
+ prexist_arg_num = unique_value_hash[v]
346
+ if prexist_arg_num
347
+ sql.gsub!(":#{key}", "$#{prexist_arg_num}")
348
+ else
349
+ if sql.gsub!(":#{key}", "$#{x}") == nil
350
+ #nothing changed, param not used, delete it
351
+ params.delete key
352
+ else
353
+ unique_value_hash[v] = x
354
+ sql_vals << convert_to_query_attribute(key, v)
355
+ x += 1
356
+ end
357
+ end
358
+ end
359
+ end
360
+ ret = ActiveRecord::Base.connection.exec_query sql, name, sql_vals, prepare: prepare, async: async
361
+ else
362
+ ret = ActiveRecord::Base.connection.execute sql, name
363
+ end
364
+
365
+ #this returns a PG::Result object, which is pretty basic. To make this into User/Post/etc objects we do
366
+ #the following
367
+ if instantiate_class or not self.abstract_class
368
+ instantiate_class = self if not instantiate_class
369
+ #no I am not actually this cool see https://stackoverflow.com/questions/30826015/convert-pgresult-to-an-active-record-model
370
+ ret = zip_ar_result(ret)
371
+ return ret.map{|r| dynamic_init(instantiate_class, r)}
372
+ # fields = ret.columns
373
+ # vals = ret.rows
374
+ # ret = vals.map { |v|
375
+ # dynamic_init()
376
+ # instantiate_class.instantiate(Hash[fields.zip(v)])
377
+ # }
378
+ else
379
+ if raw
380
+ return ret
381
+ else
382
+ return zip_ar_result(ret)
383
+ end
384
+ end
385
+ end
386
+ alias headache_sql dynamic_sql
387
+
388
+ def instaload(sql, table_name: nil, relied_on: false)
389
+ table_name ||= "_" + self.to_s.underscore.downcase.pluralize
390
+ klass = self.to_s
391
+ sql = "\t" + sql.strip
392
+ return {table_name: table_name, klass: klass, sql: sql, relied_on: relied_on}
393
+ end
394
+
395
+ def _dynamic_instaload_handle_with_statements(with_statements)
396
+ %Q{WITH #{
397
+ with_statements.map{|ws|
398
+ "#{ws[:table_name]} AS (\n#{ws[:sql]}\n)"
399
+ }.join(", \n")
400
+ }}
401
+ end
402
+
403
+ def _dynamic_instaload_union(insta_array)
404
+ insta_array.map{|insta|
405
+ start = "SELECT row_to_json(#{insta[:table_name]}.*) AS row, '#{insta[:klass]}' AS _klass, '#{insta[:table_name]}' AS _table_name FROM "
406
+ if insta[:relied_on]
407
+ ending = "#{insta[:table_name]}\n"
408
+ else
409
+ ending = "(\n#{insta[:sql]}\n) AS #{insta[:table_name]}\n"
410
+ end
411
+ next start + ending
412
+ }.join(" UNION ALL \n")
413
+ #{ other_statements.map{|os| "SELECT row_to_json(#{os[:table_name]}.*) AS row, '#{os[:klass]}' AS _klass FROM (\n#{os[:sql]}\n)) AS #{os[:table_name]}\n" }.join(' UNION ALL ')}
414
+ end
415
+
416
+ def dynamic_instaload_sql(name, insta_array, opts = { })
417
+ with_statements = insta_array.select{|a| a[:relied_on]}
418
+ sql = %Q{
419
+ #{ _dynamic_instaload_handle_with_statements(with_statements) if with_statements.any? }
420
+ #{ _dynamic_instaload_union(insta_array)}
421
+ }
422
+ ret_hash = insta_array.map{|ar| [ar[:table_name].to_s, []]}.to_h
423
+ ApplicationRecord.headache_sql(name, sql, opts).rows.each{|row|
424
+ #need to pre-parsed as it has a non-normal output.
425
+ table_name = row[2]
426
+ klass = row[1].constantize
427
+ json = row[0]
428
+ parsed = JSON.parse(json)
429
+
430
+ ret_hash[table_name ].push dynamic_init(klass, parsed)
431
+ }
432
+ return ret_hash
433
+ end
434
+ alias swiss_instaload_sql dynamic_instaload_sql
435
+
436
+ def dynamic_attach(instaload_sql_output, base_name, attach_name, base_on: nil, attach_on: nil, one_to_one: false, debug: false)
437
+ base_arr = instaload_sql_output[base_name]
438
+
439
+ #return if there is nothing for us to attach to.
440
+ return unless base_arr.any?
441
+
442
+ #set variables for neatness and so we dont compute each time
443
+ # base class information
444
+ base_class = base_arr.first.class
445
+ base_class_is_hash = base_class <= Hash
446
+ # attach name information for variables
447
+ attach_name_sym = attach_name.to_sym
448
+ attach_name_with_at = "@#{attach_name}"
449
+
450
+ #variable accessors and defaults.
451
+ base_arr.each{|o|
452
+ o.singleton_class.public_send(:attr_accessor, attach_name_sym) unless base_class_is_hash
453
+ o.instance_variable_set(attach_name_with_at, []) unless one_to_one
454
+ }
455
+
456
+ #make sure the attach class has something going on
457
+ attach_arr = instaload_sql_output[attach_name]
458
+ return unless attach_arr.any?
459
+
460
+ # attach class information
461
+ attach_class = attach_arr.first.class
462
+ attach_class_is_hash = attach_class <= Hash
463
+
464
+ # default attach column info
465
+ default_attach_col = (base_class.to_s.downcase + "_id")
466
+
467
+ #decide on the method of getting the matching id for the base table
468
+ unless base_on
469
+ if base_class_is_hash
470
+ base_on = Proc.new{|x| x['id']}
471
+ else
472
+ base_on = Proc.new{|x| x.id}
473
+ end
474
+ end
475
+
476
+ #return an id->object hash for the base table for better access
477
+ h = base_arr.map{|o|
478
+ [base_on.call(o), o]
479
+ }.to_h
480
+
481
+ #decide on the method of getting the matching id for the attach table
482
+ unless attach_on
483
+ if attach_class_is_hash
484
+ attach_on = Proc.new{|x| x[default_attach_col]}
485
+ else
486
+ attach_on = Proc.new{|x|
487
+ x.method(default_attach_col).call
488
+ }
489
+ end
490
+ end
491
+
492
+ # if debug
493
+ # Rails.logger.info(base_arr.map{|b|
494
+ # base_on.call(b)
495
+ # })
496
+ # Rails.logger.info(attach_arr.map{|a|
497
+ # attach_on.call(a)
498
+ # })
499
+ # end
500
+
501
+ #method of adding the object to the base
502
+ #(b=base, a=attach)
503
+ add_to_base = Proc.new{|b, a|
504
+ if one_to_one
505
+ if base_class_is_hash
506
+ b[attach_name] = a
507
+ else
508
+ b.instance_variable_set(attach_name_with_at, a)
509
+ end
510
+ else
511
+ if base_class_is_hash
512
+ b[attach_name].push a
513
+ else
514
+ b.instance_variable_get(attach_name_with_at).push a
515
+ end
516
+ end
517
+ }
518
+
519
+ #for every attachable
520
+ # 1. match base id to the attach id (both configurable)
521
+ # 2. cancel out if there is no match
522
+ # 3. otherwise add to the base object.
523
+ attach_arr.each{|attach|
524
+ if out = attach_on.call(attach) #you can use null to escape the vals
525
+ if base = h[out] #it is also escaped if no base element is found
526
+ add_to_base.call(base, attach)
527
+ end
528
+ end
529
+ }
530
+ return attach_arr
531
+ end
532
+ alias swiss_attach dynamic_attach
533
+
534
+ def zip_ar_result(x)
535
+ fields = x.columns
536
+ vals = x.rows
537
+ vals.map { |v|
538
+ Hash[fields.zip(v)]
539
+ }
540
+ end
541
+
542
+ def dynamic_init(klass, input)
543
+ if klass.abstract_class
544
+ return input
545
+ else
546
+ #handle attributes through ar if allowed. Throws an error on unkown variables, except apparently for devise classes? 😡
547
+ active_record_handled = input.slice(*(klass.attribute_names & input.keys))
548
+ record = klass.instantiate(active_record_handled)
549
+ #set those that were not necessarily expected
550
+ record.dynamic = input.slice(*(input.keys - klass.attribute_names)).transform_keys{|k|k.to_sym}
551
+ return record
552
+ end
553
+ end
377
554
 
378
555
  def quick_safe_increment(id, col, val)
379
556
  where(id: id).update_all("#{col} = #{col} + #{val}")
@@ -415,4 +592,4 @@ module DynamicRecordsMeritfront
415
592
  self.class.where(id: self.id).update_all("#{col} = #{col} + #{val}")
416
593
  end
417
594
 
418
- end
595
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynamic-records-meritfront
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.10
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luke Clancy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-10-13 00:00:00.000000000 Z
11
+ date: 2022-10-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hashid-rails