dynamic-records-meritfront 2.0.19 → 3.0.1

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: c23065ffa740c578b38c7311ada210295ad2b876240480109913eff333a7c7b6
4
- data.tar.gz: e38654aeb1f6e5f3d8a91a334b0489847674bde43451a7bb7f641770f5f061fb
3
+ metadata.gz: 6a2496e72da8096b231edb9fab7d9a469926bbcd9a698d0224a78ee7ed5e25a5
4
+ data.tar.gz: 23100e7e24dad43bdfeab2f1291d4616c72e3726eba69a1c3cb2be99cf73df19
5
5
  SHA512:
6
- metadata.gz: 63eb8b2cf1342d742e77140091583201dea03dccad9edb8aa78201d663eec39a352e9741978e967ba5fe8b0bfc461478328fe1e0dde46bba434b36495e3842fc
7
- data.tar.gz: 35cd5e867cd6853b7614ce0d67c2253794f31b03af59c0e7189a66bca30c4e8f7eaa984858a2bb659607c60b8dc8b4a29ed9ba9bc5d944435ab38a425283db01
6
+ metadata.gz: a2861fed694119175940f5153f429491cc25210611da866e55b5a52720610655a89b8291dd7bf1a1042fa155ce8c7dbeacbb846470e01645dda9381d868c6b42
7
+ data.tar.gz: 84110301448a31b997b6acbd89b99dfbe3d5b3c6432e7ba7cafc20b78bfa92a80818a4291da6a30090643fc7eec3da2028f8a30030339e0ac7cdd90f7c12d49a
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dynamic-records-meritfront (2.0.19)
4
+ dynamic-records-meritfront (3.0.1)
5
5
  hashid-rails
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -1,10 +1,33 @@
1
1
  # Dynamic Records Meritfront
2
2
 
3
- Dyanmic Records Meritfront contains some helpers methods for active record. These methods have the goal of allowing one to
3
+ Dyanmic Records Meritfront helps extend active record functionality to make it more dynamic. These methods have the goal of allowing one to
4
4
  1. communicate with the frontend quicker and more effectively through Global HashIds
5
- 2. communicate with the backend more effectively with raw sql queries. This becomes especially relevant when you hit the limits of Active Record Relations and the usual way of querying in rails. For instance, if you have a page-long dynamic sql query.
5
+ 2. communicate with the backend more effectively with sql queries. This becomes especially relevant when you hit the limits of Active Record Relations and the usual way of querying in rails. For instance, if you have dynamic sql queries that are hard to convert properly into ruby.
6
+ 3. add other helper methods to work with your database, such as checking if relations exist, or if a migration has been run.
6
7
 
7
- Note that postgres is a requirement for this gem. I dont tend to get much feedback, so any given would be appreciated.
8
+ Note that postgres is currently a requirement for this gem.
9
+
10
+ ## Basic Examples
11
+ ```ruby
12
+ # returns a json-like hash list of user data
13
+ users = ApplicationRecord.dynamic_sql(
14
+ 'get_5_users',
15
+ 'select * from users limit :our_limit',
16
+ our_limit: 5
17
+ )
18
+
19
+ #returns a list of users (each an instance of User)
20
+ users = User.dynamic_sql(
21
+ 'get_users_from_ids',
22
+ 'select * from users where id = ANY (:ids)',
23
+ ids: [1,2,3]
24
+ )
25
+
26
+ uhgid = users.first.hgid #returns a hashed global id like: 'gid://appname/User/K9YI4K'
27
+ user = User.locate_hgid(uhgid) #returns that user
28
+
29
+ #... and much more!
30
+ ```
8
31
 
9
32
  ## Installation
10
33
 
@@ -38,58 +61,21 @@ end
38
61
 
39
62
  These are methods written for easier sql usage.
40
63
 
41
- #### has_run_migration?(nm)
42
-
43
- put in a string name of the migration's class and it will say if it has allready run the migration.
44
- good during enum migrations as the code to migrate wont run if enumerate is there
45
- as it is not yet enumerated (causing an error when it loads the class that will have the
46
- enumeration in it). This can lead it to being impossible to commit clean code.
47
-
48
- <details><summary>example usage</summary>
49
- only load relationa if it exists in the database
50
-
51
- ```ruby
52
- if ApplicationRecord.has_run_migration?('UserImageRelationsTwo')
53
- class UserImageRelation < ApplicationRecord
54
- belongs_to :imageable, polymorphic: true
55
- belongs_to :image
56
- end
57
- else
58
- class UserImageRelation; end
59
- end
60
-
61
- ```
62
- </details>
63
-
64
- #### has_association?(*args)
65
-
66
- accepts a list, checks if the model contains those associations
67
-
68
- <details><summary>example usage</summary>
69
- Check if object is a votable class
70
-
71
- ```ruby
72
- obj = Comment.first
73
- obj.has_association?(:votes) #true
74
- obj = User.first
75
- obj.has_association?(:votes) #false
76
- ```
77
- </details>
78
-
79
64
  #### self.dynamic_sql(name, sql, opts = { })
80
65
  A better and safer way to write sql. Can return either a Hash, ActiveRecord::Response object, or an instantiated model.
66
+
81
67
  with options:
82
- - instantiate_class: returns User, Post, etc objects instead of straight sql output.
83
- I prefer doing the alterantive
84
- ```User.headache_sql(...)```
85
- which is also supported
86
- - prepare: sets whether the db will preprocess the strategy for lookup (defaults true) (have not verified the prepared-ness)
87
- - name_modifiers: allows one to change the preprocess associated name, useful in cases of dynamic sql.
68
+ - options not stated below: considered sql arguments, and will replace their ":option_name" with a sql argument. Always use sql arguments to avoid sql injection. Lists are converted into a format such as ```{1,2,3,4}```. Lists of lists are converted into ```(1,2,3), (4,5,6), (7,8,9)``` etc. So as to allow easy inserts/upserts.
69
+ - raw: whether to return a ActiveRecord::Response object or a hash when called on an abstract class (like ApplicationRecord). Default can be switched with DYNAMIC_SQL_RAW variable on the class level.
70
+ - instantiate_class: determines what format to return. Can return ActiveRecord objects (User, Post, etc), or whatever raw is set to. I prefer doing the alterantive ```User.dynamic_sql(...)``` which is also supported. For example, ```User.dynamic_sql(...)``` will return User records. ```ApplicationRecord.dynamic_sql(..., raw: false)``` will return a List of Hashes with the column names as keys. ```ApplicationRecord.dynamic_sql(..., raw: true)``` will return an ActiveRecord::Response.
71
+
72
+ other options:
73
+
74
+ - prepare: Defaults to true. Gets passed to ActiveRecord::Base.connection.exec_query as a parameter. Should change whether the command will be prepared, which means that on subsequent calls the command will be faster. Downsides are when, for example, the sql query has hard-coded arguments, the query always changes, causing technical issues as the number of prepared statements stack up.
75
+ - name_modifiers: allows one to change the associated name dynamically.
88
76
  - multi_query: allows more than one query (you can seperate an insert and an update with ';' I dont know how else to say it.)
89
- this disables other options including arguments (except name_modifiers). Not sure how it effects prepared statements.
90
- - 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?
91
- - raw: whether to return a ActiveRecord::Response object or a hash
92
- - other options: considered sql arguments
77
+ this disables other options including arguments (except name_modifiers). Not sure how it effects prepared statements. Not super useful.
78
+ - async: Defaults to false. 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?
93
79
 
94
80
  <details>
95
81
  <summary>example usage</summary>
@@ -135,16 +121,12 @@ This is an example of why this method is good for dynamic prepared statements.
135
121
  SELECT id FROM votes LIMIT 1
136
122
  }).first
137
123
  v.inspect # "#<Vote id: 696969>"
138
- v.dynamic # nil
139
124
 
140
125
  #get a cool test vote. Note that is_this_vote_cool is not on the vote table.
141
126
  test = Vote.dynamic_sql('test', %Q{
142
127
  SELECT id, 'yes' AS is_this_vote_cool FROM votes LIMIT 1
143
128
  }).first
144
- test.inspect # "#<Vote id: 696969>" we dont have the dynamic attributes as normal ones because of some implementation issues and some real issues to do with accidently logging sensative info. Implementation issues are to do with the fact that ActiveRecord::Base doesn't expect database columns to change randomly, and doesn't allow us to append to the attributes accessor.
145
- test.dynamic # <OpenStruct is_this_vote_cool='yes'>
146
- test.dynamic.is_this_vote_cool # "yes"
147
- test.dynamic[:is_this_vote_cool] #yes
129
+ test.inspect # #<Vote id: 696969, is_this_vote_cool: "yes"> #getting attributes added dynamically to the models, and also showing up on inspects, was... more difficult than i anticipated.
148
130
  ```
149
131
  </details>
150
132
 
@@ -154,7 +136,7 @@ Get users who match a list of ids. Uses a postgresql Array, see the potential is
154
136
 
155
137
  ```ruby
156
138
  id_list = [1,2,3]
157
- return User.headache_sql('get_usrs', %Q{
139
+ return User.dynamic_sql('get_usrs', %Q{
158
140
  SELECT * FROM users WHERE id = ANY (:id_list)
159
141
  }, id_list: id_list)
160
142
  ```
@@ -179,7 +161,7 @@ Do an upsert
179
161
  DO UPDATE SET updated_at = :time
180
162
  }, rows: rows, time: t)
181
163
  ```
182
- This will output sql similar to below. Note this can be done for multiple conversation_participants. Also note that it only sent one time variable as an argument as headache_sql detected that we were sending duplicate information.
164
+ This will output sql similar to below. Note this can be done for multiple conversation_participants. Also note that it only sent one time variable as an argument as dynamic_sql detected that we were sending duplicate information.
183
165
  ```sql
184
166
  INSERT INTO conversation_participants (user_id, conversation_id, invited_by, created_at, updated_at)
185
167
  VALUES ($1,$2,$3,$4,$4)
@@ -191,16 +173,17 @@ This will output sql similar to below. Note this can be done for multiple conver
191
173
 
192
174
 
193
175
  #### self.dynamic_preload(records, associations)
194
- Preloads from a list of records, and not from a ActiveRecord_Relation. This will be useful when using the above headache_sql method (as it returns a list of records, and not a record relation). This is basically the same as a normal relation preload but it works on a list.
176
+ Preloads from a list of records, and not from a ActiveRecord_Relation. This will be useful when using the above dynamic_sql method (as it returns a list of records, and not a record relation). This is basically the same as a normal relation preload but it works on a list.
195
177
 
196
178
  <details>
197
179
  <summary>example usage</summary>
198
180
  Preload :votes on some comments. :votes is an active record has_many relation.
199
181
 
200
182
  ```ruby
201
- comments = Comment.headache_sql('get_comments', %Q{
183
+ comments = Comment.dynamic_sql('get_comments', %Q{
202
184
  SELECT * FROM comments LIMIT 4
203
185
  })
186
+ comments.class.to_s # 'Array' note: not a relation.
204
187
  ApplicationRecord.headache_preload(comments, [:votes])
205
188
  puts comments[0].votes #this line should be preloaded and hence not call the database
206
189
 
@@ -208,12 +191,50 @@ Preload :votes on some comments. :votes is an active record has_many relation.
208
191
  user.comments.preload(:votes)
209
192
  ```
210
193
  </details>
211
-
194
+
195
+ #### has_run_migration?(nm)
196
+
197
+ put in a string name of the migration's class and it will say if it has allready run the migration.
198
+ good during enum migrations as the code to migrate wont run if enumerate is there
199
+ as it is not yet enumerated (causing an error when it loads the class that will have the
200
+ enumeration in it). This can lead it to being impossible to commit clean code.
201
+
202
+ <details><summary>example usage</summary>
203
+ only load relationa if it exists in the database
204
+
205
+ ```ruby
206
+ if ApplicationRecord.has_run_migration?('UserImageRelationsTwo')
207
+ class UserImageRelation < ApplicationRecord
208
+ belongs_to :imageable, polymorphic: true
209
+ belongs_to :image
210
+ end
211
+ else
212
+ class UserImageRelation; end
213
+ end
214
+
215
+ ```
216
+ </details>
217
+
218
+ #### has_association?(*args)
219
+
220
+ accepts a list of association names, checks if the model has those associations
221
+
222
+ <details><summary>example usage</summary>
223
+ Check if object is a votable class
224
+
225
+ ```ruby
226
+ obj = Comment.first
227
+ obj.has_association?(:votes) #true
228
+ obj = User.first
229
+ obj.has_association?(:votes) #false
230
+ ```
231
+ </details>
232
+
212
233
  #### self.dynamic_instaload_sql(name, insta_array, opts = { })
213
- *instaloads* a bunch of diffrent models at the same time by casting them to json before returning them. Kinda cool. Seems to be more efficient to preloading when i tested it.
234
+ *instaloads* a bunch of diffrent models at the same time by casting them to json before returning them. Kinda cool. Maybe a bit overcomplicated. Seems to be more efficient to preloading when i tested it.
214
235
  - name is passed to dynamic_sql and is the name of the sql request
215
236
  - opts are passed to dynamic_sql (except for the raw option which is set to true. Raw output is not allowed on this request)
216
- - insta-array is an array of instaload method outputs. See examples for more.
237
+ - requires a list of instaload method output which provides information for how to treat each sql block.
217
238
 
218
239
  <details>
219
240
  <summary>example usage</summary>
@@ -221,7 +242,7 @@ Preload :votes on some comments. :votes is an active record has_many relation.
221
242
 
222
243
  ```ruby
223
244
  # the ruby entered
224
- output = ApplicationRecord.swiss_instaload_sql('test', [
245
+ output = ApplicationRecord.dynamic_instaload_sql('test', [
225
246
  User.instaload('SELECT id FROM users WHERE users.id = ANY (:user_ids) AND users.created_at > :time', table_name: 'limited_users', relied_on: true),
226
247
  User.instaload(%Q{
227
248
  SELECT friends.smaller_user_id AS id, friends.bigger_user_id AS friended_to
@@ -261,26 +282,58 @@ the sql:
261
282
 
262
283
  the output:
263
284
  ```ruby
264
- {
265
- "limited_users"=>[#<User id: 3>, #<User id: 1>, #<User id: 4>],
266
- "users_friends"=>[
267
- #<User id: 21>,
268
- #<User id: 5>,
269
- ...],
270
- "users_follows"=> [
271
- {"followable_id"=>22, "follower_id"=>4},
272
- {"followable_id"=>23, "follower_id"=>4}, ...]
273
- }
285
+ {"limited_users"=>
286
+ [#<User id: 3>,
287
+ #<User id: 14>,
288
+ #<User id: 9>,
289
+ ...],
290
+ "users_friends"=>
291
+ [#<User id: 9, friended_to: 14>,
292
+ #<User id: 21, friended_to: 14>,
293
+ #<User id: 14, friended_to: 9>,
294
+ ...],
295
+ "users_follows"=>
296
+ [{"followable_id"=>931, "follower_id"=>23},
297
+ {"followable_id"=>932, "follower_id"=>23},
298
+ {"followable_id"=>935, "follower_id"=>19},
299
+ ...]}
300
+
301
+
302
+ ```
303
+ </details>
304
+
305
+ #### self.instaload(sql, table_name: nil, relied_on: false, dont_return: false)
306
+ A method used to prepare data for the dynamic_instaload_sql method. It returns a hash of options.
307
+ - klass called on: if called on an abstract class (ApplicationRecord) it will return a list of hashes with the data. Otherwise returns a list of the classes records.
308
+ - table_name: sets the name of the temporary postgresql table. This can then be used in further instaload sql snippets.
309
+ - relied_on: will make it so other instaload sql snippets can reference this table (it makes it use posrgresql's WITH operator)
310
+ - dont_return: when used with relied_on makes it so that this data is not returned to rails from the database.
311
+
312
+ note that the order of the instaload methods matter depending on how they reference eachother.
313
+ <details>
314
+ <summary> format data </summary>
315
+
316
+ ```ruby
317
+
318
+ User.instaload('SELECT id FROM users WHERE users.id = ANY (:user_ids) AND users.created_at > :time', table_name: 'limited_users', relied_on: true)
319
+ #output:
320
+ {
321
+ table_name: "limited_users",
322
+ klass: "User",
323
+ sql: "\tSELECT id FROM users WHERE users.id = ANY (:user_ids) AND users.created_at > :time",
324
+ relied_on: true,
325
+ dont_return: false
326
+ }
327
+
274
328
  ```
275
329
  </details>
276
330
 
277
331
  #### self.dynamic_attach(instaload_sql_output, base_name, attach_name, base_on: nil, attach_on: nil, one_to_one: false)
278
- taking the output of the dynamic_instaload_sql, this method attaches the models together so they have relations. Note: still undecided on using singleton attr_accessors or putting the relationship on the model.dynamic hash. Its currently using the accessors.
279
- - instaload_sql_output: output of above dynamic_instaload_sql
332
+ taking the output of the dynamic_instaload_sql, this method attaches the models together so they are attached.
280
333
  - base_name: the name of the table we will be attaching to
281
334
  - attach_name: the name of the table that will be attached
282
- - base_on: put a proc here to override the matching behavior on the base table. Default is {|user| user.id}
283
- - attach_on: put a proc here to override the matching behavior on the attach table. Default is {|post| post.user_id}
335
+ - base_on: put a proc here to override the matching key for the base table. Default is, for a user and post type, {|user| user.id}
336
+ - attach_on: put a proc here to override the matching key for the attach table. Default is, for a user and post type, {|post| post.user_id}
284
337
  - one_to_one: switches between a one-to-one relationship or not
285
338
 
286
339
  <details>
@@ -289,98 +342,21 @@ taking the output of the dynamic_instaload_sql, this method attaches the models
289
342
  ```ruby
290
343
 
291
344
  ApplicationRecord.dynamic_attach(out, 'limited_users', 'users_friends', attach_on: Proc.new {|users_friend|
292
- users_friend.dynamic[:friended_to]
345
+ users_friend.friended_to
293
346
  })
294
347
  ApplicationRecord.dynamic_attach(out, 'limited_users', 'users_follows', attach_on: Proc.new {|follow|
295
348
  follow['follower_id']
296
349
  })
297
- pp out['limited_users'].map{|o| {id: o.id, users_friends: o.dynamic.users_friends.first(4), users_follows: o.dynamic.users_follows.first(4)}}
298
-
299
- ```
300
-
301
- printed output:
302
- ```ruby
303
- [{:id=>3,
304
- :users_friends=>[#<User id: 21>, #<User id: 5>, #<User id: 6>],
305
- :users_follows=>
306
- [{"followable_id"=>935, "follower_id"=>3},
307
- {"followable_id"=>938, "follower_id"=>3},
308
- {"followable_id"=>939, "follower_id"=>3},
309
- {"followable_id"=>932, "follower_id"=>3}]},
310
- {:id=>14,
311
- :users_friends=>
312
- [#<User id: 18>, #<User id: 9>, #<User id: 21>, #<User id: 5>],
313
- :users_follows=>
314
- [{"followable_id"=>936, "follower_id"=>14},
315
- {"followable_id"=>937, "follower_id"=>14},
316
- {"followable_id"=>938, "follower_id"=>14},
317
- {"followable_id"=>939, "follower_id"=>14}]},
318
- {:id=>9,
319
- :users_friends=>
320
- [#<User id: 19>, #<User id: 15>, #<User id: 14>, #<User id: 7>],
321
- :users_follows=>
322
- [{"followable_id"=>938, "follower_id"=>9},
323
- {"followable_id"=>937, "follower_id"=>9},
324
- {"followable_id"=>932, "follower_id"=>9},
325
- {"followable_id"=>933, "follower_id"=>9}]}, ... ]
350
+ pp out['limited_users']
326
351
 
327
352
  ```
328
353
 
329
- </details>
330
-
331
- #### dynamic_print(v, print: true)
332
- - prints models along with dynamic variables using the pretty-printer. Fails in production to prevent leaking sensative information.
333
- - The reason this exists is that I could not override the inspect method for ActiveRecord. In my case, devise then overrode it from me. A little annoying. Because of that, this is now the best way to view both attributes and dynamic variables in the same location.
334
-
335
- <details>
336
- <summary> example using output of dynamic_attach example </summary>
337
-
338
- ```ruby
339
- ApplicationRecord.dynamic_print(out['limited_users'])
340
- ```
341
-
342
354
  printed output:
343
355
  ```ruby
344
- [#<struct DynamicRecordsMeritfront::RecordForPrint
345
- class="User",
346
- attributes={"id"=>3},
347
- dynamic=
348
- {:users_friends=>
349
- [#<struct DynamicRecordsMeritfront::RecordForPrint
350
- class="User",
351
- attributes={"id"=>5},
352
- dynamic={:friended_to=>3}>,
353
- #<struct DynamicRecordsMeritfront::RecordForPrint
354
- class="User",
355
- attributes={"id"=>6},
356
- dynamic={:friended_to=>3}>,
357
- #<struct DynamicRecordsMeritfront::RecordForPrint
358
- class="User",
359
- attributes={"id"=>21},
360
- dynamic={:friended_to=>3}>],
361
- :users_follows=>
362
- [{"followable_id"=>935, "follower_id"=>3},
363
- {"followable_id"=>938, "follower_id"=>3},
364
- {"followable_id"=>939, "follower_id"=>3},
365
- {"followable_id"=>932, "follower_id"=>3},
366
- {"followable_id"=>5, "follower_id"=>3},
367
- {"followable_id"=>4, "follower_id"=>3},
368
- {"followable_id"=>23, "follower_id"=>3},
369
- {"followable_id"=>22, "follower_id"=>3},
370
- {"followable_id"=>15, "follower_id"=>3},
371
- {"followable_id"=>6, "follower_id"=>3},
372
- {"followable_id"=>3, "follower_id"=>3},
373
- {"followable_id"=>8, "follower_id"=>3},
374
- {"followable_id"=>7, "follower_id"=>3},
375
- {"followable_id"=>1, "follower_id"=>3},
376
- {"followable_id"=>18, "follower_id"=>3},
377
- {"followable_id"=>16, "follower_id"=>3},
378
- {"followable_id"=>21, "follower_id"=>3},
379
- {"followable_id"=>9, "follower_id"=>3},
380
- {"followable_id"=>19, "follower_id"=>3}]}>,
381
-
382
- ...
383
-
356
+ #<User id: 3, users_friends: [#<User id: 5, friended_to: 3>, #<User id: 6, friended_to: 3>, #<User id: 21, friended_to: 3>], users_follows: [{"followable_id"=>935, "follower_id"=>3}, {"followable_id"=>938, "follower_id"=>3}, ...]>,
357
+ #<User id: 14, users_friends: [#<User id: 9, friended_to: 14>, #<User id: 21, friended_to: 14>, ...], users_follows: [{"followable_id"=>936, "follower_id"=>14}, {"followable_id"=>937, "follower_id"=>14}, {"followable_id"=>938, "follower_id"=>14}, ...]>,
358
+ #<User id: 9, users_friends: [#<User id: 14, friended_to: 9>, #<User id: 22, friended_to: 9>, ...], users_follows: [{"followable_id"=>938, "follower_id"=>9}, {"followable_id"=>937, "follower_id"=>9}, ...]>,
359
+ #<User id: 19, users_friends: [#<User id: 1, friended_to: 19>, #<User id: 18, friended_to: 19>, ...], users_follows: [{"followable_id"=>935, "follower_id"=>19}, {"followable_id"=>936, "follower_id"=>19}, {"followable_id"=>938, "follower_id"=>19}, ...]>,
384
360
  ```
385
361
 
386
362
  </details>
@@ -412,7 +388,8 @@ See the hashid-rails gem for more (https://github.com/jcypret/hashid-rails). Als
412
388
 
413
389
  ## Potential Issues
414
390
 
415
- This gem was made with a postgresql database. This could cause a lot of issues with the sql-related methods. I dont have the bandwidth to help switch it elsewhere.
391
+ - This gem was made with a postgresql database. This could cause a lot of issues with the sql-related methods if you do not. I dont have the bandwidth to help switch it elsewhere, but if you want to take charge of that, I would be more than happy to assist by answering questions an pointing out any areas that need transitioning.
392
+ - If you return a password column (for example) as pwd, this gem will accept that. That would mean that the password could me accessed as model.pwd. This is cool - until all passwords are getting logged in production servers. So be wary of accessing, storing, and logging of sensative information. Active Record has in built solutions for this type of data, as long as you dont change the column name. This gem is a sharp knife, its very versitile, but its also, you know, sharp.
416
393
 
417
394
  ## Changelog
418
395
 
@@ -438,6 +415,34 @@ This gem was made with a postgresql database. This could cause a lot of issues w
438
415
  - changed dynamic_attach so that it now uses the model.dynamic attribute, instead of using singleton classes. This is better practice, and also contains all the moving parts of this gem in one place.
439
416
  - added the dynamic_print method to easier see the objects one is working with.
440
417
 
418
+ 2.0.21
419
+ - figured out how to add to a model's @attributes, so .dynamic OpenStruct no longer needed, no longer need dynamic_print, singletons are out aswell. unexpected columns are now usable as just regular attributes.
420
+ - overrode inspect to show the dynamic attributes aswell, warning about passwords printed to logs etc.
421
+
422
+ 2.0.24
423
+ - added error logging in dynamic_sql method for the sql query when and if that fails. So just look at log file to see exactly what sql was running and what the args are.
424
+ - added a dont_return option to the instaload method which works with the relied_on option to have a normal WITH statement that is not returned.
425
+
426
+ 3.0.1
427
+ - Previous versions will break when two sql attributes unexpectantly share the same value. Yeah my bad, was trying to be fancy and decrease sql argument count.
428
+ - People using symbols as sql values (and expecting them to be turned to strings) may have breakages. (aka a sql option like "insert_list:
429
+ [[:a, 1], [:b, 2]]" will break)
430
+ - setting DYNAMIC_SQL_RAW apparently did nothing, you actually need to set DynamicRecordsMeritfront::DYNAMIC_SQL_RAW. Changed default to false, which may break things. But
431
+ since things may be broken already, it seemed like a good time to do this.
432
+ - Went to new version due to 1. a large functionality improvement, 2. the fact that previous versions are broken as explained above.
433
+ - more on breaking error
434
+ - got this error: ActiveRecord::StatementInvalid (PG::ProtocolViolation: ERROR: bind message supplies 3 parameters, but prepared statement "a27" requires 4)
435
+ - this tells me that names are not actually required to be unique for prepared statement identification, which was a bad assumption on my part
436
+ - this also tells me that uniq'ing variables to decrease the number of them was a bad idea which could cause random failures.
437
+ - functionality improvements
438
+ - The biggest change is that names are now optional! name_modifiers is now depreciated functionality as it serves no useful purpose. Will leave in for compatibility but take out of documentation. Used to think the name was related to prepared statements. This will lead simpler ruby code.
439
+ - If name is left out, the name will be set to the location in your app which called the method. For example, when dynamic_sql was called from irb, the name was: "(irb):45:in `irb_binding'". This is done using stack trace functionality.
440
+ - dynamic_instaload_sql is now just instaload_sql. dynamic_instaload_sql has been aliased.
441
+ - Name is optional on instaload_sql aswell
442
+ - MultiAttributeArrays (array's of arrays) which can be passed into dynamic_sql largely for inserts/upserts will now treat symbols as an attribute name. This leads to more consise sql without running into above error.
443
+ - When dynamic_sql errors out, it now posts some helpful information to the log.
444
+ - Added a test script. No experience testing, so its just a method you pass a model, and then it does a rollback to reverse any changes.
445
+
441
446
  ## Contributing
442
447
 
443
448
  Bug reports and pull requests are welcome on GitHub at https://github.com/LukeClancy/dynamic-records-meritfront. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/LukeClancy/dynamic-records-meritfront/blob/master/CODE_OF_CONDUCT.md).
@@ -1,5 +1,5 @@
1
1
 
2
2
  module DynamicRecordsMeritfront
3
- VERSION = '2.0.19'
3
+ VERSION = '3.0.1'
4
4
  end
5
5
  #this file gets overwritten automatically on minor updates, major ones need to be manually changed
@@ -11,15 +11,102 @@ module DynamicRecordsMeritfront
11
11
  module Hashid::Rails::ClassMethods
12
12
  alias hfind find_by_hashid
13
13
  end
14
-
15
14
  included do
16
15
  # include hash id gem
17
16
  include Hashid::Rails
18
17
  #should work, probably able to override by redefining in ApplicationRecord class.
19
18
  #Note we defined here as it breaks early on as Rails.application returns nil
20
19
  PROJECT_NAME = Rails.application.class.to_s.split("::").first.to_s.downcase
21
- DYNAMIC_SQL_RAW = true
20
+ DYNAMIC_SQL_RAW = false
22
21
  end
22
+ class DynamicSqlVariables
23
+ attr_accessor :sql_hash
24
+ attr_accessor :params
25
+ def initialize(params)
26
+ @sql_hash = {}
27
+ self.params = params
28
+ end
29
+
30
+ def add_key_value(key, value = nil)
31
+ value = params[key] if value.nil?
32
+ #tracks the variable and returns the keys sql variable number
33
+ sql_hash[key] ||= convert_to_query_attribute(key, value)
34
+ return sql_hash.keys.index(key) + 1
35
+ end
36
+
37
+ def next_sql_num
38
+ #gets the next sql variable number
39
+ sql_hash.keys.length + 1
40
+ end
41
+
42
+ def get_array_for_exec_query
43
+ sql_hash.values
44
+ end
45
+
46
+ #thank god for some stack overflow people are pretty awesome https://stackoverflow.com/questions/64894375/executing-a-raw-sql-query-in-rails-with-an-array-parameter-against-postgresql
47
+ #BigIntArray = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::BigInteger.new).freeze
48
+ #IntegerArray = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::Integer.new).freeze
49
+
50
+ #https://api.rubyonrails.org/files/activemodel/lib/active_model/type_rb.html
51
+ # active_model/type/helpers
52
+ # active_model/type/value
53
+ # active_model/type/big_integer
54
+ # active_model/type/binary
55
+ # active_model/type/boolean
56
+ # active_model/type/date
57
+ # active_model/type/date_time
58
+ # active_model/type/decimal
59
+ # active_model/type/float
60
+ # active_model/type/immutable_string
61
+ # active_model/type/integer
62
+ # active_model/type/string
63
+ # active_model/type/time
64
+ # active_model
65
+
66
+ DB_TYPE_MAPS = {
67
+ String => ActiveModel::Type::String,
68
+ Symbol => ActiveModel::Type::String,
69
+ Integer => ActiveModel::Type::BigInteger,
70
+ BigDecimal => ActiveRecord::Type::Decimal,
71
+ TrueClass => ActiveModel::Type::Boolean,
72
+ FalseClass => ActiveModel::Type::Boolean,
73
+ Date => ActiveModel::Type::Date,
74
+ DateTime => ActiveModel::Type::DateTime,
75
+ Time => ActiveModel::Type::Time,
76
+ Float => ActiveModel::Type::Float,
77
+ Array => Proc.new{ |first_el_class| ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(DB_TYPE_MAPS[first_el_class].new) }
78
+ }
79
+
80
+ def convert_to_query_attribute(name, v)
81
+ #yes its dumb I know dont look at me look at rails
82
+
83
+ # https://stackoverflow.com/questions/40407700/rails-exec-query-bindings-ignored
84
+ # binds = [ ActiveRecord::Relation::QueryAttribute.new(
85
+ # "id", 6, ActiveRecord::Type::Integer.new
86
+ # )]
87
+ # ApplicationRecord.connection.exec_query(
88
+ # 'SELECT * FROM users WHERE id = $1', 'sql', binds
89
+ # )
90
+
91
+ return v if v.kind_of? ActiveRecord::Relation::QueryAttribute #so users can have fine-grained control if they are trying to do something
92
+ #that we didn't handle properly.
93
+
94
+ type = DB_TYPE_MAPS[v.class]
95
+ if type.nil?
96
+ raise StandardError.new("#{name} (of value: #{v}, class: #{v.class}) unsupported class for ApplicationRecord#headache_sql")
97
+ elsif type.class == Proc
98
+ a = v[0]
99
+ # if a.nil?
100
+ # a = Integer
101
+ # elsif a.class == Array
102
+ a = a.nil? ? Integer : a.class
103
+ type = type.call(a)
104
+ else
105
+ type = type.new
106
+ end
107
+ ActiveRecord::Relation::QueryAttribute.new( name, v, type )
108
+ end
109
+ end
23
110
 
24
111
  class MultiRowExpression
25
112
  #this class is meant to be used in congunction with headache_sql method
@@ -42,27 +129,50 @@ module DynamicRecordsMeritfront
42
129
  #assuming we are putting in an array of arrays.
43
130
  self.val = val
44
131
  end
45
- def for_query(x = 0, unique_value_hash:)
132
+ def for_query(key, var_track)
46
133
  #accepts x = current number of variables previously processed
47
134
  #returns ["sql string with $# location information", variables themselves in order, new x]
48
- db_val = val.map{|attribute_array| "(#{
49
- attribute_index = 0
135
+ x = -1
136
+ db_val = val.map{|attribute_array| "(#{
50
137
  attribute_array.map{|attribute|
51
- prexist_num = unique_value_hash[attribute]
52
- if prexist_num
53
- attribute_array[attribute_index] = nil
54
- ret = "$#{prexist_num}"
55
- else
56
- unique_value_hash[attribute] = x
57
- ret = "$#{x}"
58
- x += 1
59
- end
60
- attribute_index += 1
61
- next ret
138
+ if attribute.kind_of? Symbol
139
+ #allow pointers to other more explicit variables through symbols
140
+ x = var_track.add_key_value(attribute, nil)
141
+ else
142
+ k = "#{key}_#{var_track.next_sql_num.to_s}"
143
+ x = var_track.add_key_value(k, attribute)
144
+ end
145
+ next "$" + x.to_s
62
146
  }.join(",")
63
147
  })"}.join(",")
64
- return db_val, val.flatten.select{|a| not a.nil?}, x
148
+ return db_val
149
+ end
150
+ end
151
+
152
+ def questionable_attribute_set(atr, value)
153
+ #this is needed on initalization of a new variable after the actual thing has been made already.
154
+
155
+ #set a bunk type of the generic value type
156
+ @attributes.instance_variable_get(:@types)[atr] = ActiveModel::Type::Value.new
157
+ #Set it
158
+ self[atr] = value
159
+ end
160
+
161
+ def inspect
162
+ #basically the same as the upstream active record function (as of october 25 2022 on AR V7.0.4)
163
+ #except that I changed self.class.attribute_names -> self.attribute_names to pick up our
164
+ #dynamic insanity. Was this a good idea? Well I guess its better than not doing it
165
+ inspection = if defined?(@attributes) && @attributes
166
+ self.attribute_names.filter_map do |name|
167
+ if _has_attribute?(name)
168
+ "#{name}: #{attribute_for_inspect(name)}"
169
+ end
170
+ end.join(", ")
171
+ else
172
+ "not initialized"
65
173
  end
174
+
175
+ "#<#{self.class} #{inspection}>"
66
176
  end
67
177
 
68
178
  module ClassMethods
@@ -204,70 +314,6 @@ module DynamicRecordsMeritfront
204
314
  end
205
315
  end
206
316
 
207
- #thank god for some stack overflow people are pretty awesome https://stackoverflow.com/questions/64894375/executing-a-raw-sql-query-in-rails-with-an-array-parameter-against-postgresql
208
- #BigIntArray = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::BigInteger.new).freeze
209
- #IntegerArray = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::Integer.new).freeze
210
-
211
- #https://api.rubyonrails.org/files/activemodel/lib/active_model/type_rb.html
212
- # active_model/type/helpers
213
- # active_model/type/value
214
- # active_model/type/big_integer
215
- # active_model/type/binary
216
- # active_model/type/boolean
217
- # active_model/type/date
218
- # active_model/type/date_time
219
- # active_model/type/decimal
220
- # active_model/type/float
221
- # active_model/type/immutable_string
222
- # active_model/type/integer
223
- # active_model/type/string
224
- # active_model/type/time
225
- # active_model
226
-
227
- DB_TYPE_MAPS = {
228
- String => ActiveModel::Type::String,
229
- Symbol => ActiveModel::Type::String,
230
- Integer => ActiveModel::Type::BigInteger,
231
- BigDecimal => ActiveRecord::Type::Decimal,
232
- TrueClass => ActiveModel::Type::Boolean,
233
- FalseClass => ActiveModel::Type::Boolean,
234
- Date => ActiveModel::Type::Date,
235
- DateTime => ActiveModel::Type::DateTime,
236
- Time => ActiveModel::Type::Time,
237
- Float => ActiveModel::Type::Float,
238
- Array => Proc.new{ |first_el_class| ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(DB_TYPE_MAPS[first_el_class].new) }
239
- }
240
-
241
- def convert_to_query_attribute(name, v)
242
- #yes its dumb I know dont look at me look at rails
243
-
244
- # https://stackoverflow.com/questions/40407700/rails-exec-query-bindings-ignored
245
- # binds = [ ActiveRecord::Relation::QueryAttribute.new(
246
- # "id", 6, ActiveRecord::Type::Integer.new
247
- # )]
248
- # ApplicationRecord.connection.exec_query(
249
- # 'SELECT * FROM users WHERE id = $1', 'sql', binds
250
- # )
251
-
252
- return v if v.kind_of? ActiveRecord::Relation::QueryAttribute #so users can have fine-grained control if they are trying to do something
253
- #that we didn't handle properly.
254
-
255
- type = DB_TYPE_MAPS[v.class]
256
- if type.nil?
257
- raise StandardError.new("#{v}'s class #{v.class} unsupported type right now for ApplicationRecord#headache_sql")
258
- elsif type.class == Proc
259
- a = v[0]
260
- # if a.nil?
261
- # a = Integer
262
- # elsif a.class == Array
263
- a = a.nil? ? Integer : a.class
264
- type = type.call(a)
265
- else
266
- type = type.new
267
- end
268
-
269
- ActiveRecord::Relation::QueryAttribute.new( name, v, type )
270
- end
271
317
  #allows us to preload on a list and not a active record relation. So basically from the output of headache_sql
272
318
  def dynamic_preload(records, associations)
273
319
  ActiveRecord::Associations::Preloader.new(records: records, associations: associations).call
@@ -275,122 +321,137 @@ module DynamicRecordsMeritfront
275
321
 
276
322
  alias headache_preload dynamic_preload
277
323
 
278
- def dynamic_sql(name, sql, opts = { }) #see below for opts
279
- # - instantiate_class - returns User, Post, etc objects instead of straight sql output.
280
- # I prefer doing the alterantive
281
- # User.headache_class(...)
282
- # which is also supported
283
- # - prepare sets whether the db will preprocess the strategy for lookup (defaults true) (I dont think turning this off works...)
284
- # - name_modifiers allows one to change the preprocess associated name, useful in cases of dynamic sql.
285
- # - multi_query allows more than one query (you can seperate an insert and an update with ';' I dont know how else to say it.)
286
- # this disables other options (except name_modifiers). Not sure how it effects prepared statements. Its a fairly useless
287
- # command as you can do multiple queries anyway with 'WITH' statements and also gain the other options.
288
- # - async does what it says but I haven't used it yet so. Probabably doesn't work
289
- # - raw switches between using a Hash or a ActiveRecord::Response object when used on a abstract class
290
-
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
324
+ def dynamic_sql(*args) #see below for opts
325
+ # call like: dynamic_sql(name, sql, option_1: 1, option_2: 2)
326
+ # or like: dynamic_sql(sql, {option: 1, option_2: 2})
327
+ # or like: dynamic_sql(sql, option: 1, option_2: 2)
328
+ # or just: dynamic_sql(sql)
329
+ #
330
+ # Options: (options not listed will be sql arguments)
331
+ # - instantiate_class - returns User, Post, etc objects instead of straight sql output.
332
+ # I prefer doing the alterantive
333
+ # User.headache_class(...)
334
+ # which is also supported
335
+ # - prepare sets whether the db will preprocess the strategy for lookup (defaults true) (I dont think turning this off works...)
336
+ # - name_modifiers allows one to change the preprocess associated name, useful in cases of dynamic sql.
337
+ # - multi_query allows more than one query (you can seperate an insert and an update with ';' I dont know how else to say it.)
338
+ # this disables other options (except name_modifiers). Not sure how it effects prepared statements. Its a fairly useless
339
+ # command as you can do multiple queries anyway with 'WITH' statements and also gain the other options.
340
+ # - async does what it says but I haven't used it yet so. Probabably doesn't work
341
+ # - raw switches between using a Hash or a ActiveRecord::Response object when used on a abstract class
342
+ args << {} unless args[-1].kind_of? Hash
343
+ if args.length == 3
344
+ name, sql, opts = args
345
+ elsif args.length == 2
346
+ sql, opts = args
347
+ #give default name functionality as a pointer to source code location
348
+ #of the method that called this. Love ruby. Meta up the a$$
349
+ first_app_stack_trace = caller[0...3].select{|str| not str.include?('dynamic_records_meritfront.rb')}.first
350
+ shorter_source_loc = first_app_stack_trace.split('/')[-1]
351
+ name = shorter_source_loc
352
+ else
353
+ raise StandardError.new("bad input to DynamicRecordsMeritfront#dynamic_sql method.")
354
+ end
355
+
356
+ #grab options from the opts hash
357
+ instantiate_class = opts.delete(:instantiate_class)
358
+ name_modifiers = opts.delete(:name_modifiers)
359
+ raw = opts.delete(:raw)
360
+ raw = DYNAMIC_SQL_RAW if raw.nil?
361
+ name_modifiers ||= []
362
+ prepare = opts.delete(:prepare) != false
363
+ multi_query = opts.delete(:multi_query) == true
364
+ async = opts.delete(:async) == true
365
+ params = opts
366
+
367
+ #unique value hash cuts down on the number of repeated arguments like in an update or insert statement
368
+ #by checking if there is an equal existing argument and then using that argument number instead.
369
+ #If this functionality is used at a lower level we should probably remove this.
370
+ #________________________________
371
+ #got this error: ActiveRecord::StatementInvalid (PG::ProtocolViolation: ERROR: bind message supplies 3 parameters, but prepared statement "a27" requires 4)
372
+ #this error tells me two things
373
+ # 1. the name of a sql statement actually has no effect on prepared statements (whoops).
374
+ # This means we should accept queries with no name.
375
+ # 2. Need to get rid of the unique variable name functionality which uniques all the variables
376
+ # to decrease the amount sent to database
377
+
378
+ #name_modifiers are super unnecessary now I realize the given name is not actually related
379
+ #to prepped statements. But will keep it as it is backwards compatitable and sorta useful maybe.
380
+ for mod in name_modifiers
381
+ name << "_#{mod.to_s}" unless mod.nil?
382
+ end
383
+ begin
384
+ var_track = DynamicSqlVariables.new(params)
385
+ unless multi_query
386
+ #https://stackoverflow.com/questions/49947990/can-i-execute-a-raw-sql-query-leverage-prepared-statements-and-not-use-activer/67442353#67442353
387
+ #change the keys to $1, $2 etc. this step is needed for ex. {id: 1, id_user: 2}.
388
+ #doing the longer ones first prevents id replacing :id_user -> $1_user
389
+ keys = params.keys.sort{|a,b| b.to_s.length <=> a.to_s.length}
390
+
391
+ for key in keys
392
+ #replace MultiRowExpressions
393
+ v = params[key]
394
+ #check if it looks like one
395
+ looks_like_multi_attribute_array = ((v.class == Array) and (not v.first.nil?) and (v.first.class == Array))
396
+ if v.class == MultiRowExpression or looks_like_multi_attribute_array
397
+ #we need to substitute with the correct sql now.
398
+ v = MultiRowExpression.new(v) if looks_like_multi_attribute_array #standardize
399
+ #process into appropriate sql while keeping track of variables
400
+ sql_for_replace = v.for_query(key, var_track)
401
+ #replace the key with the sql
402
+ sql.gsub!(":#{key}", sql_for_replace)
403
+ else
404
+ x = var_track.next_sql_num
405
+ if sql.gsub!(":#{key}", "$#{x}")
406
+ var_track.add_key_value(key, v)
407
+ end
408
+ end
409
+ end
410
+ sql_vals = var_track.get_array_for_exec_query
411
+ ret = ActiveRecord::Base.connection.exec_query sql, name, sql_vals, prepare: prepare, async: async
412
+ else
413
+ ret = ActiveRecord::Base.connection.execute sql, name
414
+ end
415
+ rescue Exception => e
416
+ #its ok if some of these are empty, just dont want the error
417
+ name ||= ''
418
+ sql ||= ''
419
+ sql_vals ||= ''
420
+ prepare ||= ''
421
+ async ||= ''
422
+ Rails.logger.error(%Q{
423
+ DynamicRecords#dynamic_sql debug info.
424
+ name: #{name.to_s}
425
+ sql: #{sql.to_s}
426
+ sql_vals: #{sql_vals.to_s}
427
+ prepare: #{prepare.to_s}
428
+ async: #{async.to_s}
429
+ })
430
+ raise e
431
+ end
432
+
433
+ #this returns a PG::Result object, which is pretty basic. To make this into User/Post/etc objects we do
434
+ #the following
435
+ if instantiate_class or not self.abstract_class
436
+ instantiate_class = self if not instantiate_class
437
+ #no I am not actually this cool see https://stackoverflow.com/questions/30826015/convert-pgresult-to-an-active-record-model
438
+ ret = ret.to_a
439
+ return ret.map{|r| dynamic_init(instantiate_class, r)}
440
+ # fields = ret.columns
441
+ # vals = ret.rows
442
+ # ret = vals.map { |v|
443
+ # dynamic_init()
444
+ # instantiate_class.instantiate(Hash[fields.zip(v)])
445
+ # }
446
+ else
447
+ if raw
448
+ return ret
449
+ else
450
+ return ret.to_a
451
+ end
452
+ end
453
+ end
386
454
  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
455
 
395
456
  def _dynamic_instaload_handle_with_statements(with_statements)
396
457
  %Q{WITH #{
@@ -401,7 +462,9 @@ module DynamicRecordsMeritfront
401
462
  end
402
463
 
403
464
  def _dynamic_instaload_union(insta_array)
404
- insta_array.map{|insta|
465
+ insta_array.select{|insta|
466
+ not insta[:dont_return]
467
+ }.map{|insta|
405
468
  start = "SELECT row_to_json(#{insta[:table_name]}.*) AS row, '#{insta[:klass]}' AS _klass, '#{insta[:table_name]}' AS _table_name FROM "
406
469
  if insta[:relied_on]
407
470
  ending = "#{insta[:table_name]}\n"
@@ -413,13 +476,31 @@ module DynamicRecordsMeritfront
413
476
  #{ 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
477
  end
415
478
 
416
- def dynamic_instaload_sql(name, insta_array, opts = { })
479
+ def instaload(sql, table_name: nil, relied_on: false, dont_return: false)
480
+ table_name ||= "_" + self.to_s.underscore.downcase.pluralize
481
+ klass = self.to_s
482
+ sql = "\t" + sql.strip
483
+ return {table_name: table_name, klass: klass, sql: sql, relied_on: relied_on, dont_return: dont_return}
484
+ end
485
+
486
+ def instaload_sql(*args) #name, insta_array, opts = { })
487
+ args << {} unless args[-1].kind_of? Hash
488
+ if args.length == 3
489
+ name, insta_array, opts = args
490
+ elsif args.length == 2
491
+ insta_array, opts = args
492
+ name = nil
493
+ else
494
+ raise StandardError.new("bad input to DynamicRecordsMeritfront#instaload_sql method.")
495
+ end
496
+
417
497
  with_statements = insta_array.select{|a| a[:relied_on]}
418
498
  sql = %Q{
419
499
  #{ _dynamic_instaload_handle_with_statements(with_statements) if with_statements.any? }
420
500
  #{ _dynamic_instaload_union(insta_array)}
421
501
  }
422
- ret_hash = insta_array.map{|ar| [ar[:table_name].to_s, []]}.to_h
502
+ returned_arrays = insta_array.select{|ar| not ar[:dont_return]}
503
+ ret_hash = returned_arrays.map{|ar| [ar[:table_name].to_s, []]}.to_h
423
504
  opts[:raw] = true
424
505
  ApplicationRecord.headache_sql(name, sql, opts).rows.each{|row|
425
506
  #need to pre-parsed as it has a non-normal output.
@@ -428,11 +509,94 @@ module DynamicRecordsMeritfront
428
509
  json = row[0]
429
510
  parsed = JSON.parse(json)
430
511
 
431
- ret_hash[table_name ].push dynamic_init(klass, parsed)
512
+ ret_hash[table_name].push dynamic_init(klass, parsed)
432
513
  }
433
514
  return ret_hash
434
515
  end
435
- alias swiss_instaload_sql dynamic_instaload_sql
516
+ alias swiss_instaload_sql instaload_sql
517
+ alias dynamic_instaload_sql instaload_sql
518
+
519
+ def test_drmf(model_with_an_id_column_and_timestamps)
520
+ m = model_with_an_id_column_and_timestamps
521
+ ar = m.superclass
522
+ mtname = m.table_name
523
+ ApplicationRecord.transaction do
524
+ puts 'test recieving columns not normally in the record.'
525
+ rec = m.dynamic_sql(%Q{
526
+ SELECT id, 5 AS random_column from #{mtname} LIMIT 10
527
+ }).first
528
+ raise StandardError.new('no id') unless rec.id
529
+ raise StandardError.new('no dynamic column') unless rec.random_column
530
+ puts 'pass 1'
531
+
532
+ puts 'test raw off with a custom name'
533
+ recs = ar.dynamic_sql('test_2', %Q{
534
+ SELECT id, 5 AS random_column from #{mtname} LIMIT 10
535
+ }, raw: false)
536
+ raise StandardError.new('not array of hashes') unless recs.first.class == Hash and recs.class == Array
537
+ rec = recs.first
538
+ raise StandardError.new('no id [raw off]') unless rec['id']
539
+ raise StandardError.new('no dynamic column [raw off]') unless rec['random_column']
540
+ puts 'pass 2'
541
+
542
+ puts 'test raw on'
543
+ recs = ar.dynamic_sql('test_3', %Q{
544
+ SELECT id, 5 AS random_column from #{mtname} LIMIT 10
545
+ }, raw: true)
546
+ raise StandardError.new('not raw') unless recs.class == ActiveRecord::Result
547
+ rec = recs.first
548
+ raise StandardError.new('no id [raw]') unless rec['id']
549
+ raise StandardError.new('no dynamic column [raw]') unless rec['random_column']
550
+ puts 'pass 3'
551
+
552
+ puts 'test when some of the variables are diffrent then the same (#see version 3.0.1 notes)'
553
+ x = Proc.new { |a, b|
554
+ recs = ar.dynamic_sql('test_4', %Q{
555
+ SELECT id, 5 AS random_column from #{mtname} WHERE id > :a LIMIT :b
556
+ }, a: a, b: b)
557
+ }
558
+ x.call(1, 2)
559
+ x.call(1, 1)
560
+ puts 'pass 4'
561
+
562
+ puts 'test MultiAttributeArrays, including symbols and duplicate values.'
563
+ time = DateTime.now
564
+ ids = m.limit(5).pluck(:id)
565
+ values = ids.map{|id|
566
+ [id, :time, time]
567
+ }
568
+ ar.dynamic_sql(%Q{
569
+ INSERT INTO #{mtname} (id, created_at, updated_at)
570
+ VALUES :values
571
+ ON CONFLICT (id) DO NOTHING
572
+ }, values: values, time: time)
573
+ puts 'pass 5'
574
+
575
+ puts 'test arrays'
576
+ recs = ar.dynamic_sql(%Q{
577
+ SELECT id from #{mtname} where id = ANY(:idz)
578
+ }, idz: ids, raw: false)
579
+ puts recs
580
+ raise StandardError.new('wrong length') if recs.length != 5
581
+ puts 'pass 6'
582
+
583
+
584
+ puts 'test instaload_sql'
585
+ out = ar.instaload_sql([
586
+ ar.instaload("SELECT id FROM users", relied_on: true, dont_return: true, table_name: "users_2"),
587
+ ar.instaload("SELECT id FROM users_2 WHERE id % 2 != 0 LIMIT :limit", table_name: 'a'),
588
+ m.instaload("SELECT id FROM users_2 WHERE id % 2 != 1 LIMIT :limit", table_name: 'b')
589
+ ], limit: 2)
590
+ puts out
591
+ raise StandardError.new('Bad return') if out["users_2"]
592
+ raise StandardError.new('Bad return') unless out["a"]
593
+ raise StandardError.new('Bad return') unless out["b"]
594
+ puts 'pass 7'
595
+
596
+ raise ActiveRecord::Rollback
597
+ #ApplicationRecord.dynamic_sql("SELECT * FROM")
598
+ end
599
+ end
436
600
 
437
601
  def dynamic_attach(instaload_sql_output, base_name, attach_name, base_on: nil, attach_on: nil, one_to_one: false)
438
602
  base_arr = instaload_sql_output[base_name]
@@ -445,22 +609,19 @@ module DynamicRecordsMeritfront
445
609
  base_class = base_arr.first.class
446
610
  base_class_is_hash = base_class <= Hash
447
611
 
448
- # attach name information for variables
449
- attach_name_sym = attach_name.to_sym
450
- attach_name_with_at = "@#{attach_name}"
451
612
 
452
613
  #variable accessors and defaults.
453
614
  base_arr.each{ |o|
454
615
  #
455
616
  # there is no way to set an attribute after instantiation I tried I looked
456
617
  # I dealt with silent breaks on symbol keys, I have wasted time, its fine.
457
- cancer = o.instance_variable_get(:@attributes).instance_variable_get(:@values)
618
+
458
619
  if not base_class_is_hash
459
620
  if one_to_one
460
621
  #attach name must be a string
461
- cancer[attach_name] = nil
622
+ o.questionable_attribute_set(attach_name, nil)
462
623
  else
463
- cancer[attach_name] = []
624
+ o.questionable_attribute_set(attach_name, [])
464
625
  end
465
626
  end
466
627
  # o.dynamic o.singleton_class.public_send(:attr_accessor, attach_name_sym) unless base_class_is_hash
@@ -538,11 +699,7 @@ module DynamicRecordsMeritfront
538
699
  alias swiss_attach dynamic_attach
539
700
 
540
701
  def zip_ar_result(x)
541
- fields = x.columns
542
- vals = x.rows
543
- vals.map { |v|
544
- Hash[fields.zip(v)]
545
- }
702
+ x.to_a
546
703
  end
547
704
 
548
705
  def dynamic_init(klass, input)
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: 2.0.19
4
+ version: 3.0.1
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-25 00:00:00.000000000 Z
11
+ date: 2022-12-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hashid-rails